Skip to content

Displays tabular data, configurable columns and data mapper.

import { BbTable } from 'bitboss-ui';

On this page

Props

NameTypeDefaultDescription
accessibleLabel((columns: MappedCell[], item: any) => string) | undefined-

Function that accepts the columns and the current item as arguments and returns a label to be used for accessibility purposes. The cells arrive in RENDER order (they follow order, so the label reads like the row) — look a cell 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
actionsTextstring | undefined-

Label used in the header of the actions column.

align"left" | "center" | "right" | undefined"left"

Text alignment of the columns.

captionstring | undefined-

Caption that describes the content of the table. Used for accessibility purposes.

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
columnsBbTableColumn<T>[] | undefined[]

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) and styling (thClass, tdClass, rowClass). actions and select are not columns: the #actions slot creates the actions column and selectable the selection one, so a column with either key only adds a stray data cell. Pin a column with fixed: 'left' | 'right' on its definition.

See example
compactboolean | undefinedfalse

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

dependenciesunknown[] | undefined[]

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

depsDebounceTimenumber | undefined0

Timeout used to debounce response to changes to dependencies.

disabledboolean | undefinedfalse

Disables the component

disableSelectAllboolean | undefinedfalse

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
displayCaptionboolean | undefinedfalse

Boolean that defines whether to display or hide the caption. By default is true and the caption is hidden.

enforceCoherenceboolean | undefinedfalse

After every load (initial, dependencies refetch or a change of the items array) prunes the row-keyed models of values whose row is no longer in the result set: modelValue, unselected-items, highlighted and expanded-items. Values whose rows are still present survive. Single-value models (modelValue without multiple, and highlighted, which is always single) reset to null. Do **not** enable it on paginated tables where the page feeds dependencies: every page turn is a refetch, so state held on rows of other pages (the select-all + unselected-items pattern, cross-page selections) would be pruned. Reconcile in the items provider instead.

Leave it off on any table whose page turn goes through dependencies — every page is a refetch, 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
expandedItemsany[] | undefined[]

Used by v-model:expandedItems. Array of the currently expanded items — toggled by the row expand control and by external writes (mirrors the highlighted/sort v-model surfaces). Drives the #expand slot rows and their aria-expanded/aria-controls wiring.

fixedboolean | undefinedfalse

Splits the width equally among the columns that declare no width (each undeclared column becomes a minmax(0, 1fr) track) making each column take up the same amount of space.

fixedActionsboolean | undefinedfalse

Pins the actions column (the one the #actions slot creates) to the right edge, position: sticky, in place. Data columns pin through column.fixed; this is the same feature for the structural column that has no definition to carry it. No effect without an #actions slot.

fixedHeadersboolean | undefinedfalse

Boolean that sets the headers as sticky to the top of the table.

fixedSelectboolean | undefinedfalse

Pins the selection column to the left edge, position: sticky, in place. Data columns pin through column.fixed; this is the same feature for the structural column that has no definition to carry it. 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
headerRowClassClasses | undefined-

Defines the classes to be passed to the header row.

highlightedany-

Used by v-model:highlighted. A parallel, UI-intent state to selection — typically "the row whose details are open". A single item value (or null) — highlight is single by design. **Opt-in, and undefined is the opt-out.** Leave it unbound and the whole mechanic is off: clicking a row emits click:row and nothing else — no class, no aria-current, no internal state. Bind it and clicking a row toggles it. The value itself is the switch: undefined means "never initialised", null means "initialised and currently empty", so bind ref(null) rather than ref() — a bound-but-undefined model warns in dev and stays off. A value seeded through useBbTableContext(id, { highlighted }) counts as bound the same way. A highlighted row gets aria-current="true" and the bb-table-data__row--highlighted class, which the shipped stylesheet paints with a neutral fill plus a brand accent bar; override the whole look with rowClass. Highlight: a parallel, UI-intent state to selection ("the row whose details are open"). Always a single value or null. Purely mechanical — highlighted rows only get a class and aria-current, no shipped styling.

idstring | undefined-

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
inheritColumnWidthsstring | boolean | undefined-

Opt a nested table into inheriting its column widths from an ancestor table's matching tracks (accounting for select/actions columns). This also makes the parent's tracks authoritative, so it is **off by default** — a nested table renders independently unless you ask for alignment. - false (the default): do not inherit; size independently. - true: inherit from the nearest ancestor table. - a table id (string): inherit from the specific ancestor in the chain with that id rather than the immediate parent. Use this when an intermediate table has a different column count, so the extra columns would otherwise misalign against it.

interactiveWhileLoadingboolean | undefinedfalse

Keeps the table interactive while it is loading. The header and the rows are inert while loading by default — a refetch is about to replace the rows, so editing, selecting or sorting them races the incoming data. This escape hatch exists for tables that must stay usable during background refreshes (e.g. a poll that must not interrupt an open editor). The loading announcement is never inert, so the busy state is always conveyed.

itemsrequiredItem[] | ((prefill: boolean, modelValue?: any) => Item[] | Promise<Item[]>)[]

Rows to render, or a fetcher that returns them.

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

See example
itemValueItemAccessor<Item> | undefined-

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

See example
keyboardNavigationboolean | undefinedfalse

Opt-in keyboard navigation with a roving tabindex: the body is one tab stop, ArrowUp/ArrowDown move the focused row (Home/End jump), and Enter/Space activate it through the same path as a mouse click (highlight toggle + click:row). Focus and highlight stay independent — moving does not highlight until committed. Focused rows show the standard focus ring.

Screen readers in browse mode intercept the arrow keys, so a reader takes the usual focus-mode step before the roving tabindex responds. Pair it with a bound highlighted (or a row-class) so the cursor is visible as well as announced.

See example
legendstring | undefined-

Text content of the <legend> a selectable table renders (it becomes a <fieldset>, and the legend is that fieldset's accessible name). Omit it and the table falls back to caption, then to a localized default — the fieldset always ends up with SOME accessible name — but a specific legend describes the selection better than either fallback.

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
loadingboolean | undefinedfalse

Sets the component in a loading state, usually triggering some visual styles. With no rows yet this renders skeletons; over existing rows the rows stay on screen (dimmed, with an indeterminate bar) rather than being replaced — a refetch should not blank 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
loadingTextstring | undefinedthe 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.

maxnumber | undefinedInfinity

Maximum number of items that can be selected.

modelValueany[]

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

multipleboolean | undefinedtrue

Allows the selection of multiple items.

namestring | undefined-

Defines the name of the input.

noDataTextstring | undefinedthe localized `common.noDataText` ("No data to display")

String displayed when there are no items to display.

Also the place to explain a failed fetch: there is no error-text prop and no table-level error state, so a rejected provider is yours to surface — through this text or the #no-data slot.

See example
orderstring[] | undefined[]

Used by v-model:order. Ordered array of column keys — the render order of the data columns. Keys listed here render first, in this order; declared columns it omits follow, in declaration order (so a column added to columns later renders last); a hidden column keeps its slot without rendering, and keys that match no declared column render nothing but are PRESERVED in place on every write. The header drag / keyboard reorder of reorderable writes it back complete (every declared key present, hidden ones included, duplicates dropped — first occurrence wins) with exactly the moved column relocated: every other key keeps its relative order, and a drop that changes nothing emits nothing. Works uncontrolled when unbound, exactly like sort. The select and actions columns are structural and never part of it.

pagestring | number | undefined-

Current page number starting from 1, used for accessibility purposes. Also flows into the shared table context, so a BbPagination paired through id reads it as the current page. Numeric strings are coerced (server pagination fields often arrive as strings), exactly like BbPagination's equivalent props.

perPagestring | number | undefined-

Number of items per page, used for accessibility purposes. Also flows into the shared table context read by a BbPagination paired through id. When omitted, an array items table infers it from items.length; an explicit value (or a context write) always wins over that inference. Numeric strings are coerced, exactly like BbPagination's per-page.

ptPtItemMap<BbTablePtPart, "row", BbTablePtState, Item> | undefined-

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

pt:bodyPtValue | undefined-
pt:body:highlightedPtValue | undefined-
pt:body:loadingPtValue | undefined-
pt:body:selectedPtValue | undefined-
pt:cellPtValue | undefined-
pt:cell:highlightedPtValue | undefined-
pt:cell:loadingPtValue | undefined-
pt:cell:selectedPtValue | undefined-
pt:headPtValue | undefined-
pt:head:highlightedPtValue | undefined-
pt:head:loadingPtValue | undefined-
pt:head:selectedPtValue | undefined-
pt:rootPtValue | undefined-
pt:root:highlightedPtValue | undefined-
pt:root:loadingPtValue | undefined-
pt:root:selectedPtValue | undefined-
pt:rowPtValue | ((ctx: PtItemCtx<Item, BbTablePtState>) => PtItemResult) | undefined-
pt:row:highlightedPtValue | undefined-
pt:row:loadingPtValue | undefined-
pt:row:selectedPtValue | undefined-
pt:tablePtValue | undefined-
pt:table:highlightedPtValue | undefined-
pt:table:loadingPtValue | undefined-
pt:table:selectedPtValue | undefined-
readonlyboolean | undefinedfalse

Sets the input in a readonly state.

reorderableboolean | undefinedfalse

Lets the user reorder the data columns: drag a header cell (AG Grid style — the column moves live under the pointer and a chip with its label follows) or, from the keyboard, use each header's reorder handle (ArrowLeft / ArrowRight move the column one slot, Home / End to the ends). Every move writes v-model:order. Not needed by an external control (a column panel) that only writes order. The select / actions columns and a custom #thead are never draggable.

resizableboolean | undefinedfalse

Lets the user resize the data columns of a root table: drag the handle on a header's trailing edge (the column follows live, the width commits on release), or press Alt + ArrowLeft / ArrowRight on the focused header; double-click the handle to go back to the declared width. Each commit fires resize:column(key, width) once. column.width stays the single declared truth: a resized width is an override the table keeps until that column's width changes — write the reported width back into your column definition to persist it. Ignored on a table that inherits its widths from a parent.

rowClassBbTableRowClasses<Item> | undefined-

Defines the classes to be passed to each data row. Can also be a function of the row's item for dynamic values.

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
selectableboolean | ((item: Item) => boolean) | undefined-

Defines whether the table is selectable. Can be a global boolean that affects all rows or a function that accepts an item and returns a boolean that only affects that item.

selectAllboolean | undefinedfalse

Defines whether the "Select all" checkbox is checked.

selectAllLabelstring | undefined-

Text of the label used by the "Select all" boolean.

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
selectTextstring | undefined-

Text of the header used for the radio inputs when the table is not multiple.

sortBbTableSortEntry[] | undefined[]

Used by v-model:sort. Ordered array of [columnKey, direction] entries, one per actively sorted column. Bidirectional: the header buttons of sortable columns update it, and external writes update the header indicators. The table does not sort its items — apply the model to your own data source.

tdClassColumnClasses<Item> | undefined-

Defines the classes to be passed to every data cell. Can also be a function for dynamic values.

thClassClasses | undefined-

Defines the classes to be passed to every header cell.

totalItemsstring | number | undefined-

Total number of items there is. Also flows into the shared table context, where a BbPagination paired through id derives its page count from it. When omitted, an array items table infers it from items.length; an explicit value (or a context write) always wins over that inference. Numeric strings are coerced, exactly like BbPagination's total-items.

Pass it with page and per-page so the table can announce the whole result set: aria-rowcount (now totalItems + 1, the header row counts) and each row's absolute aria-rowindex.

See example
totalPagesstring | number | undefined-

Seeds the page count shown by a BbPagination paired through id. A shortcut for when the server reports a page count rather than a row count: the table renders nothing from it, it only flows into the shared table context. A known total always wins — when totalItems (or a context write) provides one, the page count is derived from it and this seed is ignored. Numeric strings are coerced, exactly like BbPagination's total-pages.

unselectedItemsany[] | undefined[]

Items that are not selected.

virtualboolean | undefinedfalse

Windows the body: only the rows in (and just around) the vertical scrollport are mounted, so a client-side dataset of any size scrolls at a constant cost. The table itself is the scroller as soon as you bound its height (max-height / height, or a flex item with min-height: 0); left unbounded, the nearest scrolling ancestor or the page scrolls it. The header sticks to that scroller. Column widths are measured once from the first rendered batch and then hold — later rows never widen them. Expand rows and nested tables stay fully rendered inside their row (a nested table is never windowed); the row that owns focus stays mounted even when scrolled out of view. Ignored by a table that inherits its widths from a parent, 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.

NameTypeDescription
expandedItemsany[] | undefined

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

See example
highlightedany

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
modelValueany

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
orderstring[] | undefined

Bind it without reorderable when a column panel owns the order. Keys matching no declared column render nothing but keep their position, so hide a column by filtering columns — never the order array — and it returns exactly where it was.

selectAllboolean | undefined
sortBbTableSortEntry[] | undefined

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
unselectedItemsany[] | undefined

Events

NameTypeDescription
click:row(e: "click:row", event: MouseEvent, item: any): void

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(e: "contextmenu:row", event: MouseEvent, item: any): void
dblclick:row(e: "dblclick:row", event: MouseEvent, item: any): void
hide:column(e: "hide:column", key: string): void

A column was hidden from its header (a #header:<key> / #header:<key>:append control calling the slot's hideColumn()). Fired once; the table keeps the column hidden for the mount, or until the column's hidden flag changes — write hidden: true into the definition to persist it, hidden: false (a column panel) to show it again.

item:selected(e: "item:selected", value: any): void
item:unselected(e: "item:unselected", value: any): void
resize:column(e: "resize:column", key: string, width: number | null): void

A column was resized by the user (resizable): the new width in px, or null when the handle was double-clicked to reset the column 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(e: "update:expandedItems", value: any[]): void
update:highlighted(e: "update:highlighted", value: any): void
update:modelValue(e: "update:modelValue", value: any): void
update:order(e: "update:order", value: string[]): void
update:selectAll(e: "update:selectAll", value: boolean): void
update:sort(e: "update:sort", value: BbTableSortEntry[]): void
update:unselectedItems(e: "update:unselectedItems", value: any[]): void

Slots

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

NameTypeDescription
<key>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 normalized — lowercased, every run of non-word characters folded to _ — so unitPrice is #unitprice. Build names in code with slotKey, never a hand copy of the rule. Keep the column in columns: it still supplies the label, alignment and width.

See example
actions{ 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 rendered in the actions cell for each row. Providing this slot is what CREATES the actions column (there is no prop): the header cell (labelled by actions-text, replaceable through #header:actions) and one cell per row appear with it, and go away with it. Pin it with fixed-actions.

expand{ 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:<key>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 header:<key>:prepend / :append.

See example
header:<key>:appendBbTableHeaderAffixSlotProps<Item>

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

See example
header:<key>:prependBbTableHeaderAffixSlotProps<Item>

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

See example
header:actions{ text: string; }

Replaces the default actions header cell content.

header:appendBbTableHeaderAffixSlotProps<Item>

Content rendered after every data column's header cell content — the label + sort control, or the column's own header:<key> replacement. A column's header:<key>:append slot wins over this one 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 #header:<key> 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:prependBbTableHeaderAffixSlotProps<Item>

Content rendered before every data column's header cell content — the label + sort control, or the column's own header:<key> replacement. A column's header:<key>:prepend slot wins over this one 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
header:select{ multiple: boolean; selectAll: boolean; text: string; }

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

loading{ 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
no-dataobject

Content shown when the items list is empty and not loading. It fills the table's own full-width empty-state cell (it already spans the selection/actions columns), so provide the *content* — a message, an illustration, a "clear filters" button — never a row/cell of your own.

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
select{ 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
tbodyobject

Replaces the content of the body row group (.bb-table__body). Use when you need full control over the body markup. Provide role-complete rows — <div role="row"> holding one <div role="cell"> per column — never a <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{ columnCount: number; }

Fills the footer row group (.bb-table__foot). Use when you need a footer. Provide role-complete rows — <div role="row"> holding <div role="cell"> elements — never a <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. Nothing lints the roles — a row group of plain <div>s renders correctly and is invisible to a screen reader.

See example
theadobject

Replaces the content of the header row group (.bb-table__head). Use when you need full control over the header markup. The table is a CSS grid, not a <table>: provide role-complete rows — <div role="row"> holding one <div role="columnheader"> per column — and never a <tr>/<th>. Rows are subgrid rows, so each cell lands on the matching column track.

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

Changes from v2

Props removed or renamed in 3.0. If you are coming from v2, the answer is here.

  • actionsremovedfails silently
  • allowSelectAlldisableSelectAllfails silently
  • enabledWhileLoadinginteractiveWhileLoadingfails silently
  • fixedColumnsremovedfails silently