Skip to main content
View as Markdown

Tables & Lists

Data components render records from your tables. Each binds through the shared dataSource module and renders the matching records. Filtering, sorting and pagination belong to dataSource, not to the component — a table has no top-level filter or sort of its own.

app.yaml
- type: data-table
  dataSource: { table: tasks, sort: [{ field: due_date, direction: asc }] }
  columns:
    - { field: title, sortable: true }
    - { field: due_date, format: relative-date }

data-table

Property Description
dataSource Table binding. Carries filter, sort and pagination.
columns Column definitions. Auto-generated from the table's fields when omitted.
selection Row selection.
bulkActions Actions offered once rows are selected.
groupBy Row grouping. Headers count the whole result set, not the loaded page.
summary Aggregates for the grid — and for each group once the grid is grouped.
toolbar Which toolbar controls to show.
rowHeight short, medium (default), or tall.
striped / bordered Alternating row backgrounds; cell borders.
rowColorField Field whose declared option colours fill each row — see below.
showRowNumbers Prepend a row-number column.
emptyMessage / noMatchMessage Shown when the source returns nothing; when a search or filter excludes everything.
onRowClick Action invoked when a row is clicked. Accepts two shapes only — see below.
rowExpand Expand a row into its full record. true, false, or an object — see below.
autoSave Inline-edit behavior. See Auto-Save.
search In-table search configuration.

onRowClick

A row click accepts exactly two shapes:

Shape Effect
{ type: navigate, path } Navigates to path. $record.<field> tokens are replaced with the clicked row's values.
{ action: openDrawer, component } Opens the sibling drawer component with that id. Optional props.width overrides its width for this trigger.
app.yaml
onRowClick:
  type: navigate
  path: /deals/$record.id

Note that openDrawer is keyed on action, not type.

Every other action type — auth, crud, automation, filter, toast, fetch — is rejected here, because a row click has never run them. Richer behavior belongs on the opened drawer's own actions, where the full action set is available: open the record in a drawer, then act on it from there.

rowExpand

Opening a row's full record is a property of the grid, so it needs no sibling component and no id to keep in step:

app.yaml
- type: data-table
  dataSource: { table: deals }
  rowExpand: true
Value Effect
true Rows expand into a panel whose fields are derived from the bound table.
false No expand — the same as omitting the key, so an expand can be switched off in place.
{ fields?, canEdit?, title? } Narrows the field list, makes the panel read-only, and names it.

With no fields, the panel shows every declared field of the bound table, in declared order — not the visible columns. The reason to expand a row is to see what the row cannot show, so deriving from the columns would make expand a no-op on a grid that already displays everything. Each field keeps the label and description it declares. An explicit fields list narrows and reorders the panel, and may name a field that is not a column.

app.yaml
- type: data-table
  dataSource: { table: deals }
  columns: [{ field: name }, { field: stage }]
  rowExpand:
    fields: [name, stage, notes]
    canEdit: false
    title: Deal detail

rowExpand and onRowClick cannot both be declared. A row has one click, and two answers to what it does is a config whose halves cannot both apply — so it is refused rather than one silently winning. rowExpand: false alongside onRowClick is fine, since false is off.

rowExpand is also refused on a grid bound to a read endpoint (dataSource.system or systemSource): the panel derives its controls from the bound table's field schema, and a read endpoint has none. Use a record-drawer with dataSource.system and its own recordFields, opened with onRowClick: { action: openDrawer, component }.

The hand-wired form — onRowClick: { action: openDrawer } plus a sibling drawer — is unchanged and is not deprecated. rowExpand is a shorthand for the common case, not a replacement: the drawer can still bind a different table from the grid's, bind a read endpoint, be shared by two grids, and carry footer actions.

What a click means

A row can carry several meanings at once. The target decides, never the timing:

Click lands on Effect
An editable cell Double-click opens the cell editor; the record does not expand.
The selection checkbox Selects the row only.
A group header Collapses or expands that group, at any nesting level.
Any other cell Expands the record.

Rows stay reachable from the keyboard: a row carrying an action is focusable and answers Enter, and no per-row button is added.

columns

A column is either a field column or an action column (type: actions, carrying actions, each { label, action, icon, confirm, visibleWhen, editSelect }).

Field column property Description
field Field name from the bound table.
label Header override.
width / minWidth Pixel widths.
align left (default), center, right.
format Cell rendering: truncate, currency, percentage, compact, relative-date, relative-time, short-date, long-date, datetime, yes-no, check-cross.
valueLabels Raw value to display label map, e.g. { todo: 'To do' }.
frozen Pin the column so it stays visible while scrolling horizontally.
visible Set false to hide the column.
sortable / filterable / editable Per-column capability toggles.
cellStyle { when: { <operator>: value }, className } rules applied per cell. Operators: eq, neq, in, notIn, contains, gt, lt, gte, lte.
app.yaml
columns:
  - field: status
    frozen: true
    cellStyle: [{ when: { eq: overdue }, className: 'text-red-600' }]
  - type: actions
    actions: [{ label: Delete, action: { type: crud, operation: delete, table: tasks } }]

Selection, grouping & summaries

selection takes a required modenone, single, or multiple — plus showCheckboxes. Each bulkActions entry is { label, icon, action, confirm }, where confirm is the dialog wording and may contain {count}, replaced with the number of selected rows.

groupBy accepts field, direction (asc default), collapsed, and thenBy — up to two further levels nested inside the first, three in total. Each entry in thenBy takes the same three keys. summary accepts field, label, and function — one of count, sum, avg, min, max.

direction orders the group headers themselves, not the rows inside a group — those follow dataSource.sort and the column headers. Grouping by a single-select or status field orders the headers by the field's declared option order rather than alphabetically, so a stage of prospect/qualified/won reads in the order it was authored. Each header also counts the whole result set: a group holding 30 records reads (30) even on a page showing 22 of them — and that holds at every level, so a sub-group spilling past a page boundary still reports its full size.

Every level answers for itself. direction applies per level and independently: an outer level can open on a declared pipeline order while the level nested inside it reads desc, and neither touches the other. collapsed is per level too, and folding nests — folding a parent hides everything beneath it, sub-group headers included. A field named by any level need not be a visible column; the header carries its value, which is often the reason to group by it in the first place.

Naming the same field at two levels is refused at validation: every record in a group already shares the value that group was formed on, so a repeated level puts exactly one sub-group inside each group and partitions nothing.

A declared summary describes the grid as a whole, and once the grid is grouped it additionally describes each group — at every level, not only the innermost. There is no second option to turn this on: the per-group totals follow the summary already declared, so a grid that pairs groupBy with summary gains a total per group without changing a line of config. Group totals stay visible when a group is collapsed, which is what makes collapsing everything a useful way to compare groups.

Because every level is summarised, the figures reconcile: a sum over the sub-groups of one parent equals that parent's, and the parents' equal the footer's. That is the point of nesting — comparing a parent against its children only works when both are answered.

Like the footer, a group total describes the whole group rather than the part of it on the current page, and it is rendered in its column's format. The footer keeps answering for the entire grid alongside them, so one grouped view shows the per-group figures at every depth and the overall one together.

app.yaml
- type: data-table
  dataSource: { table: deals }
  pagination: { pageSize: 25 }
  groupBy: { field: stage, direction: asc }
  summary:
    - { field: name, function: count, label: Deals }
    - { field: value, function: sum, label: Pipeline value }

Each stage reports its own deal count and pipeline value, and the footer reports both across every stage. Group totals sit under the column each describes; a summary over a field that is not a visible column keeps its value and renders beside the group name rather than being dropped.

Nesting adds levels without adding options — the same summary now answers at each of them:

app.yaml
- type: data-table
  dataSource: { table: deals }
  pagination: { pageSize: 25 }
  groupBy:
    field: region
    direction: asc
    thenBy:
      - { field: stage, direction: desc }
      - { field: owner, collapsed: true }
  summary:
    - { field: name, function: count, label: Deals }
    - { field: value, function: sum, label: Pipeline value }

Regions read in their declared order, the stages inside each region read in reverse, and the owners inside each stage start folded. owner is not among the columns, and does not need to be.

toolbar is a record of booleans selecting which controls appear: search, filters, sort, export, refresh, density, columnToggle, groupBy, views, viewSwitcher.

rowColorField

Names a field whose declared option colours fill each row — the grid's spelling of the colorField that kanban, calendar and timeline read. It is spelled rowColorField rather than colorField because a grid already decides colour per column, where a bare colorField would read as "colour the cells".

app.yaml
- type: data-table
  dataSource: { table: orders }
  rowColorField: order_status

The grammar is the same as on the record views: the fill comes from the option colours declared on the named field, and the row's text colour is derived from that fill so it stays legible against any hue.

Unlike a calendar or timeline, a grid invents nothing. A value whose option declares no colour is not filled — there is no fallback palette. The same goes for a row whose value is empty, and for a grid with no rowColorField at all: all three are simply unfilled.

Unlike the record views' colorField, this one is checked. sovrium validate rejects a rowColorField naming a field that does not exist on the bound table, because an unfilled grid looks identical whether the name was a typo or the colours were deliberately left undeclared.

list

A vertical list of records. Everything about presentation lives under listDisplay.

listDisplay property Description
itemTemplate Per-record template: title, subtitle, image, badge, metadata.
loadMore button (a Load More control) or infinite (load on scroll).
highlight Highlight matched search terms.
divider Draw dividers between items.
maxItems Cap on items displayed.
emptyMessage Shown when nothing matches.

metadata is an array of { field, format } rendered in the item footer.

app.yaml
- type: list
  dataSource: { table: articles, sort: [{ field: published_at, direction: desc }] }
  listDisplay:
    itemTemplate:
      title: '$record.title'
      metadata: [{ field: published_at, format: relative-date }]
    loadMore: button

data-form / form

A record-backed form. Both type names share one schema. action decides what submitting does; fields overrides how individual fields render.

Property Description
dataSource Table binding — table-only by design, since writes go to a table, never to a read endpoint. mode: single supplies current values for an edit form.
fields Per-field config — field plus label, placeholder, control, options, defaultValue, readOnly, disabled, hidden, visibleWhen, requiredWhen, disabledWhen.
fieldGroups { label, fields } sections dividing the form.
layout single-column (default), two-column, or custom.
action What submitting runs. See Interactions.
app.yaml
- type: data-form
  dataSource: { table: contacts, mode: single, param: id }
  layout: two-column
  action:
    type: crud
    operation: update
    table: contacts
  fields:
    - { field: email, label: 'Email address' }
    - { field: notes, control: textarea }

Moved to Boards, Calendars & Timelines.

kanban

Moved to Boards, Calendars & Timelines.

calendar

Moved to Boards, Calendars & Timelines.

data-timeline

Moved to Boards, Calendars & Timelines.

chart

Moved to Charts & KPIs.

kpi

Moved to Charts & KPIs.

Last updated August 11, 2026

This documentation was written with AI, so errors or outdated content are possible. Sovrium is in beta. Contributions and corrections are welcome.

Built with Sovrium