Column Pinning
Column Pinning
Pinning holds a column against the left or right edge of the table while the rest scroll. The state is a pair of column id lists, and the package hands you the three column slices, the three header slices, and the three cell slices that a pinned layout needs. This page ports TanStack Table's Column Pinning (React) Guide.
There are two ways to render it, and the package supports both. Keep every
column in one table and use position: sticky CSS on the pinned cells, or
render the left, center, and right regions as three separate tables side by
side. The split layout is what the region functions below exist for.
How pinning affects column order
Column order is decided in one place, and the pin split is its last step:
allColumns -> State.columnOrder -> Config.groupedColumnMode -> visibility -> pin split
The order inside the left and right regions comes from State.columnPinning
itself, in the order those lists hold. State.columnOrder therefore only
decides the order of the unpinned ("center") columns. See Column
Ordering.
State
Column pinning owns one state slice: the ids pinned to each edge.
-- in Table.State
columnPinning : ColumnPinning
-- the type
type alias ColumnPinning =
{ left : List String
, right : List String
}
-- in Table.initialState
columnPinning = { left = [], right = [] }
TanStack calls these fields start and end, which are logical directions:
in a left-to-right layout start is the left edge, and in a right-to-left
layout it is the right edge. This port names them left and right, because
that is what a caller writing CSS is thinking about. The start / end
wording survives in one place only: the header ids of the per-region header
groups, which read start_1_identity_firstName, because the ported test
asserts those exact strings.
Ids in left and right are leaf column ids.
pinColumn removes a column from both
lists before adding it to one, so it never lands in both. If you write the
record yourself and put an id in both, the left list wins.
Config options
| Option | Type | Default | Description |
|---|---|---|---|
enableColumnPinning |
Bool |
True |
Turns pinning off for the whole table. With it False, columnCanPin is False for every column. |
Config is a plain record, so you set it with a record update:
{ config | enableColumnPinning = False }.
Column options
| Builder | Type | Default | Description |
|---|---|---|---|
withEnablePinning |
Bool -> Column row -> Column row |
True |
Allow or forbid pinning this column. |
A group column can be pinned when at least one leaf below it allows it, and pinning a group column pins every leaf below it.
config : Table.Config Person
config =
Table.config
[ Table.column "firstName" (.firstName >> Value.String)
|> Table.withHeader "First name"
, Table.column "lastName" (.lastName >> Value.String)
|> Table.withHeader "Last name"
, Table.column "department" (.department >> Value.String)
|> Table.withHeader "Department"
, Table.column "actions" (\_ -> Value.Null)
|> Table.withHeader "Actions"
|> Table.withEnablePinning False
]
Transitions
| Function | Signature | Description |
|---|---|---|
pinColumn |
ColumnPinPosition -> Column row -> State -> State |
Pin one column to an edge, or unpin it. |
setColumnPinning |
ColumnPinning -> State -> State |
Replace both lists at once. |
resetColumnPinning |
State -> State |
Unpin every column. |
ColumnPinPosition is an abstract type with three values, standing in for
TanStack's 'start' | 'end' | false:
| Value | Meaning |
|---|---|
pinnedLeft |
Pinned to the left edge. TanStack's 'start'. |
pinnedRight |
Pinned to the right edge. TanStack's 'end'. |
columnUnpinned |
Not pinned. TanStack's false. |
Wire pinColumn to a message that carries the position and the column:
update : Msg -> Model -> Model
update msg model =
case msg of
Pin position column ->
{ model | state = Table.pinColumn position column model.state }
ClearPinning ->
{ model | state = Table.resetColumnPinning model.state }
To start with columns already pinned, write the state once:
pinnedByDefault : Table.State
pinnedByDefault =
Table.setColumnPinning
{ left = [ "firstName" ], right = [ "actions" ] }
Table.initialState
Queries
Regions
Most read functions come in four shapes: one per region, plus a general one
taking a ColumnRegion. ColumnRegion replaces TanStack's optional
position argument, which cannot be optional in Elm.
| Value | Meaning |
|---|---|
allColumnsRegion |
Every column. What TanStack means by leaving position out. |
leftColumnsRegion |
The left-pinned columns. |
centerColumnsRegion |
The unpinned columns. |
rightColumnsRegion |
The right-pinned columns. |
Per-column
| Function | Signature | Description |
|---|---|---|
columnCanPin |
Config row -> Column row -> Bool |
Can this column be pinned? |
columnIsPinned |
State -> Column row -> ColumnPinPosition |
Where is it pinned? A group column reports its first pinned leaf, left before right. |
columnPinnedIndex |
State -> Column row -> Int |
Its position inside its pinned region. An unpinned column gives 0, matching TanStack. |
A pin control renders from those two:
pinControls : Table.State -> Table.Column Person -> Html Msg
pinControls state column =
if not (Table.columnCanPin config column) then
text ""
else if Table.columnIsPinned state column == Table.columnUnpinned then
span []
[ button [ onClick (Pin Table.pinnedLeft column) ] [ text "Pin left" ]
, button [ onClick (Pin Table.pinnedRight column) ] [ text "Pin right" ]
]
else
button [ onClick (Pin Table.columnUnpinned column) ] [ text "Unpin" ]
Per-table
| Function | Signature | Description |
|---|---|---|
isSomeColumnsPinned |
State -> Bool |
Is anything pinned to either edge? |
isSomeColumnsPinnedLeft |
State -> Bool |
Anything pinned left? |
isSomeColumnsPinnedRight |
State -> Bool |
Anything pinned right? |
Column lists
| Function | Signature | Description |
|---|---|---|
leftLeafColumns |
Config row -> State -> List (Column row) |
Left-pinned leaf columns, hidden ones included. |
centerLeafColumns |
Config row -> State -> List (Column row) |
Unpinned leaf columns, in table order. |
rightLeafColumns |
Config row -> State -> List (Column row) |
Right-pinned leaf columns. |
pinnedLeafColumns |
Config row -> State -> ColumnRegion -> List (Column row) |
The same three by region. |
leftVisibleLeafColumns |
Config row -> State -> List (Column row) |
Left-pinned leaf columns that are visible. |
centerVisibleLeafColumns |
Config row -> State -> List (Column row) |
Visible unpinned leaf columns. |
rightVisibleLeafColumns |
Config row -> State -> List (Column row) |
Right-pinned leaf columns that are visible. |
pinnedVisibleLeafColumns |
Config row -> State -> ColumnRegion -> List (Column row) |
The same three by region. allColumnsRegion gives visibleLeafColumns unchanged. |
pinnedColumns |
Config row -> State -> PinnedColumns row |
All three visible slices at once, as { left, center, right }. |
PinnedColumns row is a plain record:
type alias PinnedColumns row =
{ left : List (Column row)
, center : List (Column row)
, right : List (Column row)
}
visibleLeafColumns is not
pin-ordered. It keeps table order and ignores pinning entirely, exactly as
TanStack's getVisibleLeafColumns does. The pin partition lives in two other
places: the header seam, which is what the per-region header group functions
below build on, and
visibleCells, which returns the
left cells, then the center cells, then the right cells. So a one-table sticky
layout that renders headers with the header groups and cells with
visibleCells is already in pin order without asking for it.
Header and footer lists
| Function | Signature |
|---|---|
leftHeaderGroups / centerHeaderGroups / rightHeaderGroups |
Config row -> State -> List (HeaderGroup row) |
leftFooterGroups / centerFooterGroups / rightFooterGroups |
Config row -> State -> List (HeaderGroup row) |
leftFlatHeaders / centerFlatHeaders / rightFlatHeaders |
Config row -> State -> List (Header row) |
leftLeafHeaders / centerLeafHeaders / rightLeafHeaders |
Config row -> State -> List (Header row) |
A flat header list is every header of every row of that region; a leaf header list is only the headers with no sub-headers. See Header Groups and Headers.
Cell lists
| Function | Signature | Description |
|---|---|---|
leftVisibleCells |
Config row -> State -> Row row -> List Cell |
The row's visible cells whose column is pinned left. |
centerVisibleCells |
Config row -> State -> Row row -> List Cell |
Its visible unpinned cells. |
rightVisibleCells |
Config row -> State -> Row row -> List Cell |
Its visible cells pinned right. |
Sticky layout
One table, one row, three groups of cells, each with its own class:
viewRow : Table.State -> Table.Row Person -> Html Msg
viewRow state row =
tr []
(List.map (viewCell "pinned-left") (Table.leftVisibleCells config state row)
++ List.map (viewCell "unpinned") (Table.centerVisibleCells config state row)
++ List.map (viewCell "pinned-right") (Table.rightVisibleCells config state row)
)
The left and right CSS offsets a sticky column needs are widths, so they
come from Column Sizing:
getColumnStart with
leftColumnsRegion, and
getColumnAfter with
rightColumnsRegion.
Split layout
Three tables, each built from one region's headers and one region's cells:
viewLeftTable : Table.State -> Table.RowModel Person -> Html Msg
viewLeftTable state model =
table []
[ thead []
(List.map viewHeaderRow (Table.leftHeaderGroups config state))
, tbody []
(List.map
(\row -> tr [] (List.map (viewCell "pinned-left") (Table.leftVisibleCells config state row)))
(Table.rowsInDisplayOrder config state model)
)
]
viewHeaderRow : Table.HeaderGroup Person -> Html Msg
viewHeaderRow group =
tr [] (List.map (\header -> th [] [ text (Table.headerColumnId header) ]) group.headers)
Not covered
TanStack's page also describes owning the columnPinning state through the
atoms option or state.columnPinning plus onColumnPinningChange. There is
nothing to port: State.columnPinning is always yours, held in your own
model.
resetColumnPinning here is TanStack's resetColumnPinning(true): it clears
both lists. There is no table.initialState to restore, so to go back to a
pinning you started with, call setColumnPinning with the record you
remembered.
TanStack marks each returned cell with a position field. This port does not:
the lists carry the same cells unchanged, and you know the region from the
function you called.
Example
Column Pinning, ported from TanStack's Column Pinning example.
Two more layouts of the same feature:
Column Pinning (Split)
renders the three regions as three separate tables, and
Sticky Column Pinning
keeps them in one table with position: sticky.