Elements

Table

A Table lays out a few rows the app writes as children (invoices, a summary, a comparison) and gives screen readers a table: the table itself, groups of rows, rows, column headers and cells, each with its role. Its parts are TableHeader, TableBody and TableFooter (groups of rows), TableRow (its cells side by side), TableHead (a column’s header cell), TableCell and TableCaption. The look is the app’s: no part has colors, borders, paddings or widths of its own, and each takes the app’s class and style. For many rows, sorting, selection and resizable columns, DataTable is the table.

el-table.tsx
el-table.tsxLive
import { mount, Text, View } from "nsh";
import { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "nsh/base";
import { Stage, size, ui } from "./palette";

// A few rows the app writes out: the parts give screen readers a table, the
// classes and the palette give it its look.
const invoices = [
  { id: "INV001", status: "Paid", method: "Card", amount: 250 },
  { id: "INV002", status: "Pending", method: "Transfer", amount: 150 },
  { id: "INV003", status: "Unpaid", method: "Card", amount: 350 },
  { id: "INV004", status: "Paid", method: "Cash", amount: 450 },
];
const widths = ["w-24", "flex-1", "w-24", "w-24"];

function Cell(props: { i: number; head?: boolean; end?: boolean; children: string }) {
  const style = { fontSize: size.label, color: props.head ? ui().muted : ui().text, fontWeight: props.head ? ("500" as const) : ("400" as const) };
  const Part = props.head ? TableHead : TableCell;
  return (
    <Part class={`${widths[props.i]} px-3 py-2.5`}>
      <Text style={{ ...style, textAlign: props.end ? "right" : "left" }}>{props.children}</Text>
    </Part>
  );
}

function App() {
  const line = { borderBottomWidth: 1, borderColor: ui().line };
  const total = invoices.reduce((sum, i) => sum + i.amount, 0);
  return (
    <Stage width={520}>
      <View style={{ ...ui().card, borderRadius: 12, overflow: "hidden" }}>
        <Table accessibilityLabel="Recent invoices">
          <TableHeader>
            <TableRow style={line}>
              {["Invoice", "Status", "Method", "Amount"].map((title, i) => (
                <Cell i={i} head end={i === 3}>
                  {title}
                </Cell>
              ))}
            </TableRow>
          </TableHeader>
          <TableBody>
            {invoices.map((inv) => (
              <TableRow style={line}>
                <Cell i={0}>{inv.id}</Cell>
                <Cell i={1}>{inv.status}</Cell>
                <Cell i={2}>{inv.method}</Cell>
                <Cell i={3} end>{`$${inv.amount.toFixed(2)}`}</Cell>
              </TableRow>
            ))}
          </TableBody>
          <TableFooter>
            <TableRow>
              <Cell i={0} head>Total</Cell>
              <Cell i={1}>{""}</Cell>
              <Cell i={2}>{""}</Cell>
              <Cell i={3} end>{`$${total.toFixed(2)}`}</Cell>
            </TableRow>
          </TableFooter>
          <TableCaption class="px-3 py-2">
            <Text style={{ fontSize: size.detail, color: ui().muted }}>A list of your recent invoices.</Text>
          </TableCaption>
        </Table>
      </View>
    </Stage>
  );
}

mount(App);
use nsh_base::{table, table_body, table_cell, table_head, table_header, table_row, theme};
use nsh_core::ui::prelude::*;

fn invoices() -> impl IntoElement {
    let cell = |label: &'static str| table_cell().w(px(120.0)).px_3().py_2().child(text(label));
    table()
        .accessibility_label("Recent invoices")
        .child(
            table_header().child(
                table_row()
                    .border_b_1()
                    .border_color(move || theme().border)
                    .child(table_head().w(px(120.0)).px_3().py_2().child(text("Invoice")))
                    .child(table_head().w(px(120.0)).px_3().py_2().child(text("Amount"))),
            ),
        )
        .child(table_body().child(table_row().child(cell("INV001")).child(cell("$250.00"))))
}

Table and its parts come from nsh/base, the nsh-base crate in Rust, where table(), table_header(), table_body(), table_footer(), table_row(), table_head(), table_cell() and table_caption() make them, with the typed style methods and .child(..). A row lays its cells out side by side; the app gives cells their widths, so the columns line up.

#Props

PropRustTypeDefaultDescription
accessibilityLabel.accessibility_label(..)stringnoneTable only: what screen readers call the table.
children.child(..)JSX.ElementnoneWhat the part holds: groups and a caption in a Table, rows in a group, cells in a row, content in a cell.
class, stylestyle methodsstring, StyleProp<ViewStyle>noneThe part’s look and layout.

#Parts

PartInsideRole for screen readers
Tabletable
TableHeaderTablerow group: the rows of column headers
TableBodyTablerow group: the rows of data
TableFooterTablerow group: totals, a summary row
TableRowa group, or Tablerow; its cells side by side
TableHeada TableRow of the headercolumn header
TableCellTableRowcell
TableCaptionTablecontent the app shows about the table

Each platform gives the roles to its screen reader: table, row, cell and column header roles on Linux (AT-SPI), the web (ARIA), Android and the GPUI backend; grids and groups on macOS and Windows; headers as headers on iOS.

#See also

  • DataTable: a table of any number of rows and columns, with selection, sorting and resizable, movable columns.
  • Accessibility: the accessibilityRole values, table, row, cell, columnheader and rowgroup among them.