Skip to content
Nizivo UI

DataView

The same records as a table on a wide screen and as a list on a narrow one. The server renders both and a container query shows one, by the view's own width, so nothing changes on hydration.

Server component, no client JS

Import

import { DataView } from '@nizivo/react/data-view';

// Once per app (Getting started), after
// @nizivo/tokens/tokens.css and @nizivo/react/css/base.css
import '@nizivo/react/css/data-view.css';

Static or interactive

Server. No client JavaScript. The server renders the table and the list, and CSS shows one of them, so no script measures the screen and the server HTML is final. How to choose.

Parts

DataView.Root holds a DataView.Table (the wide presentation: put a Table.Container in it) and a DataView.List (the narrow one: DataView.Item rows). From 40rem of the root's own width the table shows; below that, the list. It is the view's width, not the screen's, so a DataView in a narrow column shows the list on a wide screen too.

List rows

A DataView.Item is a row of Figma's Order List Item: 14 / 16 px padding, 12 px between its parts, at least 44 px tall, with a hairline below. Lay out what goes in it (an avatar, a title and a line under it, a value and a badge at the end). With interactive, the row's DataView.ItemLink is a stretched link, so the whole row is one hit area and one tab stop, as on Table.Row; other buttons and links in the row stay clickable above it.

Both presentations are in the page

The hidden presentation is display: none: out of the accessibility tree and the tab order. But both are in the HTML, so the records are rendered twice. Give the list an aria-label (the table has its caption), and do not put form controls with the same id or name in both; a selectable table keeps its selection in the table presentation only.

Examples

Basic

Wide (720 px): the table presentation, 08's Recent orders.

Recent orders
OrderCustomerFulfilmentTotal
#1042
Maya Rao
Unfulfilled$112.00
#1041
Jonah Okafor
Fulfilled$64.50
#1040
Priya Shah
On hold$238.00
import type { JSX } from 'react';
import { Avatar } from '@nizivo/react/avatar';
import { Badge, type BadgeTone } from '@nizivo/react/badge';
import { Inline } from '@nizivo/react/inline';
import { Table } from '@nizivo/react/table';
import { DataView } from '@nizivo/react/data-view';

interface Order {
  id: string;
  placed: string;
  customer: string;
  fulfilment: [string, BadgeTone];
  total: number;
}

/** Fixed data, as a server would load it; times are UTC instants. */
const ORDERS: readonly Order[] = [
  {
    id: '1042',
    placed: '2026-10-14T09:12:00Z',
    customer: 'Maya Rao',
    fulfilment: ['Unfulfilled', 'warning'],
    total: 112,
  },
  {
    id: '1041',
    placed: '2026-10-14T08:47:00Z',
    customer: 'Jonah Okafor',
    fulfilment: ['Fulfilled', 'success'],
    total: 64.5,
  },
  {
    id: '1040',
    placed: '2026-10-13T17:05:00Z',
    customer: 'Priya Shah',
    fulfilment: ['On hold', 'neutral'],
    total: 238,
  },
];

// An explicit locale and time zone: the server and the browser print the same text (§16.3).
const TIME = new Intl.DateTimeFormat('en-US', {
  hour: '2-digit',
  minute: '2-digit',
  hourCycle: 'h23',
  timeZone: 'UTC',
});

const MONEY = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });

/** The same orders as table rows and as list rows; the root's width picks one. */
function Orders({ width }: { width: number }): JSX.Element {
  return (
    <DataView.Root style={{ inlineSize: width }}>
      <DataView.Table>
        <Table.Container>
          <Table.Root>
            <Table.Caption visuallyHidden>Recent orders</Table.Caption>
            <Table.Header>
              <Table.Row>
                <Table.Head>Order</Table.Head>
                <Table.Head>Customer</Table.Head>
                <Table.Head>Fulfilment</Table.Head>
                <Table.Head numeric>Total</Table.Head>
              </Table.Row>
            </Table.Header>
            <Table.Body>
              {ORDERS.map((order) => (
                <Table.Row key={order.id} interactive>
                  <Table.Cell>
                    <Table.RowLink href={`#order-${order.id}`}>#{order.id}</Table.RowLink>
                  </Table.Cell>
                  <Table.Cell>
                    <Inline gap="2">
                      <Avatar name={order.customer} size="sm" decorative />
                      {order.customer}
                    </Inline>
                  </Table.Cell>
                  <Table.Cell>
                    <Badge tone={order.fulfilment[1]}>{order.fulfilment[0]}</Badge>
                  </Table.Cell>
                  <Table.Cell numeric>{MONEY.format(order.total)}</Table.Cell>
                </Table.Row>
              ))}
            </Table.Body>
          </Table.Root>
        </Table.Container>
      </DataView.Table>
      <DataView.List aria-label="Recent orders">
        {ORDERS.map((order) => (
          <DataView.Item key={order.id} interactive>
            <Avatar name={order.customer} size="lg" decorative />
            <span style={{ display: 'grid', flex: 1, gap: 2, minInlineSize: 0 }}>
              <DataView.ItemLink href={`#order-${order.id}`}>{order.customer}</DataView.ItemLink>
              <span style={{ font: 'var(--nz-font-mono-m)', color: 'var(--nz-text-tertiary)' }}>
                #{order.id} ·{' '}
                <time dateTime={order.placed}>{TIME.format(new Date(order.placed))}</time>
              </span>
            </span>
            <span style={{ display: 'grid', gap: 4, justifyItems: 'end' }}>
              <span style={{ font: 'var(--nz-font-numeric-m)' }}>{MONEY.format(order.total)}</span>
              <Badge tone={order.fulfilment[1]}>{order.fulfilment[0]}</Badge>
            </span>
          </DataView.Item>
        ))}
      </DataView.List>
    </DataView.Root>
  );
}

export function DataViewExample(): JSX.Element {
  return <Orders width={720} />;
}

Narrow

Narrow (360 px): the same orders as list rows, 08 Responsive's Order List Item.

Recent orders
OrderCustomerFulfilmentTotal
#1042
Maya Rao
Unfulfilled$112.00
#1041
Jonah Okafor
Fulfilled$64.50
#1040
Priya Shah
On hold$238.00
import type { JSX } from 'react';
import { Avatar } from '@nizivo/react/avatar';
import { Badge, type BadgeTone } from '@nizivo/react/badge';
import { Inline } from '@nizivo/react/inline';
import { Table } from '@nizivo/react/table';
import { DataView } from '@nizivo/react/data-view';

interface Order {
  id: string;
  placed: string;
  customer: string;
  fulfilment: [string, BadgeTone];
  total: number;
}

/** Fixed data, as a server would load it; times are UTC instants. */
const ORDERS: readonly Order[] = [
  {
    id: '1042',
    placed: '2026-10-14T09:12:00Z',
    customer: 'Maya Rao',
    fulfilment: ['Unfulfilled', 'warning'],
    total: 112,
  },
  {
    id: '1041',
    placed: '2026-10-14T08:47:00Z',
    customer: 'Jonah Okafor',
    fulfilment: ['Fulfilled', 'success'],
    total: 64.5,
  },
  {
    id: '1040',
    placed: '2026-10-13T17:05:00Z',
    customer: 'Priya Shah',
    fulfilment: ['On hold', 'neutral'],
    total: 238,
  },
];

// An explicit locale and time zone: the server and the browser print the same text (§16.3).
const TIME = new Intl.DateTimeFormat('en-US', {
  hour: '2-digit',
  minute: '2-digit',
  hourCycle: 'h23',
  timeZone: 'UTC',
});

const MONEY = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });

/** The same orders as table rows and as list rows; the root's width picks one. */
function Orders({ width }: { width: number }): JSX.Element {
  return (
    <DataView.Root style={{ inlineSize: width }}>
      <DataView.Table>
        <Table.Container>
          <Table.Root>
            <Table.Caption visuallyHidden>Recent orders</Table.Caption>
            <Table.Header>
              <Table.Row>
                <Table.Head>Order</Table.Head>
                <Table.Head>Customer</Table.Head>
                <Table.Head>Fulfilment</Table.Head>
                <Table.Head numeric>Total</Table.Head>
              </Table.Row>
            </Table.Header>
            <Table.Body>
              {ORDERS.map((order) => (
                <Table.Row key={order.id} interactive>
                  <Table.Cell>
                    <Table.RowLink href={`#order-${order.id}`}>#{order.id}</Table.RowLink>
                  </Table.Cell>
                  <Table.Cell>
                    <Inline gap="2">
                      <Avatar name={order.customer} size="sm" decorative />
                      {order.customer}
                    </Inline>
                  </Table.Cell>
                  <Table.Cell>
                    <Badge tone={order.fulfilment[1]}>{order.fulfilment[0]}</Badge>
                  </Table.Cell>
                  <Table.Cell numeric>{MONEY.format(order.total)}</Table.Cell>
                </Table.Row>
              ))}
            </Table.Body>
          </Table.Root>
        </Table.Container>
      </DataView.Table>
      <DataView.List aria-label="Recent orders">
        {ORDERS.map((order) => (
          <DataView.Item key={order.id} interactive>
            <Avatar name={order.customer} size="lg" decorative />
            <span style={{ display: 'grid', flex: 1, gap: 2, minInlineSize: 0 }}>
              <DataView.ItemLink href={`#order-${order.id}`}>{order.customer}</DataView.ItemLink>
              <span style={{ font: 'var(--nz-font-mono-m)', color: 'var(--nz-text-tertiary)' }}>
                #{order.id} ·{' '}
                <time dateTime={order.placed}>{TIME.format(new Date(order.placed))}</time>
              </span>
            </span>
            <span style={{ display: 'grid', gap: 4, justifyItems: 'end' }}>
              <span style={{ font: 'var(--nz-font-numeric-m)' }}>{MONEY.format(order.total)}</span>
              <Badge tone={order.fulfilment[1]}>{order.fulfilment[0]}</Badge>
            </span>
          </DataView.Item>
        ))}
      </DataView.List>
    </DataView.Root>
  );
}

export function DataViewNarrowExample(): JSX.Element {
  return <Orders width={360} />;
}

Parts

Use the parts through the namespace (DataView.Root) or by their flat names (DataViewRoot); both work from Server Components.

DataView.Root

Server component Renders data-slot="data-view" with the class nz-data-view. Props: DataViewRootProps. Source: src/data-view/data-view.tsx.

NameTypeDefaultDescription
childrenReactNode–

A DataView.Table, then a DataView.List, holding the same records.

renderRenderProp<NoState>–

Must render a <div>.

  • Also takes every attribute of <div>; children is redeclared above. (ComponentPropsWithRef<"div">)

render takes an element, or, in client code, a function of the props and no state.

DataView.Table

Server component Renders data-slot="data-view-table" with the class nz-data-view-table. Props: DataViewTableProps. Source: src/data-view/data-view.tsx.

NameTypeDefaultDescription
childrenReactNode–

The wide presentation: a Table.Container with its table.

renderRenderProp<NoState>–

Must render a <div>.

  • Also takes every attribute of <div>; children is redeclared above. (ComponentPropsWithRef<"div">)

render takes an element, or, in client code, a function of the props and no state.

DataView.List

Server component Renders data-slot="data-view-list" with the class nz-data-view-list. Props: DataViewListProps. Source: src/data-view/data-view.tsx.

NameTypeDefaultDescription
childrenReactNode–

The narrow presentation: DataView.Item rows.

renderRenderProp<NoState>–

Must render a <ul> or an <ol>.

  • Also takes every attribute of <ul>; children is redeclared above. (ComponentPropsWithRef<"ul">)

render takes an element, or, in client code, a function of the props and no state.

DataView.Item

Server component Renders data-slot="data-view-item" with the class nz-data-view-item. Props: DataViewItemProps. Source: src/data-view/data-view.tsx.

NameTypeDefaultDescription
interactivebooleanfalse

The whole row is the hit area of its DataView.ItemLink (a stretched link) and shows the hover fill; other controls in it stay clickable above the link.

renderRenderProp<NoState>–

Must render an <li>.

  • Also takes every attribute of <li>. (ComponentPropsWithRef<"li">)

render takes an element, or, in client code, a function of the props and no state.

Server component Renders data-slot="data-view-item-link" with the class nz-data-view-item-link. Props: DataViewItemLinkProps. Source: src/data-view/data-view.tsx.

NameTypeDefaultDescription
renderRenderProp<NoState>–

Must render an <a>, or a router's link component that renders one.

  • Also takes every attribute of <a>. (ComponentPropsWithRef<"a">)

render takes an element, or, in client code, a function of the props and no state.

Other exports

DataViewItemLinkProps type

import type { DataViewItemLinkProps } from '@nizivo/react/data-view'

The props of DataView.ItemLink, listed above.

DataViewItemProps type

import type { DataViewItemProps } from '@nizivo/react/data-view'

The props of DataView.Item, listed above.

DataViewListProps type

import type { DataViewListProps } from '@nizivo/react/data-view'

The props of DataView.List, listed above.

DataViewRootProps type

import type { DataViewRootProps } from '@nizivo/react/data-view'

The props of DataView.Root, listed above.

DataViewTableProps type

import type { DataViewTableProps } from '@nizivo/react/data-view'

The props of DataView.Table, listed above.

Data attributes and slots

Style parts by class (.nz-<slot>) and state by data attribute. Attribute meanings come from contracts/vocabulary.json.

SlotRendered byData attributes
data-view
.nz-data-view
DataView.Root
root (takes render)
none
data-view-item
.nz-data-view-item
DataView.Item
root (takes render)
  • data-interactive (present or absent) ← interactive
    The whole surface is interactive
data-view-item-link
.nz-data-view-item-link
DataView.ItemLink
root (takes render); interactive, focus: self
none
data-view-list
.nz-data-view-list
DataView.List
root (takes render)
  • data-presentation: "table""list"
    Current presentation
data-view-table
.nz-data-view-table
DataView.Table
root (takes render)
  • data-presentation: "table""list"
    Current presentation

CSS hooks

Set a hook on the component's class or any ancestor to restyle it without specificity games; leave it unset for the default shown.

HookDefaultRead as
--nz-data-view-item-bg-hover--nz-bg-hover
  • background on .nz-data-view-item[data-interactive]:hover @media (hover: hover)

Layers: nz.components. Classes: .nz-data-view, .nz-data-view-item, .nz-data-view-item-link, .nz-data-view-list, .nz-data-view-table.

Tokens this stylesheet reads (9)

--nz-bg-hover, --nz-border-subtle, --nz-border-width-default, --nz-font-label-m-strong, --nz-size-touch-min, --nz-space-0-5, --nz-space-3, --nz-space-4, --nz-tracking-label-m-strong

Accessibility

These parts are interactive. Each gets the focus outline and touch-action rule from @nizivo/react/css/base.css (the nz.base layer), so the floor survives a restyle:

  • data-view-item-link: draws the focus outline itself on keyboard focus.

Keyboard

KeysAction
TabMoves through the links and controls of the presentation that is shown; the hidden one is skipped.

Source: native links and controls; the hidden presentation is display none (data-view.css). The browser lane (test/browser/data-view.spec.tsx) checks which presentation shows at each width, Tab and the list rows' stretched link.

Figma

Node ids from design/component-map.json, which nz gen writes from component.json. The links open the Nizivo Figma library, which may ask you to sign in.

On this page