Observable Framework 1.5.1 GitHub️ 1.8k

Table input

API · Source · The table input displays tabular data. It’s fast: rows are rendered lazily on scroll. It sorts: click a header to sort, and click again to reverse. And it selects: click a checkbox on any row, and the selected rows are exported as a view value. (And for searching, see the search input.)

By default, all columns are visible. Only the first dozen rows are initially visible, but you can scroll to see more. Column headers are fixed for readability.


To show a subset of columns, or to reorder them, pass an array of property names as the columns option. By default, columns are inferred from data.columns if present, and otherwise by iterating over the data to union the property names.

The header option lets you redefine the column title; this doesn’t change the name used to reference the data.

Inputs.table(penguins, {
  columns: [
  header: {
    species: "Penguin Species",
    culmen_length_mm: "Culmen length (mm)",
    flipper_length_mm: "Flipper length (mm)",
    culmen_depth_mm: "Culmen Depth (mm)"

By default, rows are displayed in input order. You can change the order by specifying the name of a column to sort by, and optionally the reverse option for descending order. The male Gentoo penguins are the largest in this dataset, for example. Undefined values go to the end.

Inputs.table(penguins, {sort: "body_mass_g", reverse: true})

Tables are view-compatible: using the view function, you can use a table to select rows and refer to them from other cells, say to chart a subset of the data. Click the checkbox on the left edge of a row to select it, and click the checkbox in the header row to clear the selection. You can shift-click to select a range of rows.

const selection = view(Inputs.table(penguins, {required: false}));
selection // Try selecting rows above!

The required option determines the selection when no items are selected from the table. If it is true (default), the selection contains the full dataset. Otherwise, the selection is empty.

The table input assumes that all values in a given column are the same type, and chooses a suitable formatter based on the first non-null value in each column.

To override the default formatting, pass format options for the desired columns.

Inputs.table(penguins, {
  format: {
    culmen_length_mm: (x) => x.toFixed(1),
    culmen_depth_mm: (x) => x.toFixed(1),
    sex: (x) => x === "MALE" ? "M" : x === "FEMALE" ? "F" : ""

The format function can return a text string or an HTML element. For example, this can be used to render inline visualizations such as bars or sparklines.

Inputs.table(penguins, {
  format: {
    culmen_length_mm: sparkbar(d3.max(penguins, d => d.culmen_length_mm)),
    culmen_depth_mm: sparkbar(d3.max(penguins, d => d.culmen_depth_mm)),
    flipper_length_mm: sparkbar(d3.max(penguins, d => d.flipper_length_mm)),
    body_mass_g: sparkbar(d3.max(penguins, d => d.body_mass_g)),
    sex: (x) => x.toLowerCase()
function sparkbar(max) {
  return (x) => htl.html`<div style="
    background: var(--theme-green);
    color: black;
    font: 10px/1.6 var(--sans-serif);
    width: ${100 * x / max}%;
    float: right;
    padding-right: 3px;
    box-sizing: border-box;
    overflow: visible;
    display: flex;
    justify-content: end;">${x.toLocaleString("en-US")}`

There’s a similar width option if you want to give certain columns specific widths. The table input defaults to a fixed layout if there are twelve or fewer columns; this improves performance and avoids reflow when scrolling.

You can switch layout to auto if you prefer sizing columns based on content. This makes the columns widths resize with the data, which can cause the columns to jump around as the user scrolls. A horizontal scroll bar is added if the total width exceeds the table’s width.

Set layout to fixed to pack all the columns into the width of the table.

The table’s width can be controlled by the width option, in pixels. Individual column widths can alternatively be defined by specifying an object with column names as keys, and widths as values. Use the maxWidth option if the sum of column widths exceeds the desired table’s width.

The align option allows to change the text-alignment of cells, which can be right, left, or center; it defaults to right for numeric columns, and left for everything else.

The rows option indicates the number of rows to display; it defaults to 11.5. The height and maxHeight options respectively set the height and maximum height of the table, in pixels. The height defaults to (rows + 1) * 22 - 1.

Inputs.table(penguins, {
  width: {
    culmen_length_mm: 140,
    culmen_depth_mm: 140,
    flipper_length_mm: 140
  align: {
    culmen_length_mm: "right",
    culmen_depth_mm: "center",
    flipper_length_mm: "left"
  rows: 18,
  maxWidth: 840,
  multiple: false,
  layout: "fixed"

You can preselect some rows in the table by setting the value option to an array of references to the actual objects in your data.

For example, to preselect the first two items, you could set value to:

penguins.slice(0, 2)

or, if you want to preselect the rows 1, 3, 7 and 9:

[1, 3, 7, 9].map((i) => penguins[i])

The multiple option allows multiple rows to be selected (defaults to true). When false, only one row can be selected. To set the initial value in that case, just reference the preselected object:

Inputs.table(penguins, {
  value: [1, 3, 7, 9].map((i) => penguins[i]),
  multiple: true

Thanks to Ilyá Belsky and Brett Cooper for suggestions.


Inputs.table(data, options)

The available table input options are:

If width is auto, the table width will be based on the table contents; note that this may cause the table to resize as rows are lazily rendered.