Skip to content

Displays tabular data, configurable columns and data mapper.

import { BbTable } from 'bitboss-ui';

On this page

Props

NameDescription
accessibleLabel Type: ((columns: MappedCell[], item: any) => string)

The label of each row's selection checkbox, from (columns, item). Unset: the row's name in the locale's select template (its rowHeader cells, else its first column: "Select INV-1041"). The cells arrive in render order: look one up by key, never by position.

Called with the mapped cells in render order, so look a cell up by key rather than by position — order and reorderable both move them.

See example
actionsText Type: string

Label used in the header of the actions column.

align Type: "left" | "center" | "right"Default"left"

Text alignment of the columns.

caption Type: string

Caption describing the table's content, for accessibility.

The table's accessible name, and the fallback name of the <fieldset> a selectable table renders. Visually hidden unless display-caption is set. In v3 it is a <div> before the grid root rather than a <caption> inside it.

See example
columns Type: BbTableColumn<T>[]Default[]

Array of definitions of how the columns should be rendered.

Write these as BbTableColumn<YourItem>[]. A column carries extraction (key, dot paths allowed), formatting (formatter, placeholder, formatOnNull), presentation (label, align, width, skeleton), rowHeader for the column that names each row, and styling (thClass, tdClass, rowClass). actions and select are not columns: the #actions slot creates the actions column and selectable the selection one. A column keyed like one of the table's own slots (actions, select, expand, empty…) collides with that slot. Pin with fixed: 'left' | 'right', hide with hidden, lock against reordering with reorderable: false.

See example
compact Type: booleanDefaultfalse

Adds a CSS class that applies a compact style to the component.

dependencies Type: unknown[]Default[]

Defines an array of dependencies that will trigger actions in the component upon change.

depsDebounceTime Type: numberDefault0

Timeout used to debounce response to changes to dependencies.

disabled Type: booleanDefaultfalse

Disables the component

disableSelectAll Type: booleanDefaultfalse

Hides the "Select all" checkbox that selectable multi-row tables display by default.

Replaces v2's allow-select-all, with the polarity inverted: allow-select-all="false" becomes disable-select-all. The old name lands in $attrs and does nothing, so the change is silent.

See example
displayCaption Type: booleanDefaultfalse

Shows the caption as a visible title. Off by default: the caption is hidden, and screen readers still hear it as the table's name.

emptyText Type: stringDefaultthe localized common.emptyText ("No data to display")

Text shown when there are no items.

Also the place to explain a failed fetch: there is no error-text prop and no table-level error state, and a provider that rejects keeps the previous rows on screen. Catch the error in the provider, keep the reason in a ref, return [], and show it through this text or the #empty slot.

See example
enforceCoherence Type: booleanDefaultfalse

After every load, prunes modelValue, unselected-items, highlighted and expanded-items of values whose row is gone (a single value resets to null). Never on a provider-paginated table: a page change would prune the other pages' selection; reconcile in the provider instead.

Leave it off on any provider-paginated table (items is a function): every page turn is a load, so a cross-page selection would be pruned away. Reconcile in the provider instead, which receives the current selection for exactly this reason.

See example
expandedItems Type: any[]Default[]

Used by v-model:expandedItems: the expanded items, toggled by the row expand control and by external writes, like the highlighted / sort v-models. Drives the #expand rows and their aria-expanded / aria-controls wiring.

failedText Type: stringDefaultthe localized common.failedText ("Couldn't load items")

String displayed in the empty state while the latest load FAILED (the items provider threw): the twin of emptyText for that case.

fixed Type: booleanDefaultfalse

Gives the columns that declare no width an equal share of the width (a minmax(0, 1fr) track each).

fixedActions Type: booleanDefaultfalse

Pins the actions column (the one the #actions slot creates) to the right edge, position: sticky, in place. It has no column definition, so this is its column.fixed. No effect without an #actions slot.

fixedFirst Type: booleanDefaultfalse

Sticks the #<key>:first row to the top of the scrollport (under a stuck header).

fixedHeaders Type: booleanDefaultfalse

Sticks the headers to the top of the table.

fixedLast Type: booleanDefaultfalse

Sticks the #<key>:last row (a totals row) to the bottom of the scrollport.

fixedSelect Type: booleanDefaultfalse

Pins the selection column to the left edge, position: sticky, in place. It has no column definition, so this is its column.fixed. No effect unless the table is selectable.

Pinned cells are opaque and paint the table's --bg local, so a table on a surface that is not --bb-panel needs that one custom property set or the pinned column reads as a white hole. The same applies to fixed-actions and to any column.fixed.

See example
headerRowClass Type: Classes

Classes for the header row.

highlightable Type: boolean | ((item: Item) => boolean)

Which rows a click, Enter or toggleHighlighted may highlight: false for none, or a function of the row's item. Unset: every row, when highlighted is bound. A value the app sets still shows on a refused row.

highlighted Type: any

Used by v-model:highlighted: the one row the UI is about (say, whose details are open), a single item value or null. Opt-in: bound, a row click highlights; unbound, it only emits click:row. The row gets aria-current="true" and a shipped tint.

id Type: string

Stable id for this table's width context. When omitted a unique id is generated. Nested tables use the nearest ancestor id to inherit widths.

The rendezvous key for useBbTableContext(id) and for a BbPagination with a matching table-id. A typo does not error — it creates a second, unattached state node, so a toolbar that "works" while the table ignores it is almost always a misspelled id.

See example
inheritColumnWidths Type: string | boolean

Snaps a nested table's columns to an ancestor table's tracks: true = the nearest one, a table id = that one (when a table in between has a different column count). Off by default: a nested table sizes itself.

interactiveWhileLoading Type: booleanDefaultfalse

Keeps the header and rows interactive while loading (by default they are inert: a refetch is about to replace them). For background refreshes that must not interrupt an open editor.

items requiredType: Item[] | ((context: BbTableItemsContext) => Item[] | Promise<Item[]>)Default[]

Rows to render: an array, or a provider called with a BbTableItemsContext (reason, modelValue, signal), sync or async. A provider fails by THROWING, never by returning []: the rows clear and #empty gets the error. Return the page as it is.

Pass rows or a provider. Put filters and sort in dependencies; the table debounces, reports loading and discards stale responses.

See example
itemValue Type: ItemAccessor<Item>

The row's value, as a property path or a function returning any value; the selection, expandedItems and row keys compare by it.

Stable row identity as a path or function. Set it before adding selection, expansion or highlight so those models survive refetches.

See example
keyboardNavigation Type: booleanDefaultfalse

Opt-in keyboard navigation: the table becomes an ARIA grid (treegrid with an #expand slot) with ONE tab stop and roving focus over its cells. Controls in cells leave the tab order (Enter/F2 go in). Ignored with a #tbody slot.

Makes the table an ARIA grid (a treegrid with #expand): one tab stop, focus moves between cells, Enter/Space open the row, Enter/F2 step into a cell's controls. Controls inside cells leave the tab order while it is on. Ignored under a #tbody slot.

See example
legend Type: string

The <legend> of a selectable table (it renders as a <fieldset>): the selection's accessible name. Falls back to caption, then to a localized default.

Only rendered by a selectable table. Say what the selection is for ("select invoices to export") — that is what makes it worth more than the caption fallback.

See example
loading Type: booleanDefaultfalse

Loading state: skeletons with no rows yet, else the rows stay on screen, dimmed, under an indeterminate bar. A refetch never blanks a table the user is reading.

With rows already on screen this no longer blanks the table: the rows stay, dimmed, under a progress bar, and the header and rows go inert unless you set interactive-while-loading. Skeletons appear only when there is nothing to keep.

See example
loadingText Type: stringDefaultthe localized common.loadingText ("Loading...")

Screen-reader announcement for the loading row's aria-live status. Visually hidden — the skeleton (or the #loading slot) is the visible signal.

max Type: numberDefaultInfinity

Maximum number of items that can be selected.

modelValue Type: anyDefault[]

Used by v-model. Can be any serializable type.

multiple Type: booleanDefaulttrue

Allows the selection of multiple items.

multiSort Type: booleanDefaultfalse

A plain click on a sortable header adds the column to the sort, as Shift+click always does. Off, a plain click makes the sort that column alone.

name Type: string

Defines the name of the input.

order Type: string[]Default[]

Used by v-model:order: the column keys in render order. Columns it omits follow in declaration order; unknown keys stay put. A reorderable move writes it back complete. Works unbound, like sort; never holds select / actions.

page Type: string | number

Current page (from 1), for the rows' aria-rowindex and a BbPagination paired through id. Doesn't slice items: pass the current page's rows. Numeric strings are coerced.

perPage Type: string | number

Rows per page, for aria-rowindex and a paired BbPagination. Doesn't slice items. Unset, an array items infers it from its length. Numeric strings are coerced.

pt Type: PtItemMap<BbTablePtPart, "row" | "expand", BbTablePtState, Item>

Passthrough, object form: the same keys as the pt:* attributes without the prefix. See the pt:<part> row.

pt:<part> Type: PtValue | ((ctx: PtItemCtx<Item, BbTablePtState>) => PtItemResult)

Passthrough, attribute form: one attribute per part, pt:<part>, and per part and state, pt:<part>:<state>. Parts: body, caption, cell, cellColumnHeader, dragHandle, dragIcon, expand, firstRow, foot, head, headerRow, lastRow, root, row, selectIcon, sortIcon, table. States: disabled, empty, expanded, highlighted, indeterminate, loading, placeholder, selected, sorted, sortedDesc, value.

See example
readonly Type: booleanDefaultfalse

Sets the input in a readonly state.

reorderable Type: booleanDefaultfalse

Lets the user reorder the data columns: drag a header, or Shift+←/→ on a focused header. Every move writes v-model:order; a column panel that only writes order does not need it. select / actions and a custom #thead never move.

reorderableRows Type: booleanDefaultfalse

Adds a drag handle at the start of each row: the user drags a row, or lifts it with Space / Enter and the arrow keys, and reorder:rows reports { item, from, to }. The table never reorders items: splice it in the handler.

resizable Type: booleanDefaultfalse

Lets the user resize the data columns (drag a header's trailing edge, or Alt+←/→; double-click resets). Each commit fires resize:column(key, width) once: write the width back into the column definition to keep it. Ignored when inheriting widths.

rowClass Type: BbTableRowClasses<Item>

Classes for each data row, or a function of the row's item.

The column-level rowClass field is new in v3 and accepts a plain string, so a leftover per-column class of that name now type-checks and lands on the whole row. Check your column definitions when you upgrade.

See example
selectable Type: boolean | ((item: Item) => boolean)

Makes rows selectable: a boolean for every row, or a function of the row's item deciding for that row.

selectAll Type: booleanDefaultfalse

Whether the "Select all" checkbox is checked.

selectAllLabel Type: string

Label of the "Select all" checkbox.

Select-all is the "everything across every page" affordance: while it is on, modelValue stays empty and the exclusions accumulate in unselected-items. Read those two, never infer the selection from modelValue.

See example
selectText Type: string

Header text of the radio column when the table is not multiple.

sort Type: BbTableSortEntry[]Default[]

Used by v-model:sort: [columnKey, direction] entries in priority order, written by the sortable headers and shown by them. Doesn't sort items: apply it to your data or your server query.

tdClass Type: ColumnClasses<Item>

Classes for every data cell, or a function of (content, key, item).

thClass Type: Classes

Classes for every header cell.

totalItems Type: string | number

Total row count, for aria-rowcount and a paired BbPagination's page count. Unset, an array items infers it from its length. Numeric strings are coerced.

Pass it with page and per-page so the table can announce the whole result set: aria-rowcount (totalItems plus the header row, any #tfoot rows and the open expand rows) and each row's absolute aria-rowindex.

See example
totalPages Type: string | number

Seeds a paired BbPagination's page count when the server reports pages, not rows; the table renders nothing from it. A known total (totalItems) always wins. Numeric strings are coerced.

unselectedItems Type: any[]Default[]

Items that are not selected.

virtual Type: booleanDefaultfalse

Windows the body: only the rows near the scrollport are mounted. Bound the table's height to make it the scroller; column widths are measured once, from the first rows. Ignored when inheriting widths and under a #tbody slot.

Bound the height of the table or of an ancestor, or the page becomes the scroller. Columns are measured once from the first batch and then hold, and their cells clip with an ellipsis instead of wrapping — a column that must wrap needs a declared width.

See example

v-model

Values kept in sync through v-model.

NameDescription
expandedItems Type: any[]

Holds the open rows by item-value. Without item-value expansion is keyed by object identity and every refetch closes every row.

See example
highlighted Type: any

Bind ref(null), not ref(). The value is the switch: undefined leaves the whole highlight mechanic off (and warns in dev), null turns it on with nothing highlighted.

See example
modelValue Type: any

Holds row values resolved through item-value, or whole records without it. In multiple mode it must already be an array on the first render, or the table throws.

See example
order Type: string[]

Bind it without reorderable when a column panel owns the order; useBbTableContext(id).order reads and writes it from anywhere, and [] restores the declared order. Hide a column with its hidden flag, never by filtering the order array, and it returns exactly where it was. A column with reorderable: false stays at its declared slot whatever the array says.

selectAll Type: boolean
sort Type: BbTableSortEntry[]

The array order is the sort priority. The table reports it and never applies it: sort your own array, or forward it to the server as orderBy and refetch through dependencies.

See example
unselectedItems Type: any[]

Events

NameDescription
click:row Type: (event: MouseEvent, item: any): void

A body row was clicked, or activated with Enter/Space under keyboardNavigation. Carries the DOM MouseEvent and the raw record.

The payload is (event, item) — your raw record. v2 passed the table's internal row wrapper plus a trailing selected boolean; both are gone. Use it for side effects, not for navigation: a row that opens a record should contain a real link.

See example
contextmenu:row Type: (event: MouseEvent, item: any): void

A body row was right-clicked. Carries the DOM MouseEvent and the raw record.

dblclick:row Type: (event: MouseEvent, item: any): void

A body row was double-clicked. Carries the DOM MouseEvent and the raw record.

hide:column Type: (key: string): void

A column was hidden through a header slot's hideColumn(). Fired once; write hidden: true into the column definition to persist it.

item:selected Type: (value: any): void

The user selected one row. Carries the row's value (read through itemValue, the same value the selection v-model holds).

item:unselected Type: (value: any): void

The user unselected one row. Carries the row's value, like item:selected.

reorder:rows Type: (payload: { item: any; from: number; to: number; }): void

A row was moved (reorderable-rows): item is your object, from / to index the items the table holds. Splice items in the handler; a drop in place emits nothing.

resize:column Type: (key: string, width: number | null): void

The user resized a column (resizable): the new width in px, or null after a double-click on the handle reset it to its declared width.

Fires once per committed resize, with null after a double-click reset. There is no width model on purpose: write the reported width back into your column definition, which always outranks a user override.

update:expandedItems Type: (value: any[]): void

Emitted with the new value whenever v-model:expandedItems changes.

update:highlighted Type: (value: any): void

Emitted with the new value whenever v-model:highlighted changes.

update:modelValue Type: (value: any): void

Emitted with the new value whenever v-model changes.

update:order Type: (value: string[]): void

Emitted with the new value whenever v-model:order changes.

update:selectAll Type: (value: boolean): void

Emitted with the new value whenever v-model:selectAll changes.

update:sort Type: (value: BbTableSortEntry[]): void

Emitted with the new value whenever v-model:sort changes.

update:unselectedItems Type: (value: any[]): void

Emitted with the new value whenever v-model:unselectedItems changes.

Slots

The listed properties are the ones exposed to the slot scope.

NameDescription
<key> Type: object

One column's body cell in every row, by the column key through slotKey. Scope: { item, value, content, selected, expanded, highlighted, classes, sortOrder, toggleSelected, toggleExpanded, toggleHighlighted, expandProps }.

The name is the column key in kebab-case: unitPrice and unit_price are both #unit-price, address.city is #address-city. For accents or punctuation write #[slotKey('…')], and build names in code with slotKey, never a hand copy of the rule. Scope value is the row's value, not the cell's. Keep the column in columns: it still supplies the label, alignment and width.

See example
<key>:first Type: BbTableEdgeCellSlotProps<Item>

One column's cell in a row before the data rows; any such slot creates the row, aligned with the columns. Only over rows.

<key>:header Type: object

One column's header content, replacing its label and the built-in sort button. Scope: { label, items, sortable, sortOrder, toggleSort, hideColumn, classes }.

Replaces the header and the built-in sort button with it. To add beside the label and keep sorting, use <key>:header:prepend / :append.

See example
<key>:header:append Type: BbTableHeaderAffixSlotProps<Item>

Content after one column's header, keeping the sort button; beats header:append (they never stack).

See example
<key>:header:prepend Type: BbTableHeaderAffixSlotProps<Item>

Content before one column's header, keeping the sort button; beats header:prepend (they never stack).

See example
<key>:last Type: BbTableEdgeCellSlotProps<Item>

One column's cell in a row after the data rows (a totals row); any such slot creates that row, aligned like <key>:first.

actions Type: { expanded?: boolean | undefined; expandProps?: ExpandControllerProps | undefined; toggleExpanded?: (() => void) | undefined; highlighted?: boolean | undefined; toggleHighlighted?: (() => void) | undefined; selected?: boolean | undefined; toggleSelected?: (() => void) | undefined; item: Item; value: any; }

Content of each row's actions cell; this slot CREATES the actions column (there is no prop). The header cell (labelled by actions-text, replaceable through #actions:header) and one cell per row come and go with it. Pin it with fixed-actions.

actions:header Type: { text: string; }

Replaces the default actions header cell content.

empty Type: { error: unknown; }

Content shown when there are no rows and nothing is loading, inside the table's own full-width cell: pass content, never a row. error is what the latest failed load threw (null when none).

Fills the table's own full-width cell, already centred and already spanning every column. Pass content, not a row — v2 handed this slot the whole <tr>, and a hand-rolled row here is now table markup inside a grid.

See example
expand Type: { expanded: boolean; expandProps?: ExpandControllerProps | undefined; toggleExpanded: () => void; highlighted?: boolean | undefined; toggleHighlighted?: (() => void) | undefined; selected?: boolean | undefined; toggleSelected?: (() => void) | undefined; item: Item; value: any; }

Full-width expandable content row rendered below each main row when expanded.

One full-width row below its record. expandProps, expanded and toggleExpanded also reach every per-column cell slot, so the toggle can live in the identity column instead of the actions column.

See example
header:append Type: BbTableHeaderAffixSlotProps<Item>

Content after every data column's header content (the label + sort control, or its own <key>:header replacement). A column's <key>:header:append wins over this for that column; they never stack. Branch on columnKey for per-column output.

Renders after the column's existing header content, so the built-in sort button survives — which a full #<key>:header replacement does not. The table-wide form applies to every data column and never to the select or actions cells; a per-column affix wins over it and the two never stack.

See example
header:prepend Type: BbTableHeaderAffixSlotProps<Item>

Content before every data column's header content (the label + sort control, or its own <key>:header replacement). A column's <key>:header:prepend wins over this for that column; they never stack. Branch on columnKey for per-column output.

The mirror of header:append, rendered before the column's existing header content. A column key that normalizes to prepend or append shadows these table-wide names: the table-wide reading wins, and dev builds warn.

See example
loading Type: { items: Item[]; }

Content shown while table rows are loading (replaces the skeleton rows).

Replaces the skeleton, so it follows the skeleton's rule: first load only. A refetch over existing rows keeps them and shows the built-in progress bar instead, which this slot cannot change.

See example
select Type: { item: Item; value: any; checked: boolean; disabled: boolean; inputName: string; readonly: boolean; toggleSelected?: (() => void) | undefined; }

Replaces the default checkbox/radio in the selection cell for each row.

Replaces a row's checkbox or radio while keeping the table's selection path. This is the supported way to change the control — rebuilding selection with your own checkbox in a cell slot is not.

See example
select:header Type: { multiple: boolean; selectAll: boolean; text: string; }

Replaces the default selection header cell content (checkbox or radio label).

tbody Type: object

Replaces the content of the body row group (.bb-table__body), for full control of the body markup. Provide role-complete rows (<div role="row"> with one <div role="cell"> per column), never <tr>/<td>. A full-width cell spans with grid-column: 1 / -1.

Role-complete <div role="row"> rows of <div role="cell">. Taking over the body switches off virtual, which has no rows of its own to window.

See example
tfoot Type: { columnCount: number; }

Fills the footer row group (.bb-table__foot) for footers that are not column-shaped (a note, pagination). Column-aligned totals go in #<key>:last (#<key>:first for a row before the data). Role-complete rows (<div role="row"> of <div role="cell">), never <tr>/<td>.

The markup you pass is grid markup: <div role="row"> holding <div role="cell">, spanning with grid-column and aria-colspan rather than colspan. The table numbers these rows after the body (aria-rowindex) and counts them in aria-rowcount, so leave the index off. Nothing lints the roles — a row group of plain <div>s renders correctly and is invisible to a screen reader.

See example
thead Type: object

Replaces the header row group's content (.bb-table__head). The table is a CSS grid: provide <div role="row"> rows of <div role="columnheader"> cells, never <tr> / <th>.

Role-complete rows of <div role="columnheader">. The table measures its tracks off the header cells, so a custom header over an empty table publishes no tracks and a nested table cannot inherit from it until rows arrive.

See example

Renamed and removed

Names from v2 and from earlier 3.0 betas that no longer work, and what to write instead.

  • actionsremovedfails silently
  • allowSelectAlldisableSelectAllfails silently
  • enabledWhileLoadinginteractiveWhileLoadingfails silently
  • fixedColumnsremovedfails silently
  • slotheader:<x><x>:headerfails silently
  • slotheader:<x>:append<x>:header:appendfails silently
  • slotheader:<x>:prepend<x>:header:prependfails silently
  • slotheader:actionsactions:headerfails silently
  • slotheader:selectselect:headerfails silently
  • slotno-dataemptyfails silently
  • noDataTextemptyTextfails silently