Overview

elm-table is a headless table library for Elm 0.19.1: an Elm port of TanStack Table core. It gives you the state, the row processing, and the query functions for a table or datagrid, and it gives you no markup at all. You write the Html.

The package renders nothing, holds no state of its own, uses no ports, and calls no JavaScript. Every function has the shape Config row -> State -> ..., so two tables on one page never share anything by accident, and every state change is a plain value you can inspect, log, store, or test.

What is "headless" UI?

Headless UI is the name for a library that supplies the logic, the state, and the data processing behind a piece of UI but no markup and no styles. The hard parts of a table are sorting, filtering, grouping, pagination, and the bookkeeping that ties them together. A headless library takes those and leaves the rendering to you, so the look of the table is entirely yours.

With a prebuilt Elm table component you pass a record of options in and hope its styling hooks reach the thing you want to change. With elm-table you hold a State, run your data through the pipeline, and write the Html.table yourself:

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))
        ]

The shape of the API

TanStack Table builds a table instance: an object that owns the options, the state, the cached row models, and a few hundred methods. Elm has no good place to put a mutable object like that, so this port has none. Three values stand in for it.

TanStack elm-table What it is
TableOptions Config row The columns and every option, built once at the top level.
TableState State Every state slice, owned by your model.
table.getRowModel() Table.rows config state data The processed rows, recomputed when you ask for them.

Everything else is a function that takes some of those three. There is no table. prefix and nothing to construct.

The pipeline

Rows go through six stages, always in this order:

core → filtered → grouped → sorted → expanded → paginated

Table.rows runs all six. Each stage is also exposed on its own (Table.filteredRowModel, Table.sortedRowModel, and so on) so you can stop early, look at an intermediate RowModel, or splice in a stage of your own. See Row Models.

Features

Every feature of TanStack Table core is ported. Follow a link for the guide.

Three TanStack pages have no counterpart here and are listed on Features with the reason: devtools, flexRender, and the custom-feature plugin system.

Composition

Because you own the markup, the messages, and the state, the package composes with the rest of your app rather than boxing it in.

  • Your own state. State is a plain record in your model. Put a slice of it in the URL, seed it from a flag, diff it, or write it back from a decoder. Nothing is hidden.
  • Your own stages. Set manualSorting, manualFiltering, manualGrouping, manualExpanding, or manualPagination and that stage returns its input unchanged, so the server can do the work instead. See Client-Side vs Server-Side.
  • Your own rendering. Pair it with dominikmayer/elm-virtual-list for long lists, or with plain Html.Lazy. See Virtualization.

Get started