RoadieRoadie
  • Home
  • Brand
  • Foundations
  • Content
  • Tokens
  • Components
  • Charts
  • Widgets
  • Appearance
HomeBrandFoundations
Appearance

Components

  • Navigation

    • Breadcrumb
    • LinkBeta
    • NavigatorAlpha
    • Steps
    • Tabs
  • Layout

    • Accordion
    • CollapsibleBeta
    • PaneAlpha
    • ScrollArea
    • Separator
  • Actions

    • Button
    • IconButton
    • ToggleBeta
    • Toggle groupBeta
    • ToolbarBeta
  • Fields

    • EditorAlpha
    • Field
    • Fieldset
    • Input
    • Label
    • Number fieldBeta
    • OTP fieldBeta
    • Query fieldAlpha
    • Textarea
  • Choices

    • Autocomplete
    • CheckboxBeta
    • Combobox
    • Radio group
    • Select
    • SliderBeta
    • SwitchBeta
  • Date and time

    • CalendarBeta
    • CalendarTile
    • Countdown
    • Dashboard periodBeta
    • Date fieldBeta
    • Date pickerBeta
    • Date range pickerBeta
    • DateTime
    • Duration
    • Time fieldBeta
  • Collections

    • Card
    • Carousel
    • DataTableBeta
    • ListAlpha
    • Marquee
    • RecordGridAlpha
    • RecordTableAlpha
    • SortableAlpha
    • TableBeta
  • Overlays

    • Dialog
    • Drawer
    • MenuBeta
    • Popover
    • ToastBeta
    • Tooltip
  • Status

    • Badge
    • CalloutBeta
    • DeltaBeta
    • Empty state
    • MeterBeta
    • ProgressBeta
    • SkeletonBeta
    • SparklineBeta
    • StatTileBeta
  • Media & brand

    • AvatarBeta
    • Icon Tile
    • Image
    • Logo
    • PatternBeta
    • QR codeBeta
    • SpotIllustration
  • Text

    • Code
    • Highlight
    • KbdBeta
    • Mark
    • Prose

RecordGrid

Status
Alpha
Added
Components 2.16.020 Oct 2026
Docs updated
20 Oct 2026

A grid of record cards to search, filter, sort, and page through.

Import

import { RecordGrid, gridLayout } from '@oztix/roadie-components/record-grid'
import { Records, useRecords } from '@oztix/roadie-components/records'
import { recordFields } from '@oztix/roadie-core/records'

Card parts can hold functions (alt and cell), as can fields with a match, getRowId, and the useRecords instance, so render the grid in a client component, a file that starts with 'use client'.

Examples

Default

Describe the records once as fields, then name in card the field each part of a card shows. Search, the sort in Configure, and paging come built in. Cards are at least 16rem wide, as many columns fit as the width allows, and a phone shows one.

const venues = ['Kazoo Hollow Room', 'Antler Kettle Hall', 'Echidna Gaslight Theatre', 'Foxglove Tin Pavilion']
const cities = ['Brisbane', 'Melbourne', 'Sydney', 'Perth']
const names = ['Marzipan Thunderclap', 'Ember Galah Ball', 'Nectarine Hoedown', 'Tuxedo Possum Cabaret', 'Lampshade Disco']
const statuses = ['on-sale', 'selling-fast', 'sold-out']
const events = Array.from({ length: 24 }, (_, index) => ({
id: `event-${index}`,

Card parts

Each part names a field: title names the record and carries its getRowHref link, description sits under it, leading beside it, and trailing at its end, or over the banner when there is one. image is a 16:9 banner from an image URL, with a neutral tile and an image icon for a record without one. details are label and value pairs under the title, in order, each value at the card's end. A part is a field's key, or { key, alt, cell } to give an image its alt text or replace how the value shows. Values read as their field does, so a status field shows a Badge.

const field = recordFields()
const fields = [
field.text('name', { label: 'Venue' }),
field.location('location', { label: 'Location' }),
field.number('capacity', { label: 'Capacity' }),
field.number('events', { label: 'Events this year' })

States

Pass loading while records load and error when the load fails. With no records yet, loading shows still placeholder cards; with records, it dims them and runs a bar under the header. An error replaces the cards, and onRetry adds a Retry button. With no records, the empty state says whether none exist yet or none match, and offers to clear the search and filters.

const field = recordFields()
const fields = [field.text('name', { label: 'Event' }), field.location('location', { label: 'Location' }), field.number('sold', { label: 'Sold' })]
const events = [
{ id: 'a', name: 'Nectarine Hoedown', location: 'Brisbane', sold: 1840 },
{ id: 'b', name: 'Raffia Spaceship', location: 'Melbourne', sold: 920 }
]

Layouts

Give Records both a table and a grid, and Configure shows a Layout switcher above the sort. The view's layout changes with it, so the URL and a saved view keep the layout, and the search, filters, and sort stay as they are. Each layout comes back with its settings as last shown, such as the table's hidden columns, or the saved view's when it was saved in that layout, so switching back reads as unchanged. Content shows the layout the view names, or the first one given.

const venues = ['Kazoo Hollow Room', 'Antler Kettle Hall', 'Echidna Gaslight Theatre', 'Foxglove Tin Pavilion']
const names = ['Marzipan Thunderclap', 'Ember Galah Ball', 'Nectarine Hoedown', 'Tuxedo Possum Cabaret', 'Lampshade Disco']
const events = Array.from({ length: 18 }, (_, index) => ({
id: `event-${index}`,
image: '/cart-demo-event.svg',
name: `${names[index % names.length]} ${Math.floor(index / names.length) + 1}`,

Card fields

Configure lists the fields a card shows as details. Drag a handle to reorder them, or click it or press Enter for a Move menu. The eye beside each one hides it. Hidden fields are listed apart, in the fields' own order, and showing one again adds it to the end. The fields the card places as its title, description, image, leading, or trailing aren't listed. When the grid defines details, the last one stays shown.

The fields write the view's layout.fields, the shown keys in order, left out once back to the grid's details, so equalRecordViews reads the view as unchanged. A view's keys for fields this grid doesn't have, such as one only some people see, are kept in their places.

const field = recordFields()
const fields = [
field.text('name', { label: 'Event' }),
field.location('location', { label: 'Location' }),
field.date('startsAt', { label: 'Start date', moment: 'date' }),
field.number('sold', { label: 'Sold' }),

Selection and actions

Cards have no checkboxes, so a selectable grid offers Select mode, as a narrow table does. Select in the header shows a checkbox on each card, a tap anywhere on a card selects it rather than opening it, and the header offers Select all and Done. Escape or Done leaves the mode and clears the selection. The bulk actions float at the foot of the screen with the count. Switching to a table keeps the selection, with a checkbox on each row.

const field = recordFields()
const fields = [field.text('name', { label: 'Event' }), field.location('location', { label: 'Location' }), field.number('sold', { label: 'Sold' })]
const names = ['Nectarine Hoedown', 'Lampshade Disco', 'Ember Galah Ball', 'Raffia Spaceship']
const cities = ['Brisbane', 'Melbourne', 'Sydney', 'Perth']
const events = Array.from({ length: 8 }, (_, index) => ({
id: `event-${index}`,

Long lists

Past 100 records on a page, the grid renders only the rows of cards near the screen, measuring each row as it renders, so find in page and printing see only those. maxHeight or fill scrolls the cards in their own box. Give useRecords a loadRange and the grid loads records by range as people scroll, with placeholder cards for records still loading, a failed range's error and Retry in its place, and the first record on screen reported as position.row. A row set from outside, such as from the URL, scrolls into view at the row of cards that holds it. See Range loading for the loading side, which is the same for every layout.

const field = recordFields()
const fields = [field.text('name', { label: 'Event' }), field.location('location', { label: 'Location' }), field.number('sold', { label: 'Sold' })]
const names = ['Nectarine Hoedown', 'Lampshade Disco', 'Ember Galah Ball', 'Raffia Spaceship', 'Quokka Thunder Weekender']
const cities = ['Brisbane', 'Melbourne', 'Sydney', 'Perth', 'Hobart', 'Adelaide']
const events = Array.from({ length: 1000 }, (_, index) => ({
id: `event-${index}`,

Guidelines

Offer a grid where people recognise records by sight

layouts={[
tableLayout(columns),
gridLayout({ image: 'image', title: 'name', description: 'venue' })
]}

Do

Offer the grid beside the table for events, venues, and products, where an image or a status at a glance helps people find the record.

<RecordGrid fields={orderFields} card={{ title: 'orderNumber', details: ['total', 'placedAt', 'email', 'status', 'tickets'] }} />

Don’t

Don't use a grid for records people compare by their values, such as orders. A table lines the values up in columns.

Keep details to a few

gridLayout({ title: 'name', description: 'venue', details: ['startsAt', 'sold'] })

Do

Show two or three details. People add more from Configure when they need them.

gridLayout({ title: 'name', details: ['venue', 'location', 'startsAt', 'endsAt', 'sold', 'capacity', 'gross'] })

Don’t

Don't list every field. Tall cards fit few to a screen and bury the title.

Accessibility

  • caption names the cards: they are a list named by the caption, each card an item with its place in the whole list (aria-posinset and aria-setsize) when the list holds only some of them, such as a page of several or more than 100 cards. Loading by range, the size is -1 until the total is known.
  • A card's title is a paragraph, not a heading, since the grid can't know the page's outline. With getRowHref it is a link, and the whole card takes its hover and press; it opens in a new tab with Cmd or Ctrl, like any link.
  • Tab moves through the cards in reading order, row by row. In its own box (maxHeight or fill), the box is a region named "Events, scrolls" for a caption of Events.
  • In Select mode, each card's checkbox is named "Select" and the card's title, and covers the card, so a tap anywhere on it toggles, and the link steps aside. "Select" and "Done" are one button, which keeps focus as it switches. The floating bulk actions are a group named "Bulk actions". Escape leaves Select mode, and focus lands on Select.
  • The Configure button is named "Configure grid", as is the dialog it opens. The Layout switcher is a group named "Layout" of toggle buttons, Table and Grid, with the shown one pressed; arrow keys move between them, and focus stays on the one used as the layout switches. Each shown field's handle is named "Reorder" and the field, and its eye is a toggle button named "Show" and the field, pressed while the field shows. Showing or hiding a field moves it between the lists, and focus follows its eye. The last shown detail's eye is disabled when the grid defines details, and says why.
  • Loading by range, the list is aria-busy while a range loads, placeholder cards are hidden from screen readers, and a failed range is announced. Records.Status announces Loading, the error, the result count, and the selection.
  • Focus in a card that scrolls away, or whose record leaves, moves to the cards, so keyboard users keep their place.

API reference

RecordGrid

Records as a grid of cards with the standard toolbar, pagination and status.

datareadonly Row[]

Every record, which Records filters, sorts and pages in the browser. With `rowCount`, the page the server returned. Leave it out with `loadRange`, which holds the records it loads.

rowCountnumber

How many records the search and filters match on the server. Giving it turns on server mode: `data` is one page, already searched, filtered, sorted and paged, and the search waits for a pause in typing. Fetch the `request` that `onRequestChange` hears. Once given, server mode stays: an `undefined` count keeps the last one. Pass 0 until the first count arrives. With `loadRange`, each range's result carries the count instead.

rowCountExactboolean

False when `rowCount` is a floor the server stopped counting at, such as a search index's 10,000: counts read "10,000+" and paging stops at the last page the server can reach. With `loadRange`, each range's result carries it instead.

Defaults to true.

fieldsreadonly RecordsField[]
Required

What each record holds. Keep the array stable (module scope or `useMemo`).

getRowId((row: Row, index: number) => string)

Defaults to the record's index in `data`. Selecting in server or range mode needs it, as index ids repeat on every page and a refetch.

viewRecordView
defaultViewRecordViewDefaults
onViewChange((view: RecordView, position: Required<RecordPosition>) => void)

Gets the position too: a change to the search, filters or sort returns to the first page, and `onPositionChange` hears that first.

baselineRecordView

The saved or preset view the screen opened, so `modified` can tell when the view moves away from it and `resetView` can go back. Leave it out when no saved view is open.

positionRecordPosition

Page, page size and, in range mode, the first row on screen: session state, never part of a view.

defaultPositionRecordPosition
onPositionChange((position: Required<RecordPosition>) => void)
recordNameRecordName

Defaults to { one: 'record', other: 'records' }.

totalsRecordsTotals

Totals over every record the search and filters match, labelled "Totals for 1,234 records" by default. `true` or `'sum'` adds up the number and money fields in the browser, except shares, indexes and points. A server gives its own as `{ values }`, keyed by field, and `label`; keep passing the last ones while the next page loads. With `loadRange`, a range's result can carry them instead.

loadingboolean

A fetch is under way. With no records the layout shows placeholders; with records it dims them.

errorstring | boolean

The fetch failed. A string is shown as the message.

onRetry(() => void)

Shows a Retry button beside the error.

timeZonestring

The viewer's IANA zone: it decides what "today" is and how timestamps read. Defaults to the browser's.

nowInstantish

The moment relative dates resolve against. Defaults to when the list mounted in the browser: a server reads no clock, so a relative date filter shows the loading state until then. A new `now` that moves a relative date starts a range list over, so keep it stable.

scopereadonly RecordFilter[]

Filters the page sets, such as the event a list of tickets belongs to. They filter like the view's but are never part of it, so they are never saved, cleared or edited: `Records.Search` shows them as locked chips. In server mode they reach the fetch through `scopedQuery`.

selectableboolean

Lets people pick records for bulk actions.

Defaults to false.

selectionRecordSelection

Session state, like the position. A search or filter change drops picked records it hides.

defaultSelectionRecordSelection
onSelectionChange((selection: RecordSelection) => void)
rowActions((row: Row) => ReactNode)

Menu items for one record, such as `Menu.Item`s, shown in a menu on its row.

getRowHref((row: Row) => string)

The record's page. Return `undefined` for a record with none.

loadRange((request: RecordsRangeRequest) => Promise<RecordsRangeResult<Row>>)

Turns on range mode: one long list, searched, filtered and sorted on the server, that loads the records on screen as people scroll. Each gets the request for one range: one page of `pageSize` records from `start`, `end` exclusive. Resolve with its `rows` and, once known, `rowCount` (and `rowCountExact`). The list holds the rows for the request they answer, drops a reply for an older one, and starts over when the search, filters, sort, zone or a relative date's resolved dates change. Without a count, ranges load one after another until one comes back short. A rejection shows the range's error with Retry.

onRequestChange((request: RecordsRequest) => void)

Hears `request` once the list is in the browser, then each time it changes: what to fetch in server mode, as the list resolved it.

cardGridLayoutConfig<Row>
Required

The field each part of a card shows, as `gridLayout` takes them.

captionstring

Accessible name for the cards.

searchPlaceholderstring

Names the search field too, unless `searchLabel` is given.

Defaults to 'Search and filter'.

searchLabelstring

Names the search field when its placeholder doesn't.

searchFocusShortcutstring | false

The key that focuses the search, a `KeyboardEvent.key` value such as `/`, or `false` for none.

Defaults to '/'.

pageSizesnumber[]

Defaults to [25, 50, 100].

bulkActionsreadonly RecordsBulkAction[]

Turns on selection and shows the bulk actions once something is selected.

actionsreadonly RecordsAction<Row>[]

Act on everything the search and filters match, with nothing selected, such as an export. Shown at the end of the toolbar.

viewActionsRecordsViewActionsProps

Shows the open view's name, marked once it differs from `baseline`, with a menu to save, save as, rename, reset or delete it. The app keeps the views.

maxHeightstring

Scrolls the cards in their own box this tall, any CSS length.

fillboolean

Fills its parent's height and scrolls inside, like `maxHeight`. The parent needs a definite height, such as `Pane.Body`.

classNamestring
Previous page← MarqueeNext pageRecordTable →