Quick Start

elm-table manages a table's state and row processing — sorting, filtering, grouping, pagination, selection, and the rest — while you keep full control of the markup. This page gets you from elm install to a rendering table, then adds the first feature.

Installation

elm install viewengine/elm-table

Your first table

Six steps. Every block below is one declaration from a module that compiles as written; the whole module is at the bottom of the page.

1. Describe your data. Any record type works. The package never inspects it directly, only through the accessors you give it.

type alias Person =
    { id : String
    , firstName : String
    , lastName : String
    , age : Int
    }

2. Define your columns. Table.column takes an id and an accessor turning a row into a Value. Table.withHeader sets the header text.

columns : List (Table.Column Person)
columns =
    [ Table.column "firstName" (.firstName >> Value.String)
        |> Table.withHeader "First name"
    , Table.column "lastName" (.lastName >> Value.String)
        |> Table.withHeader "Last name"
    , Table.column "age" (.age >> toFloat >> Value.Number)
        |> Table.withHeader "Age"
    ]

3. Build the Config. This is TanStack's TableOptions. Build it once, at the top level of your module, not inside view — it holds no state, so there is nothing to recompute.

config : Table.Config Person
config =
    Table.config columns
        |> Table.withGetRowId (\person _ _ -> person.id)

withGetRowId gives each row a stable id. Without it a row's id is its index in the data, which is fine until the data reorders.

4. Put the State in your model. Table.initialState is every slice at its default: no sorting, no filters, page 0, everything visible.

init : Model
init =
    { state = Table.initialState }

5. Run the pipeline. Table.rowsFromList takes the config, the state, and your data, and returns a RowModel — the processed rows plus a lookup by id. Nothing is memoized, so a real app computes this once in update and stores it; this page recomputes it so each function stays a one-liner. See Table State for the storing pattern.

rowModel : Table.State -> Table.RowModel Person
rowModel state =
    Table.rowsFromList config state people

6. Render. Table.visibleLeafColumns gives the columns to draw, Table.rowsInDisplayOrder gives the rows for the current page, and Table.getValue reads one cell.

view : Model -> Html Msg
view model =
    let
        current =
            rowModel model.state
    in
    table []
        [ thead []
            [ tr [] (List.map (viewHeader model.state) (Table.visibleLeafColumns config model.state)) ]
        , tbody []
            (List.map (viewRow model.state) (Table.rowsInDisplayOrder config model.state current))
        ]
viewRow : Table.State -> Table.Row Person -> Html Msg
viewRow state row =
    tr [] (List.map (viewCell row) (Table.visibleLeafColumns config state))
viewCell : Table.Row Person -> Table.Column Person -> Html Msg
viewCell row column =
    td [] [ text (Value.toString (Table.getValue config row (Table.columnId column))) ]

A few things to note:

  • There is no table instance and nothing to construct. config, state, and your data are the three arguments every function wants.
  • Table.rowsInDisplayOrder is the list to render. It is rowModel.rows plus the expanded descendants that pagination left out when Config.paginateExpandedRows is False. Row pinning is separate, and lives in topRows, centerRows, and bottomRows.
  • Table.getValue returns a Value, not a String. Value.toString gives you TanStack's String(value) coercion; pattern match on the Value when you want your own formatting.
  • Nothing here is opt-in. Sorting works because the sorting stage is always in the pipeline, not because you registered a feature.

Add a feature: sorting

Sorting needs one message and one state transition. Table.toggleSort cycles a column through ascending, descending, and unsorted, exactly as clicking a TanStack header does.

update : Msg -> Model -> Model
update msg model =
    case msg of
        SortBy columnId ->
            { model
                | state =
                    Table.toggleSort config
                        (rowModel model.state)
                        columnId
                        { desc = Nothing, multi = False }
                        model.state
            }

The { desc, multi } record is TanStack's toggleSorting(desc, isMulti). Pass desc = Just True to force a direction; pass multi = True (from a shift-click) to add the column to the sort instead of replacing it.

toggleSort takes a RowModel because the first direction a column sorts in can depend on the data: an unsorted column of dates starts descending, a column of strings starts ascending, the same rule TanStack uses.

The header then reads the direction back out with Table.getIsSorted, which returns Maybe SortDir:

viewHeader : Table.State -> Table.Column Person -> Html Msg
viewHeader state column =
    let
        id =
            Table.columnId column

        arrow =
            case Table.getIsSorted state id of
                Nothing ->
                    ""

                Just dir ->
                    if dir == Table.sortAsc then
                        " ↑"

                    else
                        " ↓"
    in
    th [ onClick (SortBy id), style "cursor" "pointer" ]
        [ text (Maybe.withDefault id (Table.columnHeader column) ++ arrow) ]

Every other feature follows this pattern: a transition function that returns a new State, and query functions that read the state back for rendering. See the Sorting guide for custom sort functions, multi-sorting, and per-column options.

Wiring it up

Nothing about the package needs a Browser.element or a subscription, so the whole thing runs as a sandbox:

main : Program () Model Msg
main =
    Browser.sandbox { init = init, update = update, view = view }

The complete file every block above came from is docs-site/snippets/src/QuickStart.elm in the repository, and a runnable version of the same table is the Basic example.

Where to go next

Config and State. Config and State is the page to read next: what goes in each, and why there is no third thing.

Table State. Table State covers owning the State in your model, when to store the RowModel alongside it, and how to reset a slice.

Feature guides. Each feature has its own page: Column Filtering, Pagination, Row Selection, Grouping, Column Visibility, and the rest.

Examples. Every TanStack example ported to Elm, runnable, is at elm-table-examples.pages.dev, listed on the Examples page.