Column Sizing
Column Sizing
Column sizing gives every column a width, a minimum, and a maximum, and lets you override the width per column at runtime. The package computes numbers; you decide what they mean in your markup, which is almost always pixels. This page ports TanStack Table's Column Sizing (React) Guide.
If you want the user to drag a column edge to change a width, that is Column Resizing, which is a drag interaction built on top of the state described here.
State
Column sizing owns one state slice: the committed width of each column, keyed by column id.
-- in Table.State
columnSizing : Dict String Float
-- in Table.initialState
columnSizing = Dict.empty
A column with no entry falls back to its own size, and then to the table's
defaultColumn. An entry is a raw number; the clamping between minSize and
maxSize happens on read, in
getColumnSize, so a committed
width outside the bounds is stored as written and reported clamped.
Config options
| Option | Type | Default | Description |
|---|---|---|---|
defaultColumn |
SizeDefaults |
{ size = 150, minSize = 20, maxSize = 9007199254740991 } |
The sizing every column starts from. |
SizeDefaults is a plain record:
type alias SizeDefaults =
{ size : Float
, minSize : Float
, maxSize : Float
}
9007199254740991 is JavaScript's Number.MAX_SAFE_INTEGER, which is what
TanStack uses for "no maximum".
Set it with withDefaultColumn,
which is TanStack's tableOptions.defaultColumn:
config : Table.Config Person
config =
Table.config
[ Table.column "firstName" (.firstName >> Value.String)
|> Table.withHeader "First name"
|> Table.withSize 220
, Table.column "lastName" (.lastName >> Value.String)
|> Table.withHeader "Last name"
, Table.column "age" (.age >> toFloat >> Value.Number)
|> Table.withHeader "Age"
|> Table.withMinSize 60
|> Table.withMaxSize 120
]
|> Table.withDefaultColumn { size = 180, minSize = 40, maxSize = 600 }
Column options
| Builder | Type | Default | Description |
|---|---|---|---|
withSize |
Float -> Column row -> Column row |
defaultColumn.size |
This column's starting width. |
withMinSize |
Float -> Column row -> Column row |
defaultColumn.minSize |
The narrowest this column reports. |
withMaxSize |
Float -> Column row -> Column row |
defaultColumn.maxSize |
The widest this column reports. |
Read them back, with the config defaults already applied, using
columnSize,
columnMinSize, and
columnMaxSize. Those three are the
column definition's own numbers and ignore State.columnSizing;
getColumnSize is the state-aware
one and is what you render with.
A column whose minSize is larger than its maxSize reports maxSize. That
looks wrong and is deliberate: it is what TanStack's clamp does, and the
ported test asserts it.
Transitions
| Function | Signature | Description |
|---|---|---|
setColumnSize |
String -> Float -> State -> State |
Commit one column's width. |
setColumnSizing |
Dict String Float -> State -> State |
Replace the whole map. |
resetColumnSize |
String -> State -> State |
Drop one column's committed width, leaving the others alone. |
resetColumnSizing |
State -> State |
Drop every committed width. |
update : Msg -> Model -> Model
update msg model =
case msg of
SetSize columnId width ->
{ model | state = Table.setColumnSize columnId width model.state }
ResetSize columnId ->
{ model | state = Table.resetColumnSize columnId model.state }
ResetAllSizes ->
{ model | state = Table.resetColumnSizing model.state }
To start from widths you have persisted, write the map once:
startingWidths : Table.State
startingWidths =
Table.setColumnSizing
(Dict.fromList [ ( "firstName", 260 ), ( "age", 80 ) ])
Table.initialState
Queries
| Function | Signature | Description |
|---|---|---|
getColumnSize |
Config row -> State -> Column row -> Float |
The rendered width: the committed size when there is one, else the column's own size, else the default, clamped. |
getColumnStart |
Config row -> State -> ColumnRegion -> Column row -> Float |
How far from the start of its region the column begins. |
getColumnAfter |
Config row -> State -> ColumnRegion -> Column row -> Float |
How far from the end of its region the column ends. |
getHeaderSize |
Config row -> State -> Header row -> Float |
A header's width: its column's size for a leaf header, the sum of its sub-headers for a parent header. |
getHeaderStart |
Config row -> State -> List (Header row) -> Header row -> Float |
How far from the start of its header row the header begins. |
totalSize |
Config row -> State -> Float |
The width of the whole table: the sum of the top header row. |
leftTotalSize |
Config row -> State -> Float |
The width of the left-pinned region. |
centerTotalSize |
Config row -> State -> Float |
The width of the unpinned region. |
rightTotalSize |
Config row -> State -> Float |
The width of the right-pinned region. |
getColumnStart and getColumnAfter take a
ColumnRegion, which replaces TanStack's
optional position argument.
getHeaderStart takes the headers of the row the header belongs to, because a
Header does not point back at its header group. Pass
group.headers for the group you are rendering.
Applying the widths
The numbers become inline style "width" attributes. Nothing in the package
writes CSS, so the unit is yours to choose:
px : Float -> String
px n =
String.fromFloat n ++ "px"
viewHeaderCell : Table.State -> Table.Header Person -> Html Msg
viewHeaderCell state header =
th
[ style "width" (px (Table.getHeaderSize config state header)) ]
[ text (Table.headerColumnId header) ]
Give the table itself the total, and table-layout: fixed so the browser
honours the per-cell widths instead of computing its own:
viewTable : Table.State -> List (Html Msg) -> Html Msg
viewTable state children =
table
[ style "width" (px (Table.totalSize config state))
, style "table-layout" "fixed"
]
children
The offsets are what a pinned column needs for its
sticky position. A left-pinned column sits at getColumnStart from the left
edge of the left region, and a right-pinned column at getColumnAfter from
the right edge of the right region:
stickyLeft : Table.State -> Table.Column Person -> List (Html.Attribute Msg)
stickyLeft state column =
[ style "position" "sticky"
, style "left" (px (Table.getColumnStart config state Table.leftColumnsRegion column))
, style "width" (px (Table.getColumnSize config state column))
]
stickyRight : Table.State -> Table.Column Person -> List (Html.Attribute Msg)
stickyRight state column =
[ style "position" "sticky"
, style "right" (px (Table.getColumnAfter config state Table.rightColumnsRegion column))
, style "width" (px (Table.getColumnSize config state column))
]
Not covered
TanStack's page also describes owning the columnSizing state through the
atoms option or state.columnSizing plus onColumnSizingChange. There is
nothing to port: State.columnSizing is always yours, held in your own
model.
resetColumnSizing here is TanStack's resetColumnSizing(true): it empties
the map. There is no table.initialState to restore, so to go back to widths
you started with, call setColumnSizing with the map you remembered.
Example
Column Sizing, ported from TanStack's Column Sizing example.