Skip to content

Table

Compound table — <VCTable> outer + eight semantic-HTML parts (Header / Body / Footer / Row / Cell / HeadCell / Empty / Loading). A :columns :data driver covers the common entity-list shape; manual compound markup is an escape hatch for custom layouts. Single-column controlled sort via v-model:sort; opt-in row-click affordance with full keyboard navigation. Row-meta variants (_rowVariant / _cellVariants) tint rows + cells declaratively from the data.

bash
npm install @vuecs/table
vue
<script setup lang="ts">
import { ref } from 'vue';
import {
    VCTable,
    VCTableBody,
    VCTableCell,
    VCTableEmpty,
    VCTableHeadCell,
    VCTableHeader,
    VCTableRow,
} from '@vuecs/table';
import type { TableColumn, TableSortState } from '@vuecs/table';

type User = { id: number; name: string; email: string; role: string };

const sort = ref<TableSortState>([]);
const columns: TableColumn<User>[] = [
    { key: 'name', label: 'Name', sortable: true, isRowHeader: true },
    { key: 'email', label: 'Email', sortable: true },
    { key: 'role', label: 'Role' },
];
const data: User[] = [
    { id: 1, name: 'Alice', email: 'alice@example.com', role: 'admin' },
    { id: 2, name: 'Bob', email: 'bob@example.com', role: 'editor' },
];
</script>

<template>
    <VCTable v-model:sort="sort" :columns :data>
        <VCTableHeader>
            <VCTableRow>
                <VCTableHeadCell
                    v-for="col in columns" :key="col.key"
                    :column-key="col.key" :sortable="col.sortable"
                >{{ col.label }}</VCTableHeadCell>
            </VCTableRow>
        </VCTableHeader>
        <VCTableBody>
            <template #row="{ row, index }">
                <VCTableRow :row :index>
                    <VCTableCell
                        v-for="col in columns" :key="col.key"
                        :column-key="col.key" :is-row-header="col.isRowHeader"
                    >{{ row[col.key as keyof User] }}</VCTableCell>
                </VCTableRow>
            </template>
        </VCTableBody>
        <VCTableEmpty>No users yet.</VCTableEmpty>
    </VCTable>
</template>
css
@import "tailwindcss";
@import "@vuecs/design";
@import "@vuecs/table";

@custom-variant dark (&:where(.dark, .dark *));

<VCTable> props

PropTypeDefaultDescription
dataRow[][]Row payload.
columnsTableColumnRaw<Row>[]undefinedColumn definitions (object form or bare-string shorthand). When omitted, columns are derived from Object.keys(data[0]).
busybooleanfalseSets aria-busy="true" on the <table> and gates the loading-band render.
sortSortDescriptor[][]Controlled sort state. Use v-model:sort. Empty array means "no sort"; single-column sort is an array of length 1.
mustSortbooleanfalseWhen true, the cycle skips the empty step ([] → asc → desc → asc).
scrollablebooleanfalseWrap the <table> in an overflow scroll container.
stickyHeaderbooleanfalseStick the <thead> to the top of the scroll container. Requires :scrollable.
maxHeightstringundefinedCSS length applied to the scroll container's max-height.
rowClickablebooleanfalseOpt-in: each row becomes focusable + clickable. Emits @row-click; row keyboard nav activates.
expandablebooleanfalseEnable per-row expansion panels (see Expandable rows).
expandedRowSelectionKey | RowSelectionKey[] | null[]Controlled expansion state. Use v-model:expanded. Multi-mode carries an array; single-mode carries a bare key or null for "no row open".
expansionMode'single' | 'multi''multi'Accordion (single) vs unrestricted (multi) expansion.
expandableTrigger'leading' | 'trailing' | 'none''leading'Where to place the auto-injected trigger column. 'none' opts out (consumer places <VCTableExpandTrigger> inside a data cell).
density'compact' | 'normal' | 'spacious''normal'Theme variant shorthand.
striped / bordered / hoverbooleanundefinedTheme variant shorthands.

Column shape

ts
interface TableColumn<Row, K extends string = string> {
    key: K;
    label?: string;          // defaults to startCase(key)
    class?: VNodeClass;      // applied to <th> AND <td>
    headerClass?: VNodeClass;
    cellClass?: VNodeClass;
    sortable?: boolean;
    accessor?: string | ((row: Row) => unknown); // string supports dot-paths
    formatter?: (ctx: { value, key, row }) => string;
    isRowHeader?: boolean;   // <th scope="row"> instead of <td>
    cellAttrs?: Record<string, unknown> | ((ctx) => Record<string, unknown>);
    headerAttrs?: Record<string, unknown> | ((ctx) => Record<string, unknown>);
    headerTitle?: string;
    headerAbbr?: string;
    stickyColumn?: boolean;
    initialSortDirection?: 'asc' | 'desc';
}

A bare-string shorthand normalises to { key, label: startCase(key) }:

ts
columns: ['id', 'name', 'email']
// ≡ [{ key: 'id', label: 'Id' }, { key: 'name', label: 'Name' }, ...]

Default cell rendering

<VCTableCell columnKey="..."> with no slot content auto-renders the value via the column's accessor + formatter. Slot content always wins, so passing children opts out of the default render:

vue
<!-- Auto-rendered: row[col.key] / accessor / formatter -->
<VCTableCell :column-key="col.key" />

<!-- Slot wins: consumer renders manually -->
<VCTableCell :column-key="col.key">
    {{ row[col.key as keyof User] }}
</VCTableCell>

<!-- With accessor dot-path + formatter -->
const columns: TableColumn<User>[] = [
    { key: 'email', accessor: 'profile.email' },
    { key: 'price', formatter: ({ value }) => `$${value}` },
];

null / undefined resolve to empty strings. Mounting <VCTableCell> outside a <VCTable> (or with a columnKey that isn't in the columns array) renders an empty cell.

Driver auto-render

<VCTable> auto-renders the missing band(s) when :columns resolves to a non-empty list and the consumer's default slot omits them. This makes the terse form viable:

vue
<!-- Slotless: auto-header + auto-body -->
<VCTable :columns :data />

<!-- Consumer header, auto body -->
<VCTable :columns :data>
    <VCTableHeader>
        <VCTableRow>
            <VCTableHeadCell v-for="col in columns" :key="col.key">
                {{ col.label }} <span v-if="col.sortable">⇅</span>
            </VCTableHeadCell>
        </VCTableRow>
    </VCTableHeader>
</VCTable>

<!-- Auto header + body, plus Empty band -->
<VCTable :columns :data>
    <VCTableEmpty>No users yet.</VCTableEmpty>
</VCTable>

<!-- Even terser: omit :columns and let the table derive from data[0] -->
<VCTable :data />

The walker recurses into Fragments, so <template v-if> / <template v-for> around a manual <VCTableHeader> / <VCTableBody> still suppresses the auto-render correctly.

Auto-cells use the default cell renderer (accessor / formatter) documented above. For per-column custom rendering inside the auto-render path, pass a #cell-<key> slot — its render result replaces the default cell content for that column. A matching #header-<key> slot replaces the column's header text.

vue
<VCTable :columns :data>
    <template #cell-options="{ row }">
        <button @click="edit(row)">Edit</button>
        <button @click="remove(row)">Delete</button>
    </template>
    <template #header-options="{ column }">
        <span class="text-muted">{{ column.label }}</span>
    </template>
</VCTable>

Slot props match the exported TableCellSlotProps ({ row, value, key, column, index }) and TableHeadCellSlotProps ({ column, key, sort, setSort }) types. The cell-slot value honors the column's accessor (so dot-paths and accessor functions work transparently); the slot wins over the column's formatter if both are set.

<VCTable> (and <VCTableLite>) also accept #caption and #colgroup slots — their content renders inside a <caption> / <colgroup> element at the top of the <table>, before the header band.

Typed rows & slots

<VCTable> and <VCTableLite> are generic over their row type. Row is inferred from :data (or :columns) at the call site and flows into every row-bearing slot prop — no explicit annotation, no as cast:

vue
<script setup lang="ts">
import { VCTable } from '@vuecs/table';
import type { TableColumn } from '@vuecs/table';

interface User { id: number; name: string; email: string }
const users: User[] = [/* … */];
const columns: TableColumn<User>[] = [{ key: 'name' }, { key: 'email' }];
</script>

<template>
    <VCTable :data="users" :columns="columns">
        <template #cell-name="{ row }">
            <!-- `row` is `User` — `row.email` autocompletes + type-checks -->
            <a :href="`mailto:${row.email}`">{{ row.name }}</a>
        </template>
        <template #expansion="{ row }">
            {{ row.email }}
        </template>
    </VCTable>
</template>

The same inference reaches #default ({ data }, i.e. data: Row[]) and #header-<key> ({ column }, where column is TableColumn<User>) on both components. <VCTable> additionally types its #expansion slot ({ row }) and the @row-click / getRowKey callbacks — <VCTableLite> exposes none of those. When neither :data nor :columns is typed, Row falls back to Record<string, unknown>, so untyped call sites keep working.

The generic is surfaced by re-typing the component's public surface — the runtime stays a plain defineComponent render-function component (no <script setup generic>). The slot-prop types (TableSlotProps<Row>, TableCellSlotProps<Row>, TableHeadCellSlotProps<Row>, TableExpansionSlotProps<Row>) are exported, so render-function consumers can reference them directly.

TIP

The manual-compound parts (<VCTableRow>, <VCTableCell>, <VCTableHeadCell>) are not generic — when you hand-write the compound markup instead of using the :columns / #cell-<key> driver, the row inside <VCTableBody>'s #row slot is untyped (unknown) and you cast it yourself. The generic typing covers the driver path.

Row selection

<VCTable :selection-mode> enables row selection with the W3C ARIA grid pattern. When set, the table renders as role="grid" with aria-selected on each row and roving tabindex for keyboard navigation.

vue
<script setup lang="ts">
import { ref } from 'vue';
import { VCTable } from '@vuecs/table';
import type { TableColumn } from '@vuecs/table';

type User = { id: number; name: string; email: string };

const selection = ref<number[]>([]);
const columns: TableColumn<User>[] = [
    { key: 'name' },
    { key: 'email' },
];
const data: User[] = [/* ... */];
</script>

<template>
    <VCTable
        v-model:selection="selection"
        selection-mode="multi"
        :columns
        :data
    />
</template>
PropTypeDefaultDescription
selectionMode'single' | 'multi'undefinedEnables the grid pattern. undefined keeps the plain-table semantics.
selectionRowSelectionKey | RowSelectionKey[] | nullnullControlled selection state. Use v-model:selection.
getRowKey(row, index) => RowSelectionKeyundefinedResolve the selection key per row. When unset, falls back to row.id ?? index.

Click semantics in multi mode:

  • Plain click toggles the row.
  • Shift + click extends the range from the anchor.
  • Ctrl / Cmd + click toggles one row without affecting the rest.

Keyboard semantics:

  • / move focus row-by-row.
  • Home / End jump to first / last.
  • Space / Enter toggle the focused row.
  • Shift + ↓ / Shift + ↑ extend the range while moving focus.

<VCTableLite> doesn't support selection — Lite consumers bring their own state plumbing.

Selection column (isSelector)

Build a Gmail-style selection column by adding isSelector to both the head cell + each body cell. The head cell renders an indeterminate-capable checkbox (some / all / none states) that toggles select-all vs. clear-all; each body cell renders a per-row checkbox (or <input type="radio"> in single mode).

vue
<VCTable :selection-mode="'multi'" v-model:selection="selection" :columns :data>
    <VCTableHeader>
        <VCTableRow>
            <VCTableHeadCell is-selector />
            <VCTableHeadCell v-for="col in columns" :key="col.key" :column-key="col.key" />
        </VCTableRow>
    </VCTableHeader>
    <VCTableBody>
        <template #row="{ row, index }">
            <VCTableRow :row :index>
                <VCTableCell is-selector />
                <VCTableCell v-for="col in columns" :key="col.key" :column-key="col.key" />
            </VCTableRow>
        </template>
    </VCTableBody>
</VCTable>

The select-all click sets the selection to every visible row's key (via getRowKey); a second click clears the array. When the selection is single, the head cell renders empty and the per-row input becomes a radio. With selection disabled entirely, both fall back to the default slot so consumers can keep the column in place without losing layout. aria-label defaults to 'Select all rows' / 'Select row' — override via :selector-aria-label for i18n.

Expandable rows

Per-row expansion panels for inline entity detail — log payloads, audit trails, comment threads, debug data — without a separate route. Each expanded row mounts as a Vue Fragment of two <tr>: the data row + a sibling <tr><td colspan="N"> containing the panel content.

Set :expandable on <VCTable> and provide an #expansion scoped slot. The driver auto-injects a leading trigger column with a chevron button; click to toggle.

vue
<script setup lang="ts">
import { ref } from 'vue';
import { VCTable } from '@vuecs/table';

const expanded = ref<(string | number)[]>([]);
const users = [
    { id: 1, name: 'Alice', email: 'alice@example.com' },
    { id: 2, name: 'Bob', email: 'bob@example.com' },
];
</script>

<template>
  <VCTable
    v-model:expanded="expanded"
    :columns="[{ key: 'name' }, { key: 'email' }]"
    :data="users"
    expandable>
    <template #expansion="{ row }">
      <p><strong>{{ row.name }}</strong> &lt;{{ row.email }}&gt;</p>
      <p>Last seen: just now</p>
    </template>
  </VCTable>
</template>

Single (accordion) vs multi

:expansion-mode="'multi'" (default) lets any number of rows open simultaneously. :expansion-mode="'single'" collapses the previously- open row when a new one opens — the accordion pattern.

Initial state via _expanded

Mirrors the _rowVariant row-meta pattern. Mark a row open by default with _expanded: true on the row payload:

ts
const users = [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob', _expanded: true }, // starts open
];

The seed runs ONCE on the first non-empty data pass — so the canonical async pattern (data: [] then data: [...] after fetch) seeds correctly once the rows arrive, not silently at mount against the initial empty array. In 'single' mode, multiple seeds emit a dev warning and only the first wins.

Custom trigger placement

Pass :expandable-trigger="'none'" to opt out of the auto-injected column, then place <VCTableExpandTrigger> anywhere inside a data cell. The trigger reads its expansion state from the surrounding row's context — it just needs to be a descendant of an expandable<VCTableRow>.

vue
<VCTable :columns :data expandable expandable-trigger="none">
  <template #cell-name="{ row }">
    <VCTableExpandTrigger />
    {{ row.name }}
  </template>
  <template #expansion="{ row }">
    <pre>{{ JSON.stringify(row, null, 2) }}</pre>
  </template>
</VCTable>

Persistence across data refresh

v-model:expanded persists across re-renders when the bound ref lives outside your data-fetching scope (e.g. on a Pinia store or in page-level setup). When a row whose key is in :expanded gets filtered out, the key is kept — so re-adding the row re-expands it automatically. Mirrors :selection behaviour.

Animation

The expansion panel animates open via a ResizeObserver-measured height. No max-height ceiling — any panel height works correctly. Animations respect prefers-reduced-motion: reduce.

Manual / Shape B usage

When writing rows by hand (without the :columns driver), each <VCTableRow> gets its own :expandable opt-in + #expansion slot:

vue
<VCTable>
  <VCTableHeader> … </VCTableHeader>
  <VCTableBody>
    <VCTableRow v-for="(row, index) in users" :row :index expandable>
      <VCTableCell>{{ row.name }}</VCTableCell>
      <template #expansion>
        <p>Details for {{ row.name }}</p>
      </template>
    </VCTableRow>
  </VCTableBody>
</VCTable>

A standalone row can also be controlled per-instance via :open + @update:open — useful when the row sits outside any <VCTable :expandable> (e.g. tanstack-table wrappers) or when you want fully-local state.

<VCTableLite> — slim escape hatch

Same columns driver + theme system + auto-render as <VCTable>, but without the controlled-sort + row-click + keyboard-nav machinery. For consumers who bring their own state plumbing (e.g. tanstack-table layered on top) and want bundle savings on the sort machine.

vue
<script setup lang="ts">
import { VCTableLite } from '@vuecs/table';
import type { TableColumn } from '@vuecs/table';

const columns: TableColumn<User>[] = [
    { key: 'name', sortable: true },
    { key: 'email' },
];
</script>

<template>
    <!-- Same driver shape; no v-model:sort, no @row-click -->
    <VCTableLite :columns :data />
</template>

Sortable headers render with the indicator markup but clicking is a no-op. :row-clickable is not a declared prop on <VCTableLite> — it has no behavioral effect (the Lite TableContext pins rowClickable: false), but since Lite uses inheritAttrs: false + attribute fallthrough, an undeclared attribute like row-clickable will still land on the <table> DOM element if you pass it. Pass only the props Lite actually declares to avoid spurious HTML attributes. All other parts (<VCTableHeader>, <VCTableBody>, <VCTableEmpty>, <VCTableLoading>, …) work identically.

Stacked responsive mode

<VCTable :responsive /> opts in to a stacked-card layout below the 640px viewport breakpoint. The structural CSS in @vuecs/table/style.css collapses each row into a card, hides the <thead> (kept for assistive tech), and shows each column label as a ::before pseudo element using the data-label attribute that ships on every <td> since v0.1.

vue
<VCTable :columns :data responsive />

Themes can override the breakpoint or card styling by targeting [data-responsive="true"] in their own CSS. The --vc-table-stack-breakpoint CSS variable is reserved for future theme overrides (currently informational — the media-query breakpoint is the source of truth).

Row meta — _rowVariant / _cellVariants

Underscore-prefixed fields on the data row tint the row / specific cells without forcing a function prop:

ts
const users: WithRowMeta<User>[] = [
    { id: 1, name: 'Alice', _rowVariant: 'warning' },
    { id: 2, name: 'Bob', _cellVariants: { email: 'error' } },
];

Sort

Controlled sort via v-model:sort. The state shape is always SortDescriptor[] — empty array means "no sort active", single-column sort is an array of length 1, multi-column sort grows the array.

vue
<script setup lang="ts">
import { ref } from 'vue';
import type { TableSortState } from '@vuecs/table';

const sort = ref<TableSortState>([]);
</script>

<template>
    <VCTable v-model:sort="sort" :columns :data />
</template>

Clicking a :sortable header cycles [] → [asc] → [desc] → []. Enter / Space on a focused <th> does the same. aria-sort flips to ascending / descending / none. With <VCTable :must-sort>, the primary key skips the empty step ([asc] → [desc] → [asc] → ...) so the data is never unsorted.

Multi-column sort + client-side sort

PropTypeDefaultDescription
multiSortbooleanfalseWhen true, Shift-click on a sortable header adds it as a secondary key (or cycles its direction). Plain click replaces.
maxSortKeysnumber3Cap on the sort array under :multi-sort. 0 = unlimited. Adding past the cap evicts the oldest.
clientSortbooleanfalseWhen true, the table reorders :data internally — no consumer-side sort helper needed. v-model:sort still emits state.

Per-column hooks:

FieldTypeDescription
sortablebooleanHeader is interactive (click / Enter / Space cycles sort).
initialSortDirection'asc' | 'desc'First-click direction for this column. Default 'asc'.
sortFn(a, b) => numberCustom value comparator for client-side sort (semver, IPs, etc.). Receives resolved accessor (or formatter) values.
sortByFormattedbooleanClient-side sort compares formatter output instead of raw accessor value. Default false.
nullsFirstbooleanClient-side sort floats null / undefined to the top. Default — nulls sort last regardless of direction.
vue
<VCTable
    v-model:sort="sort"
    :columns :data
    multi-sort
    client-sort
/>

The numeric sort-position badge (1-based) renders via the data-sort-index attribute on secondary/tertiary <th> cells — the primary key keeps the up/down arrow. Themes can override the badge via .vc-table-head-cell[data-sort-index]::after.

<VCTableSortIndicators> — discoverable multi-sort UX

Modifier-key-free alternative to Shift-click. Renders a chip row of active sort descriptors. Each chip is clickable (toggles asc ↔ desc) and carries a × button to remove. The bar also surfaces an Add column dropdown listing unsorted sortable columns, plus a Clear all action.

vue
<VCTableSortIndicators v-model:sort="sort" :columns="columns" />
<VCTable v-model:sort="sort" :columns :data multi-sort client-sort />

Bind v-model:sort to the same ref the table uses — both stay in sync without prop forwarding. Place the chip row above or below the table; it renders a <div>, so it can't live as a slot child of <VCTable> (the default slot goes inside <table>).

All text strings are customisable. Per-instance via props (:label, :emptyContent, :addLabel, :clearLabel, :removeAriaLabel) or app-wide via useComponentDefaults:

ts
app.use(vuecs, {
    defaults: {
        tableSortIndicators: {
            label: 'Sortieren:',
            emptyContent: 'Keine Spalten sortiert',
            addLabel: '+ Spalte hinzufügen',
            clearLabel: 'Alle entfernen',
            removeAriaLabel: 'Sortierschlüssel entfernen',
        },
    },
});

Customisable text keys: label, emptyContent, addLabel, clearLabel, removeAriaLabel, toggleAscTitle, toggleDescTitle, arrowAsc, arrowDesc, removeGlyph.

Slot overrides for visual customisation: #label, #empty, #chip="{ descriptor, index, position, toggle, remove }", #add="{ options, add }", #clear="{ clear }", or #default for a complete layout replacement using the same handlers.

Breaking change in v1.x-B

v-model:sort is now SortDescriptor[] instead of v0.1's { key, direction } \| null. Migrate single-sort bindings as:

diff
- const sort = ref<TableSortState>(null);
+ const sort = ref<TableSortState>([]);

- if (sort.value) { ... }
+ if (sort.value.length > 0) {
+     const { key, direction } = sort.value[0];
+ }

Table-placeholder skeleton

<VCTablePlaceholder> is the table-shaped loading skeleton — rows × columns placeholder bars in a real <table> element so the surrounding layout reads as a table. Cell widths vary per index so the result looks natural rather than a uniform grid. Composes <VCPlaceholder> from @vuecs/placeholder per cell.

vue
<script setup>
import { VCTablePlaceholder } from '@vuecs/table';
</script>

<template>
    <VCTablePlaceholder :rows="5" :columns="7" show-header />

    <!-- Custom header markup via the `#thead` slot. -->
    <VCTablePlaceholder :rows="3" :columns="4">
        <template #thead>
            <tr><th colspan="4">Loading entries…</th></tr>
        </template>
    </VCTablePlaceholder>
</template>
PropTypeDefaultDescription
rowsnumber5Body row count.
columnsnumber4Column count.
showHeaderbooleantrueRender <thead> band.
showFooterbooleanfalseRender <tfoot> band.
animation'wave' | 'glow' | 'none''wave'Forwarded to every inner bar.

Slot overrides: #thead / #tfoot replace the default header / footer rendering with consumer markup — useful when the default fixed-column header doesn't match the real table layout.

Integrated :placeholder mode on <VCTable> / <VCTableLite>

For the common "swap real rows for a skeleton while loading" pattern, opt into :placeholder directly on the table — no VCPlaceholderWrapper chrome needed. The table already knows its :columns count and the current :data.length, so the skeleton matches the layout exactly.

vue
<VCTable :busy="loading" placeholder :columns :data />

Behavior:

  • Header still renders from :columns (real labels).
  • Body is replaced by a <tbody> of skeleton rows; :placeholder-rows overrides the count (defaults to :data.length, or 5 when the data is empty — the first-load case).
  • <VCTableLoading> / <VCTableEmpty> siblings are suppressed while in placeholder mode so AT consumers don't get a double-loader.
  • No-op when :busy is false, or when :columns is unset.

Same opt-in is available on <VCTableLite>. For more layout control (custom header markup, separate skeleton row counts, no columns-driver setup), reach for the explicit <VCTablePlaceholder> + <VCPlaceholderWrapper> composition instead.

Theme keys

ComponentSlot keys
tableroot, scrollContainer
tableHeader / tableBody / tableFooter / tableRow / tableEmptyroot
tableCellroot
tableHeadCellroot, sortIcon
tableLoadingroot, overlay
tableRowExpansionroot, cell, panel, panelInner
tableExpandTriggerroot, icon
tableExpandTriggerCellroot
tablePlaceholderroot, header, body, footer, row, cell
tableSortIndicatorsroot, label, empty, chip, chipToggle, chipPosition, chipLabel, chipArrow, chipRemove, addWrapper, add, clear

Variant axes opted-into per theme

ComponentAxisValues
tabledensity × striped × bordered × hover × stickyHeadercompact/normal/spacious × boolean × boolean × boolean × boolean
tableRowdisabled / selected / focused / rowVariantboolean × boolean × boolean × success / warning / error / info / neutral / primary
tableCellalign / stickyColumn / cellVariantleft/center/right × boolean × six semantic colors
tableHeadCellalign / stickyColumn / sortedleft/center/right × boolean × asc/desc/none
tableEmptyfilteredboolean (distinct copy / style for empty-after-filter vs empty-no-data)
tableLoadingoverlayboolean (in-table band vs absolute overlay for refresh-feedback)

Released under the Apache 2.0 License.