Elements

Components / Application

Data Table

Compound components for all tabular data in the app. Provides consistent chrome across list views, with optional toolbar controls whose complexity scales with the size and complexity of the dataset.

When to use what

PatternWhenExamples
Table, no toolbarFewer than ~8 rows, not searchable by name, scanned at a glance.Insurance policies per property, committee members, meeting agenda items.
Table + search8 to ~30 rows of named entities where the user may know who/what they want.Owners roll (lots 1–42), contractor list, document list.
Table + compound filtersMore than ~30 rows, or multi-axis filtering needed (status AND location AND health).Portfolio scheme index, arrears across all properties, compliance register.
Toolbar page actionsThe view needs create, upload, or export alongside sort and column controls.New document + Upload on a document register; Log issue on a maintenance list. Wrap in DataTableToolbarActions, not in the filter row.
Add card view toggleEntities have strong visual identity or the list is browsed rather than compared. Cards degrade better on mobile for content-rich entities.Scheme portfolio (schemes have icons, health colours). NOT owners roll (reference data — always compare columns).
Card view is not a substitute for filters. Add a card toggle when the layout benefit is real — entities have visual identity or users browse on mobile. Never add it purely for variety. When in doubt, default to the table view.

Examples

Plain table — no toolbar

5 insurance policies. No searching needed at this scale.

Preview
TypeInsurerCoverExpiryStatus
Building insurance
NRMA$24.5M18 Jun 2026Renewal pending
Public liability
NRMA$20.0M18 Jun 2027Current
Office bearers
QBE$1.0M30 Sep 2026Current
Workers compensation
Allianz—31 Dec 2026Current
Voluntary workers
Allianz$500k31 Dec 2026Current
tsx
import {
  DataTable, DataTableRow, DataTableCell,
  type DataTableColumn,
} from "@/components/ui/data-table"

const COLS: DataTableColumn[] = [
  { key: "type",   label: "Type"    },
  { key: "status", label: "Status"  },
]

<DataTable columns={COLS}>
  {rows.map(row => (
    <DataTableRow key={row.id} onClick={() => open(row)}>
      <DataTableCell>{row.type}</DataTableCell>
      <DataTableCell><Badge>{row.status}</Badge></DataTableCell>
    </DataTableRow>
  ))}
</DataTable>

Searchable table: search only

14 owners from a 42-lot roll. Search helps locate a specific owner by name or lot number. The result count and Clear all link sit below the table via DataTableFooter, not in the toolbar.

Preview
LotOwnerTypeContactLevy status
1
James Okonkwo

2 bed

Owner occ.

0411 234 567

james@example.com

Current
2
Sarah Patel

1 bed

Investor

sarah.patel@example.com

Current
5
Chen Wei

2 bed

Owner occ.

0422 345 678

Current
7
Robert Davis

3 bed

Owner occ.

0433 456 789

r.davis@example.com

Arrears
8
Lisa Wong

2 bed

Investor
No contact on file
Claim pending
12
Priya Singh

1 bed

Investor
No contact on file
Current
14
Margaret Chen

2 bed

Owner occ.

0444 567 890

m.chen@example.com

Current
18
Patricia Kim

2 bed

Owner occ.

0455 678 901

pkim@example.com

Current
22
David Park

2 bed

Owner occ.

0466 789 012

Current
23
Emma Thompson

1 bed

Investor

e.thompson@example.com

Arrears
27
Thomas Brown

2 bed

Owner occ.
No contact on file
Current
31
Jennifer Walsh

2 bed

Owner occ.

0477 890 123

j.walsh@example.com

Current
38
Michael O'Brien

3 bed

Owner occ.

0488 901 234

Arrears
42
Alicia Nguyen

3 bed penthouse

Investor
No contact on file
Current
14 lots
tsx
import {
  DataTable, DataTableRow, DataTableCell,
  DataTableToolbar, DataTableSearch, DataTableFooter,
} from "@/components/ui/data-table"

const [search, setSearch] = useState("")
const filtered = rows.filter(r => r.name.includes(search))

<DataTable
  columns={COLS}
  toolbar={
    <DataTableToolbar>
      <DataTableSearch value={search} onChange={setSearch} placeholder="Search..." />
    </DataTableToolbar>
  }
  footer={
    <DataTableFooter
      filtered={filtered.length}
      total={rows.length}
      noun="lots"
      showClear={Boolean(search.trim())}
      onClearAll={() => setSearch("")}
    />
  }
>
  {filtered.map(row => <DataTableRow key={row.id}>…</DataTableRow>)}
</DataTable>

Compound filter bar — search, filter conditions, sort, columns

14 schemes across multiple states. The + Filter button adds a condition chip for each field. Each chip shows Field is [Value] with a coloured dropdown and a clear control. Sort and column visibility sit at the right of the filter bar, separated from page actions by DataTableToolbarActions. The result count and Clear all link always sit below the filtered content as plain metadata, outside the table border. Use the same placement for non-table views (calendars, card grids): toolbar at the top, content in the middle, footer at the bottom.

Preview
SchemeLocationLotsManagerStatusHealthLevy
HV

Harbour View Towers

QLD42Sarah MitchellActiveWarningDue
TQ

The Quarter

QLD24Sarah MitchellActiveCriticalOverdue
NE

Northbridge Estate

QLD28Sarah MitchellActiveWarningDue
PR

Parkside Residences

QLD16Sarah MitchellActiveGoodCurrent
SG

Sunset Gardens

NSW8Tom WalshActiveGoodCurrent
MB

Marina Bay Complex

NSW72Tom WalshInactiveCriticalOverdue
BJ

Bondi Junction Centre

NSW92Tom WalshActiveWarningCurrent
RH

Rosebay Heights

NSW28Tom WalshActiveGoodDue
SR

Skyline Residences

VIC88Priya NairActiveGoodCurrent
FL

Fitzroy Lane Terraces

VIC16Priya NairActiveGoodCurrent
TG

The Green Residences

VIC32Priya NairActiveWarningOverdue
WV

Westfield Village

WA56Aisha KhanPendingWarningDue
MV

Mountain View Estate

TAS18Aisha KhanActiveWarningCurrent
CC

City Central Tower

ACT64Aisha KhanActiveGoodCurrent
14 schemes
tsx
import {
  DataTable, DataTableRow, DataTableCell,
  DataTableCompoundToolbar, DataTableFooter, DataTableSearch,
  DataTableToolbarActions,
} from "@/components/ui/data-table"

const anyFilters = Boolean(search.trim()) || conditions.some(c => c.value)

<DataTable
  columns={COLS}
  toolbar={
    <DataTableCompoundToolbar
      controls={
        <>
          <SortButton ... />
          <ColumnsButton ... />
          <DataTableToolbarActions>
            <Button variant="outline" size="sm">Upload</Button>
            <Button size="sm">New document</Button>
          </DataTableToolbarActions>
        </>
      }
    >
      <DataTableSearch value={search} onChange={setSearch} />
      {/* FilterConditionChip, AddFilterButton, etc. */}
    </DataTableCompoundToolbar>
  }
  footer={
    <DataTableFooter
      filtered={filtered.length}
      total={rows.length}
      noun="documents"
      showClear={anyFilters}
      onClearAll={clearAll}
    />
  }
>
  {filtered.map(row => <DataTableRow key={row.id}>…</DataTableRow>)}
</DataTable>
tsx
{/* Page CTAs belong in controls, wrapped in DataTableToolbarActions.
    Keep sort/columns outside the wrapper; use outline for secondary, default for primary. */}

<DataTableCompoundToolbar
  controls={
    <>
      <CompoundSortButton sortField={sortField} sortDir={sortDir} ... />
      <CompoundColumnsButton columns={TOGGLE_COLS} hidden={hiddenCols} ... />
      <DataTableToolbarActions>
        <Button variant="outline" size="sm">Upload</Button>
        <Button size="sm">
          <Plus className="size-3.5" aria-hidden />
          New document
        </Button>
      </DataTableToolbarActions>
    </>
  }
>
  <DataTableSearch ... />
  {/* filter chips */}
</DataTableCompoundToolbar>

{/* Do not put create/upload buttons in the filter row (children).
    Do not mix them with DataTableFilterChips — chips filter data; actions change it. */}

Toolbar page actions

Create, upload, and export buttons live in the controls slot of DataTableCompoundToolbar, wrapped in DataTableToolbarActions. The left border separates page actions from sort and column toggles. Use variant="outline" for secondary actions (Upload, Export) and one default Button for the primary action (New document). Max two buttons in the group. Never place these in the filter row beside search or condition chips.

Filter row (children)

Search, filter condition chips, + Filter. These refine what is shown.

Controls row (controls)

Sort, Columns, then DataTableToolbarActions for page CTAs. These change or add data.

Footer (footer prop)

Result count and Clear all only. Never put action buttons here.

Table with card view toggle

The same scheme data with a Table / Cards toggle. Use this pattern when entities have a visual identity (initials, health colour, icon) and users browse on mobile. Always keep the table view as the default — it is better for comparison and data-dense reading.

Preview
SchemeLocationLotsStatusHealthLevy
HV
Harbour View Towers
QLD42ActiveWarningDue
TQ
The Quarter
QLD24ActiveCriticalOverdue
NE
Northbridge Estate
QLD28ActiveWarningDue
PR
Parkside Residences
QLD16ActiveGoodCurrent
SG
Sunset Gardens
NSW8ActiveGoodCurrent
MB
Marina Bay Complex
NSW72InactiveCriticalOverdue
BJ
Bondi Junction Centre
NSW92ActiveWarningCurrent
RH
Rosebay Heights
NSW28ActiveGoodDue
SR
Skyline Residences
VIC88ActiveGoodCurrent
FL
Fitzroy Lane Terraces
VIC16ActiveGoodCurrent
TG
The Green Residences
VIC32ActiveWarningOverdue
WV
Westfield Village
WA56PendingWarningDue
MV
Mountain View Estate
TAS18ActiveWarningCurrent
CC
City Central Tower
ACT64ActiveGoodCurrent
14 schemes

Inside a Widget — framed={false}

When the table lives inside an existing bordered container (a Widget, a card) pass framed={false} so the outer rounded border and shadow are omitted. The header stripe and dividers still render.

tsx
{/* Inside a Widget — pass framed={false} to suppress the duplicate border */}
<Widget>
  <WidgetHeader>…</WidgetHeader>
  <WidgetContent flush>
    <DataTable columns={COLS} framed={false}>
      {rows.map(row => <DataTableRow key={row.id}>…</DataTableRow>)}
    </DataTable>
  </WidgetContent>
</Widget>

Props

DataTable

PropTypeDefaultDescription
columnsDataTableColumn[]—Column definitions that drive the <thead> row. Each entry has key, label, align, width, and className.
childrenReactNode—One or more <DataTableRow> elements rendered as <tbody> content.
toolbarReactNodeundefinedControls rendered above the table — wrap with <DataTableToolbar> or <DataTableCompoundToolbar>. Omit for small datasets that don't need filtering.
footerReactNodeundefinedRendered below the table, outside the framed border. Use <DataTableFooter> for result count and Clear all.
framedbooleantrueWhen true (default) wraps the table in a bordered container. Pass false when the table lives inside an existing border such as a Widget.
minWidthstring"640px"CSS min-width for the inner <table> element. Increase for wide column sets that need horizontal scroll.
emptyMessageReactNode"No records found."Shown as a spanning cell when children is empty.

DataTableRow

PropTypeDefaultDescription
onClick() => voidundefinedWhen provided, adds a pointer cursor and hover highlight.
tone"warning" | "danger"undefinedApplies a translucent tinted background — use for at-risk rows.
classNamestringundefinedExtra Tailwind classes.

DataTableCell

PropTypeDefaultDescription
align"left" | "right" | "center""left"Text alignment. Use right for numeric columns.
colSpannumberundefinedHTML colSpan for spanning cells.
classNamestringundefinedExtra Tailwind classes.

DataTableSearch

PropTypeDefaultDescription
valuestring—Controlled search query.
onChange(value: string) => void—Called on every keystroke with the new value.
placeholderstring"Search..."Input placeholder text.
iconReact.ElementTypeSearchLeading icon component. Pass a domain-specific icon from lucide-react to add context (e.g. User for an owners roll).

DataTableFilterChips

Simple inline pill chips — best for 3–5 mutually exclusive options shown permanently in the toolbar. For multi-field, additive filtering use the compound filter pattern shown in Example 3 above.

PropTypeDefaultDescription
optionsDataTableFilterOption[]—Array of { value, label } options.
valuestring (generic T)—Currently active filter value.
onChange(value: T) => void—Called when the user clicks a chip.
labelstringundefinedOptional prefix label shown before the chips, e.g. "Health:".

DataTableToolbar

Wrapper component — renders a flex-wrap items-center gap-3 row. Pass as the toolbar prop of DataTable. Accepts any children.

DataTableCount

Renders N rows when unfiltered, N of M rows when filtered. Props: filtered, total, noun (defaults to “rows”). Do not place this in the toolbar. Pass these props to DataTableFooter so the count sits below the table or filtered view.

DataTableCompoundToolbar

Layout wrapper for the compound filter bar. Pass search and filter condition chips as children. Pass sort, column visibility, and page action buttons via controls. Wrap create/upload/export buttons in DataTableToolbarActions so they stay visually distinct from table controls.

PropTypeDefaultDescription
childrenReactNode—Search input, active filter condition chips, and the + Filter button.
controlsReactNodeundefinedSort, Columns, and DataTableToolbarActions for page CTAs.

DataTableToolbarActions

Groups page-level CTAs in the compound toolbar controls slot. Renders a left border divider so Upload, New document, Export, and similar buttons are clearly separate from sort and column toggles. Place after table controls; max two buttons with one primary (default variant) only.

PropTypeDefaultDescription
childrenReactNode—One or two Button elements: outline for secondary, default for primary.

DataTableFooter

Table footer with result count and optional Clear all link. Renders below the table frame (or below any alternate content such as a card grid or calendar), outside the border. Pass as the footer prop of DataTable, or place it manually at the bottom of a space-y-3 wrapper when the content is not a table.

PropTypeDefaultDescription
filterednumber—Number of rows after filtering.
totalnumber—Total rows before filtering.
nounstring"rows"Noun for the count label, e.g. issues, lots, schemes.
showClearbooleanfalseWhen true, renders Clear all beside the count.
onClearAll() => voidundefinedCalled when the user clicks Clear all.

DataTableClearAll

Forest-coloured reset link. Usually rendered by DataTableFooter when showClear is true. Props: onClick.