Skip to main content

Component | Table

Render a table. Tables offer:

  • Actions to add interactivity to each row.
  • Customizable columns to format data in a variety of ways.
  • Built-in search to filter rows.
  • Pagination and virtualization to efficiently scale to millions of rows.
const companies = [
{ name: "Apple", tier: "Enterprise", headquarters: "Cupertino, CA", arr: 150000 },
{ name: "Asana", tier: "Basic", headquarters: "San Francisco, CA", arr: 12000 },
/* ... */
{ name: "Notion", tier: "Basic", headquarters: "San Francisco, CA", arr: 8000 },
]

ui.table(
"table-key",
companies,
{
label: "Companies",
actions: [{
label: "View Details",
onClick: (row) => page.modal(() => ui.json(row), { title: "Company Details" })
}],
columns: [
"name",
{ key: "tier", format: "tag" },
"headquarters",
{ key: "arr", label: "ARR", format: "currency" }
]
}
)

By default, each row in the table will render with a checkbox. This can be disabled by passing the allowSelect property as false, which will render the table as a display only component.

Learn more about working with inputs.

Row Actions

Actions are a powerful construct that allows you to add row-level interactivity to your table. Tables can have an unlimited number of actions, rendered either as buttons or nested inside a dropdown menu.

Actions are often used as a foundation to create robust multipage apps.

Each action's callback will receive the row as an argument.

ui.table("table-key",
<list-of-users>,
{
actions: [
{ label: "Edit", onClick: (row) => doSomething(row) },
{ label: "Delete", onClick: (row) => doSomething(row) },
// The optional surface field will render the action as a button instead of inside a dropdown (or vice-versa if false).
{ label: "View", onClick: (row, idx) => doSomething(row, idx), surface: true }
]
allowSelect: false
})

Action object properties

label

required string
The user-facing label to display for the action.

onClick

required (row: TableRow, idx: number) => void
Callback to execute when the action is clicked. The row and index will be passed as arguments.

surface

optional boolean

Set to true render the action as a button. Set to false to render the action inside a dropdown.

By default, if the table has only one action, it will render as a button. If there are multiple actions, they'll render inside a dropdown.


Columns

By default, Compose infers the table columns and how to render them. This can be overriden by passing the columns property.

Basic Usage

You can pass a list of key strings, which Compose uses to map to the data. This allows you to control which columns are rendered, and in what order, while still leveraging Compose to infer the label and data type.

const users = [
{ name: "John", age: 30, createdAt: new Date("2021-01-01"), isActive: true },
{ name: "Jane", age: 25, createdAt: new Date("2021-01-02"), isActive: false },
]

ui.table("table-key", users, { columns: ["name", "age", "isActive"] })

Advanced Usage

Instead of strings, you can also pass objects that allows you to more granularly control each column. Each column object must have a key field to map to the data.

You can also include optional fields like label, width, and format to override Compose's attempt to infer them.

const users = [
{ name: "John", age: 30, createdAt: new Date("2021-01-01"), isActive: true },
{ name: "Jane", age: 25, createdAt: new Date("2021-01-02"), isActive: false },
]

ui.table("table-key", users, {
columns: [
{ key: "name", width: "150px" },
"age",
{ key: "createdAt", format: "date" },
{ key: "isActive", label: "Status", format: "boolean" }
]
})

Column object properties

key

required string
Specify which property of the data object should be displayed in that column.

label

optional string

Specify a custom label for the column. If not provided, the label will be inferred from the key


width

optional string

Specify a custom width for the column (e.g. "150px" or "3rem"). If not provided, the width will be inferred from the content.


format

optional string literal

The data type of the column. Specifying this allows Compose to render the column in a more user-friendly format, and sort and filter correctly. If not provided, the format will be inferred from the data.

Options:

  • "date": Render a date in a human-friendly format (e.g. Oct 10, 2000). Expects a Date object or ISO string.
  • "datetime": Render a datetime in a human-friendly format (e.g. Oct 10, 2000, 11:14 AM). Expects a Date object or ISO string.
  • "number": Render a number in a human-friendly format (e.g. 1,234).
  • "currency": Render a number in a currency format (e.g. $1,234.56).
  • "boolean": Render a boolean in a human-friendly format (e.g. `✅` or `❌`). Will perform a truthiness check on non-boolean values.
  • "tag": Render values (or lists of values) as colored pills. Useful for enum-type data. You can pass a tagColors property to specify the colors of the tags. If not provided, Compose will randomly assign colors to each value.
  • "string": Stringify the value and render as is. Use when you don't want any special formatting.

tagColors

optional Record<string, string | string[]>SDK v0.23.0+

Map tag colors to column values when using format: "tag". If this property is not provided, Compose will randomly assign colors to each value.

Colors can be mapped to single values or arrays of values. If a value isn't mapped to a color, it will use the _default color if specified, or be automatically assigned a color.

Available colors: red, orange, yellow, green, blue, purple, pink, gray, brown.

Example:

{
key: "status",
format: "tag",
tagColors: {
purple: ["todo", "backlog"],
orange: "in_progress",
green: "done",
_default: "gray" // Render any unmapped values as gray
}
}

Compose tags support string, number, and boolean values, or lists of these values. Any other value will be rendered as plain text.


Pagination

Introduced in SDK v0.24.0.

Automatic Pagination

Tables with more than 5000 rows are automatically paginated without any additional configuration on your end, enabling Compose to remain performant at scale.

An important caveat is that auto-pagination disables client-side search since the browser only has a paginated subset of the data. You can re-enable search by manually paginating the table.

Manual Pagination

For very large datasets, you can manually paginate tables by passing a getter function to the data parameter. Manual pagination enables you to:

  • fetch data from your data store as needed, instead of having to load everything into memory at once.
  • manage search using your own custom logic.
  • scale endlessly (millions of rows) without performance degradation.
import { TablePageChangeParams, TablePageChangeResponse } from "@composehq/sdk"

async function onPageChange(params: TablePageChangeParams): TablePageChangeResponse {
const { offset, pageSize, searchQuery, prevTotalRecords, prevSearchQuery } = params;

// Only refetch total records on initial load and when the search query changes.
const refetchTotal =
prevTotalRecords === null || prevSearchQuery !== searchQuery;

const totalRecords = refetchTotal
? await postgres.countUsers(searchQuery)
: prevTotalRecords;

const pageOfUsers = await postgres.getUsers(offset, pageSize, searchQuery);

return {
data: pageOfUsers,
totalRecords,
};
}

page.add(() => ui.table("users-table", onPageChange))

Type definitions:

// Passed to the onPageChange callback as a single object.
interface TablePageChangeParams {
// The number of rows to skip.
offset: number;
// The number of rows to return. You can return less than this if there
// aren't enough rows to fill the page (e.g. the last page).
pageSize: number;
// The search query to filter the rows by. Will be null if no search
// is being performed.
searchQuery: string | null;
// The previous total number of records. This is provided so that you
// only recalculate the total record count when needed.
prevTotalRecords: number | null;
// The previous search query. This is provided so that you only
// recalculate the total record count when needed.
prevSearchQuery: string | null;
}

// Expected return type of the onPageChange callback.
interface TablePageChangeResponse {
// The data for the current page. Should be an array of objects.
data: TableData;
// The total number of records. This is used to calculate the number
// of pages.
totalRecords: number;
}
note

If you're not yet ready to handle search, you can disable it by passing the searchable: false property to the table.

Row Selections

Compose disables row selections by default for paginated tables. Due to constraints introduced by server-side pagination, it's not possible for Compose to pass a list of table rows to callbacks (e.g. onChange or onSubmit) when the user selects rows.

You can re-enable row selections by passing a selectionReturnType: "index" property to the table. When this is enabled, Compose will pass a list of row indices to any callbacks instead of the rows themselves.

ui.table(
"users-table",
onPageChange,
{
selectionReturnType: "index",
onChange: (selectedRows) => {
console.log(selectedRows) // [0, 1, 2, ...]
}
}
)

API reference

Function signature

type TableRow = {
[key: string]: string | number | boolean | Date | null | undefined | object
}

ui.table<T extends TableRow>(
id: string,
data: T[] | TablePageChangeCallback<T>,
properties?: Partial<{
allowSelect: boolean,
label: string,
description: string,
required: boolean
initialSelectedRows: number[],
validate: (values: T[]) => string | void,
onChange: (values: T[]) => void,
columns: TableColumn[],
actions: TableAction<T>[],
minSelections: number,
maxSelections: number,
selectionReturnType: "full" | "index",
searchable: boolean,
style: Style,
}>
)

Parameters

id

required string
A unique identifier for the component. This is necessary so that Compose can properly pass user actions back to the component.

data

required TableRow[] | TablePageChangeCallback

The data to be displayed in the table. Each item in the array represents a row in the table, and should be an object where the keys correspond to column names and the values are the cell contents.

Compose supports most data types including strings, numbers, booleans, dates, and objects.

For larger datasets, you can paginate the table server-side by passing a function that returns one page of data at a time. See the pagination section for more details.


properties.allowSelect

optional boolean

Whether to show the selections column. Defaults to true.


properties.label

optional string

The label to display above the table. If not provided, the label will be inferred from the id.


properties.description

optional string

A description to display between the label and table.


properties.required

optional boolean

Validate that the table has at least one row selected before submitting the form it's part of or calling it's onChange hook. Defaults to true.


properties.initialSelectedRows

optional number[]

A list of row indices to pre-select when the table is first rendered. Defaults to [] (empty list).


properties.validate

optional (selectedRows: TableRow[]) => string | void

Validate the selected rows before submitting. If the function returns a string, it will be displayed as an error message.


properties.onChange

optional (selectedRows: TableRow[]) => void

A callback function that is called when the table selection(s) change. The callback is passed the list of currently selected rows.


properties.columns

optional TableColumn[]

Control how columns are rendered in the table. If not provided, Compose will default to using the keys from the first row of data, then do it's best to infer human readable column names, data types, etc. See the columns section for more details.


properties.actions

optional TableAction[]

Add row-level action buttons that execute custom functions when clicked. See the row actions section for more details.


properties.minSelections

optional number

Validate that the user has selected at least some number of rows. Defaults to 0.


properties.maxSelections

optional number

Validate that the user has selected at most some number of rows. Defaults to 1000000000.


properties.selectionReturnType

optional string literalSDK v0.24.0+

Options:

  • "full": Return the full row data to callback functions when the user selects rows.
  • "index": Return the row indices to callback functions when the user selects rows.

By default, this property is set to "full".

Paginated tables only support row selections by index. If you're using a paginated table, you'll need to set this property to "index" to enable row selections. If not, Compose will disable row selections for the paginated table.


properties.searchable

optional booleanSDK v0.24.0+

Whether to show the search input. Defaults to true.

The exception is auto-paginated tables (i.e. tables with more than 5000 rows). Client-side search is not possible for these tables, and you'll need to manage search yourself using manual pagination.


properties.style

optional Style

Directly style the underlying element using CSS. See the styling guide for more details on available style properties.