Skip to content

OTable ​

OTable displays a list of items as an accessible native table, with optional selection, per-column slots, a footer row, and on-demand pagination.

Usage ​

Step by step ​

  1. Import OTable from the ooko-ui/table public subpath.
  2. Describe the columns with the item field each one displays and its header label, then pass the items.
  3. Provide getItemKey as soon as rows are selectable, sorted, or paginated on the server, so every row keeps a stable key.
  4. Add paginate to slice the items by pageSize; also pass total when the server already returns one page.

Example ​

The rendered example and its source come from the same Vue single-file component.

NameRoleAge
Alice MartinAdmin34
Bob DurandEditor41
Chloe PetitViewer28

Interactive playground ​

Change the selection mode, pagination, page size, and the loading and empty states. The preview updates immediately.

NameRoleAge
User 1Admin24
User 2Editor27
User 3Editor30
User 4Admin33
User 5Editor36

Props ​

PropTypeDefaultDescription
columnsreadonly OTableColumn<T>[]undefinedColumns to display, in order.
itemsreadonly T[]undefinedRows to display: every item, or only the current page when total is set.
getItemKey((item: T) => OTableKey)undefinedReturns the stable, unique key of an item; defaults to its position in items.
ariaLabelstringundefinedAccessible name of the table.
selectableOTableSelectionModeundefinedAdds a selection column with radios (single) or checkboxes (multi).
modelValueOTableKey | readonly OTableKey[]undefinedControlled selection: one key in single mode, an array of keys in multi mode.
loadingbooleanfalseReplaces the rows with the loading state.
paginatebooleanfalseDisplays an OPagination below the table and slices items by page.
pagenumberundefinedControlled current page, starting at one; kept internally when omitted.
pageSizenumber10Number of rows per page.
totalnumberundefinedTotal row count for server-side pagination; items is then displayed without slicing.
glassbooleanfalseUses a translucent header and translucent rules for a glass surface. Defaults to the app's glass option.

Events ​

EventPayloadDescription
update:modelValueOTableKey | OTableKey[]Emits the selected key in single mode or the next array of selected keys in multi mode.
update:pagenumberRequests a new page.

Slots ​

SlotPropsDescription
[key]{ /** Column being rendered. */ column: OTableColumn<T>; /** Item of the row. */ item: T; /** Value of the item for the column, typed from the column key. */ value: T[K]; /** Zero-based position of the row. */ index: number; }Replaces the body cells of one column; defaults to the item's value for that column.
header:[key]{ /** Column being rendered. */ column: OTableColumn<T>; }Replaces the header of one column; defaults to the column label.
footer:[key]{ /** Column being rendered. */ column: OTableColumn<T>; /** Every item passed to the table, not only the current page. */ items: readonly T[]; }Fills the footer cell of one column, such as a total; the footer row appears when any is provided.
row:actions{ /** Item of the row. */ item: T; /** Zero-based position of the row. */ index: number; }Adds a trailing column of per-row actions.
table:emptynoneReplaces the message displayed when there is no row.
table:loadingnoneReplaces the message displayed while loading is set.
TypeDefinition
OTableColumninterface OTableColumn<T extends object> { /** Item field displayed by the column; also names its key, header:key, and footer:keyslots. */ key: keyof T & string; /** Plain-text column header. */ label: string; /** CSS width of the column, such as10remor30%; sized by its content when omitted. */ width?: string; /** Inline alignment of the header, body, and footer cells. */ align?: OTableColumnAlign; /** Tints the column with a palette color. */ color?: OokoColor; }
OTableColumnAligntype OTableColumnAlign = "start" | "center" | "end";
OTableKeytype OTableKey = number | string;
OTableSelectionModetype OTableSelectionMode = "single" | "multi";

CSS custom properties ​

Custom propertyDefaultDescription
--o-table-gapvar(--o-space-3)Sets the space between the table and its pagination.
--o-table-cell-padding-blockvar(--o-space-3)Sets the block padding of every cell.
--o-table-cell-padding-inlinevar(--o-space-4)Sets the inline padding of every cell.
--o-table-border1px solid var(--o-border-color-subtle)Sets the table frame and the separator between rows.
--o-table-border-radiusvar(--o-radius-surface)Sets the table frame corner radius.
--o-table-header-backgroundvar(--o-background-neutral-subtle)Sets the header row background.
--o-table-row-hover-backgroundvar(--o-background-neutral-subtle)Sets the background of a hovered body row.
--o-table-row-selected-backgroundvar(--o-background-primary-subtle)Sets the background of a selected body row.
--o-table-glass-header-backgroundcolor-mix(in srgb, var(--o-background-neutral-subtle) 35%, transparent)Sets the translucent header row background of a glass table.
--o-table-glass-border1px solid var(--o-border-glass)Sets the frame and row separators of a glass table.

Override example ​

css
:root {
  --o-table-cell-padding-block: var(--o-space-3);
  --o-table-header-background: transparent;
  --o-table-border: 1px dashed var(--o-border-neutral-default);
  --o-table-border-radius: 0;
}

Accessibility ​

  • The component renders a native table with thead, tbody, and tfoot; every column header is a th with scope="col". Name the table with ariaLabel.
  • Selection uses real OCheckbox and ORadio controls named Select row N, and a Select all rows checkbox that reports the native mixed state when only some displayed rows are selected. The radios of single mode share one native group.
  • The selection and actions columns have visually hidden Selection and Actions headers. Give every control placed in the row:actions slot its own accessible name.
  • While loading is set, the table exposes aria-busy="true" and the pagination is disabled.
  • Rows are not interactive. Put a native link or button in a cell or in the row:actions slot instead of making a row clickable.
  • Selected rows keep a visible outline in forced-colors mode; color is never the only indicator because the selection control stays checked.

Behavior and edge cases ​

  • glass replaces the gray header band with a light translucent tint, separated from the body by its rule, and makes the frame and row rules translucent, for a table inside a glass dialog or card. It follows the app's glass option unless set on the table; --o-table-glass-* tunes it.
  • Selection is controlled: update:modelValue emits one key in single mode and a new array in multi mode. The select-all checkbox adds or removes only the displayed rows, so keys selected on other pages are preserved.
  • Without getItemKey, a row's key is its position in items. That is enough for a static list, but not for server-side pages, where positions repeat from one page to the next.
  • page is optional. Without it the table keeps the current page internally; with v-model:page the parent owns it. An out-of-range page is clamped to the last page.
  • With total, items is displayed as received and total only drives the page count. Load the requested page when update:page fires.
  • Each column exposes three slots named after its key: key for its body cells, header:key for its header, and footer:key for its footer cell. A role column is customized with #role, #header:role, and #footer:role. In #role, value has the type of the item's role field. The footer row appears as soon as one footer:key slot is provided, and is hidden while loading or empty.
  • The other slots are row:actions, table:empty, and table:loading; the prefixes keep them from colliding with an item field.
  • Columns size themselves from their content unless width is set. A table wider than its container scrolls horizontally inside the component. The rounded frame belongs to that scroll container, so it clips the scrolling content at its corners; the last row drops its separator because the frame draws the bottom edge.
  • OTable exposes no app defaults. The pagination follows the OPagination app defaults.
  • Deliberate differences from the pre-rebuild table: it is a native table instead of a CSS grid, so width is a CSS width rather than a grid track; there is no compositional mode, expandable or nested rows, or clickable rows; slot value types come from the item type, so no column-definition helper is needed.