tai-ui TradeAIAlgo UI library
Theme LightDarkSystem

Component

Data table

Dense numbers people compare across rows and columns — an option chain, a statement. A real <table>, with the parts tables don't do on their own: a reachable scroll area, sticky key columns, sorting that announces itself, and row markers that don't rely on colour.

  • 1.3.1 Info and relationships
  • 1.4.1 Use of colour
  • 2.1.1 Keyboard
  • 4.1.2 Name, role, value
  • 4.1.3 Status messages
  • 1.4.10 Reflow

Examples

Option chain — grouped headers, sticky strike, at-the-money row (illustrative data)
HTML

<tai-data-table id="chain" class="stretch" caption="Sample index option chain" description="Illustrative numbers, not market data. Spot 24,180." density="compact" style="max-height: 26rem"></tai-data-table>
Statement — sortable columns (banking)
HTML

<tai-data-table id="statement" class="stretch" caption="Current account ••••1093 — September" empty-text="No transactions this month"></tai-data-table>
<div class="log" role="log" aria-label="Sort events" id="log-sort"></div>

Usage

Give it columns and rows as properties. Formatting, classes and screen-reader-only suffixes are functions of the row.

JS

const table = document.querySelector('#statement')
table.columns = [
  { key: 'date', label: 'Date', sortable: true },
  { key: 'description', label: 'Description', rowHeader: true },
  { key: 'amount', label: 'Amount', align: 'end', sortable: true,
    format: r => gbp(r.amount),
    srSuffix: r => (r.amount < 0 ? 'paid out' : 'paid in') },
]
table.rows = transactions

Already rendering a table yourself? Wrap it: with no columns set, the element keeps your markup and adds the labelled, keyboard-reachable scroll region.

HTML

<tai-data-table caption="Fees">
  <table>…your rows…</table>
</tai-data-table>

API

NameKindDescription
columnspropertyColumn definitions (next table).
groupsproperty[{ id, label, className }]. Consecutive columns with the same group share a header (scope="colgroup").
rowspropertyThe data. Setting it re-renders the body only; scroll position and header focus are kept.
rowMarkerpropertyrow => ({ label, short }) | null. Adds a visible badge (short) and spoken text (label) to the row header.
rowClasspropertyrow => string.
sortproperty{ key, direction } or null.
caption, descriptionattributesThe table's caption (also names the region) and a second line.
caption-hiddenattributeKeeps the caption for screen readers only.
empty-textattributeShown when there are no rows.
update-messageattributeAnnounced politely each time rows change after the first render (e.g. “Option chain updated”). Leave unset for fast-moving data.
busyattributeSets aria-busy on the table while loading.
densityattributecompact for dense numeric tables.
tai-sortevent, cancellabledetail: { key, direction }. Cancel it to sort on the server; the header state still updates.
--tai-dt-sticky-bg, --tai-dt-marker-color, --tai-dt-group-row-heightcustom propertiesSticky column background, marked-row border, second sticky header row offset.
KeyTypeDescription
key, labelstringData key and visible header.
srLabelstringReplaces the header for screen readers (“Calls open interest” for “OI”).
groupstringGroup id. Grouped headers get the group name as hidden prefix text.
rowHeaderbooleanRender cells as <th scope="row"> — the cell that names the row.
stickybooleanStays visible while scrolling sideways (sticks to whichever edge it reaches).
alignstart | end | centerNumbers: end.
sortable, sortValueboolean, functionHeader becomes a button; sortValue(row) overrides the value sorted on.
formatfunctionrow => string | Node.
srSuffixfunctionrow => string, appended as visually hidden text (“highest”, “paid out”).
cellClass, cellStyle, headerClassfunction / stringStyling hooks. Light DOM, so app CSS (including Tailwind) applies.

Keyboard

KeyAction
TabReaches the table's scroll area when it overflows (no extra stop when it fits), then any sortable headers.
← → ↑ ↓With the scroll area focused, scroll it — so columns past the edge are reachable without a mouse.
Enter / SpaceOn a sortable header: sort ascending, then descending.
Screen reader table keysKept intact (e.g. NVDA/JAWS Ctrl Alt + arrows; VoiceOver VO + arrows), because this is a real table — see below.

Accessibility notes

Why a table, not role="grid"

A grid turns the table into a single widget: arrow keys move a cell cursor and screen readers switch to application mode. That's right for spreadsheets and cells you edit or act on. For reading and comparing numbers it's worse — users lose their screen reader's own table navigation and header announcements. So this component is a table, and grid behaviour is something to opt into only when cells become interactive (for example “select a strike to add a leg”).

Repeated column names get their group as hidden text

An option chain has “LTP”, “OI” and “IV” twice — once under Calls, once under Puts. scope="colgroup" is the standard answer, but screen readers announce group headers unevenly. So each grouped header also carries its group as visually hidden prefix text: the cell is announced as “Calls LTP”, not just “LTP”, in every screen reader.

  • The at-the-money row is marked with text. The app showed it with a yellow cell and an amber border only. Here the row header gets a visible “ATM” badge plus “at the money” for screen readers, read with every cell in that row.
  • The scroll area is a labelled region named by the caption, focusable only when it actually overflows (the audit found it unreachable by keyboard on phones).
  • Sorting is announced (“Sorted by Amount, descending”) through one shared polite live region, and only the sorted column carries aria-sort.
  • Light DOM on purpose: a table split across shadow roots loses its header–cell relationships, so this component renders into the document.
  • Reflow: wide tables scroll inside their region instead of making the whole page scroll sideways.

Usage guidance

Do

  • Write a caption that says what the table is (“Current account — September”).
  • Mark the column that names each row as rowHeader.
  • Put meaning from colour into text with srSuffix or rowMarker.

Don't

  • Use it for layout.
  • Announce updates for fast-ticking data — once a minute is plenty.
  • Make every column sortable “just because”; sort what people actually compare.

Using it in React

React 19 sets columns, rows and rowMarker as properties, so functions and arrays arrive intact. Keep columns stable with useMemo.

JS

import '@tradeaialgo/tai-ui'

const columns = useMemo(() => buildChainColumns(spot), [spot])
return (
  <tai-data-table caption="NIFTY option chain" density="compact"
    columns={columns} groups={GROUPS} rows={rows}
    rowMarker={r => (r.atm ? { label: 'At the money', short: 'ATM' } : null)} />
)