Headers

A header is one <th>. Headers are to the <thead> what cells are to the <tbody>. This page is the port of TanStack's Headers guide.

Header row is opaque, like Row and Column: you read it through functions rather than fields.

Where to get headers from

Header group headers

Headers arrive in header groups, one group per header row. headerGroup.headers is the list, left to right:

viewHeaderRow : Table.HeaderGroup Person -> Html msg
viewHeaderRow headerGroup =
    tr [] (List.filterMap viewHeaderCell headerGroup.headers)

Header list functions

When you want a flat list rather than the rows, these return one:

TanStack elm-table
table.getFlatHeaders() flatHeaders
table.getLeafHeaders() leafHeaders
header.getLeafHeaders() getLeafHeaders
table.getStartFlatHeaders() leftFlatHeaders
table.getCenterFlatHeaders() centerFlatHeaders
table.getEndFlatHeaders() rightFlatHeaders
table.getStartLeafHeaders() leftLeafHeaders
table.getCenterLeafHeaders() centerLeafHeaders
table.getEndLeafHeaders() rightLeafHeaders

flatHeaders is every header of every header row, group headers and placeholders included. leafHeaders is only the headers with no sub-headers, which is one per rendered column.

flatHeaderIds : Table.State -> List String
flatHeaderIds state =
    List.map Table.headerId (Table.flatHeaders config state)
leafHeaderIds : Table.State -> List String
leafHeaderIds state =
    List.map Table.headerColumnId (Table.leafHeaders config state)

getLeafHeaders is the per-header version: the columns one header actually covers, useful when a click on a group header should act on everything below it.

coveredColumns : Table.Header Person -> List String
coveredColumns header =
    List.map Table.headerColumnId (Table.getLeafHeaders header)

Header objects

Every reader takes the header and nothing else.

TanStack elm-table
header.id headerId
header.column.id headerColumnId
header.colSpan headerColSpan
header.rowSpan headerRowSpan
header.depth headerDepth
header.index headerIndex
header.isPlaceholder headerIsPlaceholder
header.placeholderId headerPlaceholderId
header.subHeaders headerSubHeaders
header.getSize() getHeaderSize
header.getStart() getHeaderStart
header.column none, use headerColumnId then findColumn
header.headerGroup none
header.getContext() none

Header ids

headerId is unique within the header rows. For a plain header it is the column id. A group header, a placeholder, or a header in a pinned region gets a compound id built from the region, the depth, the column id, and the id of its first child, joined with underscores. Use it as the key of a keyed node, not as something to parse.

There is no header.column

A header carries the column's id, not the column. To get the column, look it up:

headerColumn : Table.Header Person -> Maybe (Table.Column Person)
headerColumn header =
    Table.findColumn config (Table.headerColumnId header)

That is the one extra step compared with TanStack. It comes up whenever you want the header text, since the text lives on the column:

headerLabel : Table.Header Person -> String
headerLabel header =
    headerColumn header
        |> Maybe.andThen Table.columnHeader
        |> Maybe.withDefault (Table.headerColumnId header)

Table.columnHeader returns a Maybe String, because withHeader is optional. Falling back to the column id is the usual choice.

There is no header.headerGroup either. You have the header group in scope when you map over its headers, so pass down what you need from it.

Nested and grouped header properties

These matter once columns are nested with Table.group:

  • headerColSpan: how many columns the header spans. Goes straight into the colspan attribute.
  • headerRowSpan: how many header rows it spans. A leaf column shallower than the deepest leaf column produces a chain of placeholder headers above its real header. The placeholder at the top of the chain reports the whole chain's span, and every header it covers, including the real leaf header at the bottom, reports 0.
  • headerDepth: which header row the header is in, counting from 0 at the top.
  • headerIndex: the header's position from left to right within its header row. Not the same as headerDepth.
  • headerIsPlaceholder: true for a filler header standing in for a column that has no group at this level.
  • headerPlaceholderId: how many placeholders for the same column came before this one, as a string. Nothing on a real header.
  • headerSubHeaders: the headers nested directly under this one, empty on a leaf header.
childIds : Table.Header Person -> List String
childIds header =
    List.map Table.headerId (Table.headerSubHeaders header)
placeholderReport : Table.Header Person -> String
placeholderReport header =
    if Table.headerIsPlaceholder header then
        Table.headerColumnId header
            ++ " placeholder "
            ++ Maybe.withDefault "0" (Table.headerPlaceholderId header)
            ++ " at depth "
            ++ String.fromInt (Table.headerDepth header)
            ++ ", position "
            ++ String.fromInt (Table.headerIndex header)

    else
        Table.headerId header

Header row spanning

With an uneven column tree, merge each placeholder chain into one cell: skip every header whose headerRowSpan is 0, and render the rest with both attributes. This replaces the usual headerIsPlaceholder check, because the spanning placeholder is the cell that draws the column's header text.

viewHeaderCell : Table.Header Person -> Maybe (Html msg)
viewHeaderCell header =
    if Table.headerRowSpan header == 0 then
        Nothing

    else
        Just
            (th
                [ colspan (Table.headerColSpan header)
                , rowspan (Table.headerRowSpan header)
                ]
                [ text (headerLabel header) ]
            )

This recipe is for <thead> only. footerGroups returns the header rows in reverse, which puts a spanning placeholder below the cells it would need to cover, so keep the empty-cell placeholder pattern in <tfoot>. See Header Groups.

The body-cell equivalent is Cell Spanning.

Header rendering

TanStack renders a header with flexRender(header.column.columnDef.header, header.getContext()) because a header column option can be a string, JSX, or a function. Here withHeader takes a String and you write the markup:

viewHeaderCell : Table.State -> Table.Header Person -> Html msg
viewHeaderCell state header =
    th
        [ colspan (Table.headerColSpan header)
        , style "width" (String.fromFloat (Table.getHeaderSize config state header) ++ "px")
        ]
        [ text (headerLabel header) ]

For a sortable header, put the click handler and the direction arrow in that same function. See Sorting.

Sizing

getHeaderSize is a leaf header's column width, or the sum of the sub-header widths for a parent header. getHeaderStart is how far from the start of its header row the header begins, which is what a sticky pinned header needs. It takes the headers of the row the header belongs to, because there is no header group reference to walk back to. Pass headerGroup.headers, not flatHeaders:

viewStickyHeaderRow : Table.State -> Table.HeaderGroup Person -> Html msg
viewStickyHeaderRow state headerGroup =
    tr []
        (List.map
            (\header ->
                th
                    [ style "position" "sticky"
                    , style "left" (offsetPx state headerGroup.headers header)
                    ]
                    [ text (headerLabel header) ]
            )
            headerGroup.headers
        )
offsetPx : Table.State -> List (Table.Header Person) -> Table.Header Person -> String
offsetPx state headerRow header =
    String.fromFloat (Table.getHeaderStart config state headerRow header) ++ "px"

See Column Sizing and Column Resizing.