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
- Import
OTablefrom theooko-ui/tablepublic subpath. - Describe the
columnswith the item field each one displays and its header label, then pass theitems. - Provide
getItemKeyas soon as rows are selectable, sorted, or paginated on the server, so every row keeps a stable key. - Add
paginateto slice the items bypageSize; also passtotalwhen the server already returns one page.
Example
The rendered example and its source come from the same Vue single-file component.
| Name | Role | Age | |
|---|---|---|---|
| Alice Martin | Admin | 34 | |
| Bob Durand | Editor | 41 | |
| Chloe Petit | Viewer | 28 | |
| Average age | 38 |
Interactive playground
Change the selection mode, pagination, page size, and the loading and empty states. The preview updates immediately.
| Name | Role | Age | |
|---|---|---|---|
| User 1 | Admin | 24 | |
| User 2 | Editor | 27 | |
| User 3 | Editor | 30 | |
| User 4 | Admin | 33 | |
| User 5 | Editor | 36 |
Props
| Prop | Type | Default | Description |
|---|---|---|---|
columns | readonly OTableColumn<T>[] | undefined | Columns to display, in order. |
items | readonly T[] | undefined | Rows to display: every item, or only the current page when total is set. |
getItemKey | ((item: T) => OTableKey) | undefined | Returns the stable, unique key of an item; defaults to its position in items. |
ariaLabel | string | undefined | Accessible name of the table. |
selectable | OTableSelectionMode | undefined | Adds a selection column with radios (single) or checkboxes (multi). |
modelValue | OTableKey | readonly OTableKey[] | undefined | Controlled selection: one key in single mode, an array of keys in multi mode. |
loading | boolean | false | Replaces the rows with the loading state. |
paginate | boolean | false | Displays an OPagination below the table and slices items by page. |
page | number | undefined | Controlled current page, starting at one; kept internally when omitted. |
pageSize | number | 10 | Number of rows per page. |
total | number | undefined | Total row count for server-side pagination; items is then displayed without slicing. |
glass | boolean | false | Uses a translucent header and translucent rules for a glass surface. Defaults to the app's glass option. |
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue | OTableKey | OTableKey[] | Emits the selected key in single mode or the next array of selected keys in multi mode. |
update:page | number | Requests a new page. |
Slots
| Slot | Props | Description |
|---|---|---|
[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:empty | none | Replaces the message displayed when there is no row. |
table:loading | none | Replaces the message displayed while loading is set. |
Related types
| Type | Definition |
|---|---|
OTableColumn | interface 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; } |
OTableColumnAlign | type OTableColumnAlign = "start" | "center" | "end"; |
OTableKey | type OTableKey = number | string; |
OTableSelectionMode | type OTableSelectionMode = "single" | "multi"; |
CSS custom properties
| Custom property | Default | Description |
|---|---|---|
--o-table-gap | var(--o-space-3) | Sets the space between the table and its pagination. |
--o-table-cell-padding-block | var(--o-space-3) | Sets the block padding of every cell. |
--o-table-cell-padding-inline | var(--o-space-4) | Sets the inline padding of every cell. |
--o-table-border | 1px solid var(--o-border-color-subtle) | Sets the table frame and the separator between rows. |
--o-table-border-radius | var(--o-radius-surface) | Sets the table frame corner radius. |
--o-table-header-background | var(--o-background-neutral-subtle) | Sets the header row background. |
--o-table-row-hover-background | var(--o-background-neutral-subtle) | Sets the background of a hovered body row. |
--o-table-row-selected-background | var(--o-background-primary-subtle) | Sets the background of a selected body row. |
--o-table-glass-header-background | color-mix(in srgb, var(--o-background-neutral-subtle) 35%, transparent) | Sets the translucent header row background of a glass table. |
--o-table-glass-border | 1px 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
tablewiththead,tbody, andtfoot; every column header is athwithscope="col". Name the table withariaLabel. - Selection uses real
OCheckboxandORadiocontrols namedSelect row N, and aSelect all rowscheckbox 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
SelectionandActionsheaders. Give every control placed in therow:actionsslot its own accessible name. - While
loadingis set, the table exposesaria-busy="true"and the pagination is disabled. - Rows are not interactive. Put a native link or button in a cell or in the
row:actionsslot 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
glassreplaces 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'sglassoption unless set on the table;--o-table-glass-*tunes it.- Selection is controlled:
update:modelValueemits 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 initems. That is enough for a static list, but not for server-side pages, where positions repeat from one page to the next. pageis optional. Without it the table keeps the current page internally; withv-model:pagethe parent owns it. An out-of-range page is clamped to the last page.- With
total,itemsis displayed as received andtotalonly drives the page count. Load the requested page whenupdate:pagefires. - Each column exposes three slots named after its
key:keyfor its body cells,header:keyfor its header, andfooter:keyfor its footer cell. Arolecolumn is customized with#role,#header:role, and#footer:role. In#role,valuehas the type of the item'srolefield. The footer row appears as soon as onefooter:keyslot is provided, and is hidden while loading or empty. - The other slots are
row:actions,table:empty, andtable:loading; the prefixes keep them from colliding with an item field. - Columns size themselves from their content unless
widthis 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. OTableexposes no app defaults. The pagination follows theOPaginationapp defaults.- Deliberate differences from the pre-rebuild table: it is a native table instead of a CSS grid, so
widthis 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.