DataView
A unified data display component with three interchangeable layouts — table, cards, and grid. All layouts share the same column definitions and data API. Pagination and sorting are SSR-native: controls render as <a> links that append URL params, the server re-queries and renders the next slice. No client JavaScript is required.
For client-side reactive tables with filtering, see DataTable.
Table layout
Click a column header to sort. The page re-renders server-side — sort state and page number live in the URL. This demo on the docs page is itself using SSR pagination.
| Language | Paradigm | First appeared | Type system | Weekly downloads |
|---|---|---|---|---|
| TypeScript | Multi-paradigm | 2012 | Static | 53,200,000 |
| JavaScript | Multi-paradigm | 1995 | Dynamic | 61,800,000 |
| Python | Multi-paradigm | 1991 | Dynamic | 29,400,000 |
| Rust | Multi-paradigm | 2015 | Static | 4,600,000 |
| Go | Concurrent | 2009 | Static | 5,100,000 |
// In your page handler, read state from the URL:
const page = Number(ctx.url.searchParams.get('page') ?? 1)
const sort = ctx.url.searchParams.get('sort') ?? undefined
const order = (ctx.url.searchParams.get('order') ?? 'asc') as 'asc' | 'desc'
// Sort and paginate server-side, then pass the slice:
const sorted = sort ? [...data].sort(...) : data
const pageData = sorted.slice((page - 1) * 20, page * 20)
const columns: DataViewColumn[] = [
{ key: 'name', label: 'Language', sortable: true },
{ key: 'year', label: 'Year', sortable: true, align: 'right' },
{ key: 'weekly', label: 'Downloads', sortable: true, align: 'right',
render: (row) => Number(row.weekly).toLocaleString() },
]
<DataView
layout="table"
columns={columns}
data={pageData}
page={page}
pageSize={20}
totalRows={data.length}
sort={sort}
order={order}
urlParams={Object.fromEntries(ctx.url.searchParams)}
/>Cards layout
Each row renders as a card. Assign role: 'title' and role: 'subtitle' to promote columns to the card header; remaining columns render as labeled key-value fields. If no column has a title role, the first column is used.
- TypeScriptMulti-paradigm
- Type system
- Static
- First appeared
- 2012
- Downloads
- 53,200,000
- JavaScriptMulti-paradigm
- Type system
- Dynamic
- First appeared
- 1995
- Downloads
- 61,800,000
- PythonMulti-paradigm
- Type system
- Dynamic
- First appeared
- 1991
- Downloads
- 29,400,000
- RustMulti-paradigm
- Type system
- Static
- First appeared
- 2015
- Downloads
- 4,600,000
- GoConcurrent
- Type system
- Static
- First appeared
- 2009
- Downloads
- 5,100,000
- KotlinMulti-paradigm
- Type system
- Static
- First appeared
- 2011
- Downloads
- 3,200,000
const columns: DataViewColumn[] = [
{ key: 'name', label: 'Language', role: 'title' },
{ key: 'paradigm', label: 'Paradigm', role: 'subtitle' },
{ key: 'typing', label: 'Type system' },
{ key: 'year', label: 'First appeared' },
]
<DataView layout="cards" columns={columns} data={data} pageSize={0} />Grid layout
A compact CSS grid — each item shows the title prominently and remaining fields as small key-value pairs. Columns default to 3; pass cols to override. The grid collapses to 2 columns at 768px and 1 column at 480px automatically.
- TypeScript
- Type system
- Static
- First appeared
- 2012
- Downloads
- 53,200,000
- JavaScript
- Type system
- Dynamic
- First appeared
- 1995
- Downloads
- 61,800,000
- Python
- Type system
- Dynamic
- First appeared
- 1991
- Downloads
- 29,400,000
- Rust
- Type system
- Static
- First appeared
- 2015
- Downloads
- 4,600,000
- Go
- Type system
- Static
- First appeared
- 2009
- Downloads
- 5,100,000
- Kotlin
- Type system
- Static
- First appeared
- 2011
- Downloads
- 3,200,000
- Swift
- Type system
- Static
- First appeared
- 2014
- Downloads
- 2,900,000
- C#
- Type system
- Static
- First appeared
- 2000
- Downloads
- 6,800,000
- Ruby
- Type system
- Dynamic
- First appeared
- 1995
- Downloads
- 2,300,000
<DataView layout="grid" columns={columns} data={data} cols={3} pageSize={0} />Table styling
The table layout accepts the same border and spacing props as Table.
| Language | Paradigm | Type system |
|---|---|---|
| TypeScript | Multi-paradigm | Static |
| JavaScript | Multi-paradigm | Dynamic |
| Python | Multi-paradigm | Dynamic |
| Rust | Multi-paradigm | Static |
| Go | Concurrent | Static |
<DataView
layout="table"
columns={columns}
data={data}
pageSize={0}
striped
highlightOnHover
withTableBorder
/>Row selection
Add selectable to render a checkbox column. Wrap DataView in a <form> to handle the selected values — each checkbox uses selectName as its field name and the rowKey value as its value. A minimal inline script handles the select-all toggle with no framework overhead.
<form method="post" action="/delete-languages">
<DataView
layout="table"
columns={columns}
data={data}
pageSize={0}
selectable
rowKey="name"
selectName="language"
/>
<button type="submit">Delete selected</button>
</form>
{/* Server reads: ctx.body.getAll('language') → ['TypeScript', 'Rust'] */}Selection in cards layout
Selection works the same way in cards and grid layouts. Each card shows a checkbox in the top-right corner; a select-all control appears above the list.
<form method="post" action="/action">
<DataView layout="cards" columns={columns} data={data} selectable rowKey="name" />
<button type="submit">Submit</button>
</form>Grid-table layout
A flex-row layout that looks like a table but is built with CSS — columns are responsive. Set span on each column to control proportional widths (same as the Grid component). Use hideBelow to remove a column entirely below a breakpoint, or collapseBelow to move it into an expandable row beneath the main row. Resize your browser window to see the columns adapt — no JavaScript required.
TypeScriptMulti-paradigm201253,200,000
JavaScriptMulti-paradigm199561,800,000
PythonMulti-paradigm199129,400,000
RustMulti-paradigm20154,600,000
GoConcurrent20095,100,000
KotlinMulti-paradigm20113,200,000
const columns: DataViewColumn[] = [
{ key: 'name', label: 'Language', span: 3 },
{ key: 'paradigm', label: 'Paradigm', span: 2, collapseBelow: 'sm' },
{ key: 'year', label: 'First appeared', span: 1, align: 'right', collapseBelow: 'md' },
{ key: 'typing', label: 'Type system', span: 2, hideBelow: 'sm' },
{ key: 'weekly', label: 'Weekly downloads', span: 2, align: 'right', collapseBelow: 'sm',
render: (row) => Number(row.weekly).toLocaleString() },
]
<DataView layout="grid-table" columns={columns} data={data} pageSize={0} />The span prop also accepts a breakpoint object for fine-grained control:
// 6-unit column at base, 4 units at sm and above, 2 units at lg and above:
{ key: 'name', label: 'Language', span: { base: 6, sm: 4, lg: 2 } }Layout switching with belowLayout
Pass belowLayout to render a fallback layout below a breakpoint. Both layouts are included in the HTML and CSS controls which is visible — zero JavaScript required. The default breakpoint is 'sm' (768px); override with belowBreakpoint.
TypeScriptMulti-paradigm201253,200,000
JavaScriptMulti-paradigm199561,800,000
PythonMulti-paradigm199129,400,000
RustMulti-paradigm20154,600,000
- TypeScript
- Paradigm
- Multi-paradigm
- First appeared
- 2012
- Type system
- Static
- Weekly downloads
- 53,200,000
- JavaScript
- Paradigm
- Multi-paradigm
- First appeared
- 1995
- Type system
- Dynamic
- Weekly downloads
- 61,800,000
- Python
- Paradigm
- Multi-paradigm
- First appeared
- 1991
- Type system
- Dynamic
- Weekly downloads
- 29,400,000
- Rust
- Paradigm
- Multi-paradigm
- First appeared
- 2015
- Type system
- Static
- Weekly downloads
- 4,600,000
{/* On desktop: grid-table. Below 992px: cards. */}
<DataView
layout="grid-table"
belowLayout="cards"
belowBreakpoint="md"
columns={columns}
data={data}
pageSize={0}
/>Empty state
Pass emptyState to customise the message shown when data is empty. Accepts any renderable content — a string, JSX, or a full call-to-action block.
| Language | Paradigm | First appeared |
|---|---|---|
| No languages found. Add one → | ||
<DataView
layout="table"
columns={columns}
data={[]}
pageSize={0}
emptyState={<span>No results. <a href="/add">Add one →</a></span>}
/>SSR pagination pattern
The key to SSR pagination is passing urlParams — the current page's full search params as a plain object. DataView merges them into every link it generates, so the active sort column survives page navigation and the active page survives sort changes.
export default definePage(async (ctx) => {
const page = Number(ctx.url.searchParams.get('page') ?? 1)
const sort = ctx.url.searchParams.get('sort') ?? undefined
const order = (ctx.url.searchParams.get('order') ?? 'asc') as 'asc' | 'desc'
// Your database query uses sort + page directly:
const { rows, total } = await db.query({
orderBy: sort,
direction: order,
limit: 20,
offset: (page - 1) * 20,
})
return (
<DataView
layout="table"
columns={columns}
data={rows}
page={page}
pageSize={20}
totalRows={total}
sort={sort}
order={order}
urlParams={Object.fromEntries(ctx.url.searchParams)}
/>
)
})Props
| Prop | Type | Default | Description |
|---|---|---|---|
layout | 'table' | 'list' | 'cards' | 'grid' | 'grid-table' | 'table' | Visual layout to render |
columns | DataViewColumn[] | — | Column definitions — shared across all layouts |
data | Record<string, unknown>[] | — | Current page's rows — pre-sliced, pre-sorted by the server |
page | number | 1 | Current page number (read from URL by the server) |
pageSize | number | 10 | Rows per page. 0 disables pagination |
totalRows | number | — | Total rows across all pages — required for multi-page pagination links |
pageParam | string | 'page' | URL query param name for the page number |
sort | string | — | Active sort column key (read from URL by the server) |
order | 'asc' | 'desc' | 'asc' | Active sort direction (read from URL by the server) |
sortParam | string | 'sort' | URL query param name for the sort column |
orderParam | string | 'order' | URL query param name for sort direction |
urlParams | Record<string, string> | {} | Current URL search params as a plain object — merged into every generated link so sort state survives page changes and page resets on sort. Pass Object.fromEntries(ctx.url.searchParams). |
selectable | boolean | false | Adds a checkbox column. Wrap in a <form> to handle submissions |
rowKey | string | — | Row field used as checkbox value. Falls back to row index |
selectName | string | 'selection' | Form field name for selection checkboxes |
emptyState | children | — | Content rendered when data is empty. Defaults to "No results found" |
cols | number | 3 | Number of columns in the grid layout |
renderCard | (row, columns, index) => string | — | Custom card renderer for list, cards, and grid layouts. Replaces built-in card content; DataView still wraps each item and injects the checkbox when selectable is set. |
renderRow | (row, columns, index) => string | — | Custom row renderer for grid-table layout. Replaces built-in row cells; DataView still handles the row wrapper, expand toggle, collapsed fields, and checkbox injection. |
belowLayout | DataViewLayout | — | Fallback layout shown when the viewport is narrower than belowBreakpoint. Both layouts are rendered in the HTML; CSS controls visibility — zero client JavaScript. |
belowBreakpoint | number | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'sm' | Breakpoint at which belowLayout activates. Named values map to standard widths (xs=576px, sm=768px, md=992px, lg=1200px, xl=1400px). Custom pixel values are also accepted. |
striped | boolean | 'odd' | 'even' | false | Alternating row shading (table only) |
highlightOnHover | boolean | false | Hover highlight on rows (table only) |
withTableBorder | boolean | false | Border around the entire table (table only) |
withColumnBorders | boolean | false | Vertical borders between columns (table only) |
withRowBorders | boolean | true | Horizontal borders between rows (table only) |
borderColor | string | — | CSS color for all table borders (table only) |
horizontalSpacing | UiSpacing | — | Cell horizontal padding (table only) |
verticalSpacing | UiSpacing | — | Cell vertical padding (table only) |
tableLayout | 'auto' | 'fixed' | 'auto' | CSS table-layout algorithm (table only) |
caption | string | — | Accessible caption text (table only) |
captionSide | 'top' | 'bottom' | 'bottom' | Caption placement (table only) |
DataViewColumn
| Field | Type | Description | |
|---|---|---|---|
key | string | Row object key to read | |
label | string | Column header text and field label in cards/grid | |
sortable | boolean | Renders the column header as a sort link (table layout). Server must re-query on navigation. | |
align | 'left' | 'center' | 'right' | Text alignment for header and cells (table layout) | |
width | number | string | Fixed column width — pixel number or CSS string. Most useful with tableLayout="fixed" | |
role | 'title' | 'subtitle' | 'detail' | Display role in cards and grid layouts. 'title' is shown prominently in the card header; 'subtitle' is shown beneath it; all other columns render as labeled key-value fields. If no column has role: 'title', the first column is used as the title. | |
render | (row) => string | number | … | Custom cell renderer. Receives the full row object and returns a display value. Use this for formatted numbers, dates, computed fields, or combining multiple fields into one display string. | |
span | number | { base?, xs?, sm?, md?, lg?, xl? } | 1 | Proportional column width in grid-table layout (flex weight). Pass a number for a fixed proportion, or a breakpoint object for responsive widths — { base: 6, sm: 3 } means 6 units by default, 3 units at sm and above. |
hideBelow | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | — | Hide this column entirely below the given breakpoint in grid-table layout. The column does not appear in the expanded section. |
collapseBelow | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | — | Move this column to an expandable section below the row when the viewport is narrower than the given breakpoint (grid-table only). A chevron toggle appears automatically. |