Each :col slot of a grid is a column. Its attributes make a Masu.Column.

<:col field={:total} type={:decimal} label="Total" width={140} />

The value

field names the value of a row: a key of a map (an atom or a string) or a field of a struct.

value computes it instead, from the whole row. A column with a value needs a key, its name in Masu.Test and in filters:

<:col key="initials" value={&initials/1} label="Initials" />

A list sorts and filters such a column like any other. A database cannot: with an Ecto source the column is shown, but does not sort, filter or search (see Sources).

field may be a path to a field of a related row: a list of the row's associations (or keys of maps in it), and the field at the end.

<:col field={[:customer, :name]} label="Customer" />
<:col field={[:customer, :company, :country]} key="country" type={:enum} />
<:col field={[:tags, :name]} label="Tags" type={:enum} />

Its key is the path with dots ("customer.name") unless given, the name of the column in Masu.Test, filters and links. A row without the related row (or with it not loaded) has no value there.

A path of one related row each step is one value, which sorts, filters, searches and groups as any other: an Ecto source joins it (see Fields of related rows), and a list follows it in each row. A path through several related rows (has_many, many_to_many) is of many values: its cell shows them apart ("gift, urgent"), and a cell of your own HEEx draws them as you like (chips for tags). It filters by "has any of", "has all of", "has none of", "has none" and "has some", and does not sort or group (see Filters).

The source says whether a path is of many values: an Ecto source from the schema's associations, a list from its rows. A source of your own says it with many?/2, or the column with many={true}.

The columns of related rows are read-only.

The label

The header shows label, or the field in words (inserted_at is "Inserted at"). Give label for the words of your app, in its language.

Types

The type says how the browser formats a value in the grid's locale (the locale attribute, or the browser's), how a filter reads the value it is given, and where the cell aligns.

TypeShowsAligns
:text (the default)the value as textstart
:enumthe value as textstart
:integer1,234end
:number1,234.5, with up to ten decimal placesend
:decimal1,234.50, with the digits the value hasend
:date17 Oct 2026start
:datetime17 Oct 2026, 14:30, in the browser's time zonestart
:time14:30start
:booleana check mark for truecenter

A decimal goes to the browser as a string, so no digit is lost on the way, and keeps the digits after the point that it has: 120.50 shows two.

A DateTime shows in the browser's time zone. A NaiveDateTime shows as it is written, with no zone.

Numbers, dates and times use tabular figures, so the digits of a column line up.

raw shows a column's values as the source gives them, not formatted for the locale: 12345 and not 12,345, 2026-10-18T08:00:00Z and not the date and time in the person's language. The column keeps its type, for its alignment, its filters and its editor. A tool that shows data as it is, as a SQL client does, wants it.

Width and alignment

width is the first width of the column, in pixels; the person may resize it, down to min_width (48 unless given). The default width follows the type: 200 for text, 120 for integers and numbers, 140 for decimals and dates. A column is at least its min_width wide.

A column that takes the room

A grid wider than its columns leaves room after the last of them, while a title or a description is cut short. grow gives that room to a column, from its width up:

<.data_grid id="inbox" rows={@items}>
  <:col field={:title} min_width={280} grow />
  <:col field={:author} width={120} />
  <:col field={:updated} type={:datetime} width={170} />
</.data_grid>

The title is as wide as the grid leaves it, and follows the grid's width as the window changes; in a grid narrower than the columns, it is its width, and the grid scrolls sideways. Several columns that grow share the room, each its share: grow={2} takes twice what grow takes. A column pinned to the end stays at the grid's end.

A person who resizes a column that grows gives it their width: it is that wide, and grows no more, in their view too (see Layout and views). A column they resize beside it takes its room from the column that grows, and gives it back.

align is :start, :center or :end. Numbers align to the end, booleans to the center, everything else to the start.

Totals

aggregate={:sum} (or :avg, :min, :max, :count, :count_distinct) totals the column in a row at the bottom of the grid and in the row of each group: see Groups and totals.

Pinned and hidden

pinned={:start} or pinned={:end} keeps a column in view while the others scroll sideways; hidden starts it hidden. A person moves, hides, pins and resizes columns as they work: see Layout and views.

Sorting, filtering and searching

Every column sorts and filters unless it says sortable={false} or filterable={false}. Text and enum columns are searchable, and others are not, unless they say searchable.

Cells of your own

A :col with content renders each of its cells, with the row as :let:

<:col :let={user} field={:name}>
  <.link navigate={~p"/users/#{user}"}>{user.name}</.link>
</:col>

The server renders the cells of the rows that the browser shows, as it asks for them, and sends their HTML. The column's field and type still say how it sorts and filters.

The content is a cell of a row of a fixed height: a link, a badge, an icon, a line of text. A cell clips what does not fit.

A copy of a cell of your own gives its text, without the HTML.

Columns not known in advance

A query tool knows its columns when its query runs: a result is a list of column names, and rows as lists of values in their order, of no types. A grid of it has no :col slots; columns names them:

<.data_grid id="result" rows={@result.rows} columns={@result.columns} row_id={:position} />

Each column is a key (a string), or a keyword list or map of a :col's attributes (key, label, type, width, field...):

<.data_grid id="result" rows={@rows} columns={["name", [key: "total", type: :decimal, label: "Total"]]} />

A column without a field reads the value at its place of a row that is a list: the first column the first value. A column without a type is :auto: the grid learns its type from the values of the first block of rows (integers, decimals, dates, booleans...; values of several types, or none, are text), so that numbers align to the end, dates show in the locale, and each column has the filters of its type. A :col takes type={:auto} and field={2} too.

A source of your own gives its columns with the optional Masu.Source.columns/1, for a grid with neither :col slots nor columns (see Sources).

When the rows change for a result of other columns, the grid takes the new columns, and learns their types again. The person's view keeps what is of the columns there still (a sort, a filter, a width, a hidden column) and names the rest, as a link to columns that are gone does; their order is the new result's, unless the person moved columns. Rows as lists have no ids: row_id={:position} says so, for a grid that reads (see Sources). A column that reads a place of a list does not edit.