npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rozie-ui/data-table-svelte

v0.2.1

Published

Idiomatic Svelte headless WAI-ARIA data table (sorting, filtering, pagination, selection, column visibility/resize/reorder/pinning) on @tanstack/table-core — one accessible Rozie source compiled to Svelte.

Readme

@rozie-ui/data-table-svelte

Idiomatic svelte DataTable — a headless, fully-accessible (WAI-ARIA) data table (sorting, global + per-column filtering, pagination, row selection, column visibility / resize / reorder / pinning, sticky header) compiled from one Rozie source. The state engine is @tanstack/table-core — the SAME framework-agnostic core behind TanStack Table, wired to this framework's reactivity with NO per-framework adapter. Every visual value is a CSS custom property, so it re-skins to any design system. This package is generated; do not edit src/ by hand.

Install

npm i @rozie-ui/data-table-svelte

Peer dependencies: svelte + @tanstack/table-core + @tanstack/virtual-core + @rozie-ui/popover-svelte. Install them alongside this package.

Usage

Columns may be declared as a :columns config array or as <Column> children (or both — an id-keyed last-write-wins union). Per-cell rendering is one parent #cell / #colHeader renderer on <DataTable>, dispatched by columnId, so it works the same with either column form.

Columns as a config array

<script lang="ts">
  import DataTable from '@rozie-ui/data-table-svelte';

  const rows = [
    { id: 1, name: 'Ada Lovelace',   email: '[email protected]',  status: 'active' },
    { id: 2, name: 'Alan Turing',    email: '[email protected]',    status: 'active' },
    { id: 3, name: 'Grace Hopper',   email: '[email protected]',         status: 'away'   },
  ];
  const columns = [
    { field: 'name',   header: 'Name',   sortable: true, filterable: true },
    { field: 'email',  header: 'Email' },
    { field: 'status', header: 'Status', sortable: true },
  ];
  let sorting = $state<{ id: string; desc: boolean }[]>([]);
</script>

<DataTable data={rows} {columns} bind:sorting selectionMode="multiple" stickyHeader />

Declarative <Column> children + a custom cell

<script lang="ts">
  import DataTable, { Column } from '@rozie-ui/data-table-svelte';

  const rows = [
    { id: 1, name: 'Ada Lovelace',   email: '[email protected]',  status: 'active' },
    { id: 2, name: 'Alan Turing',    email: '[email protected]',    status: 'active' },
    { id: 3, name: 'Grace Hopper',   email: '[email protected]',         status: 'away'   },
  ];
  let sorting = $state<{ id: string; desc: boolean }[]>([]);
</script>

<DataTable data={rows} bind:sorting selectionMode="multiple" stickyHeader>
  <Column field="name" header="Name" sortable filterable />
  <Column field="email" header="Email" />
  <Column field="status" header="Status" sortable />

  <!-- One cell snippet on <DataTable>, dispatched by columnId -->
  {#snippet cell({ columnId, value })}
    {#if columnId === 'status'}<span class="badge">{value}</span>{:else}{value}{/if}
  {/snippet}
</DataTable>

Virtualized rows (windowing)

<script lang="ts">
  import DataTable, { Column } from '@rozie-ui/data-table-svelte';

  const rows = Array.from({ length: 10_000 }, (_, i) => ({
    id: i + 1,
    name: `Row ${i + 1}`,
    email: `user${i + 1}@example.com`,
    status: i % 2 ? 'active' : 'away',
  }));
</script>

<!-- PROP form — bound maxHeight sizes the scroll container. -->
<DataTable data={rows} virtual maxHeight="400px">
  <Column field="name" header="Name" />
  <Column field="email" header="Email" />
  <Column field="status" header="Status" />
</DataTable>

<!-- TOKEN form — the same height via the CSS custom property (prop wins when both
     are set; the token is the fallback). estimateRowHeight tunes the seed. -->
<DataTable data={rows} virtual estimateRowHeight={48} style="--rozie-data-table-max-height: 400px">
  <Column field="name" header="Name" />
  <Column field="email" header="Email" />
  <Column field="status" header="Status" />
</DataTable>

Editable cells (inline edit + validation)

<script lang="ts">
  import DataTable, { Column } from '@rozie-ui/data-table-svelte';

  // The component OWNS edit state — bind ONE model (bind:data) + listen for commits.
  let rows = $state([
    { id: 1, name: 'Alpha', qty: 3, status: 'active',   active: true,  score: 41 },
    { id: 2, name: 'Beta',  qty: 7, status: 'archived', active: false, score: 92 },
  ]);
  const statusOptions = [
    { value: 'active',   label: 'Active' },
    { value: 'archived', label: 'Archived' },
    { value: 'pending',  label: 'Pending' },
  ];
  const validateQty = (value: unknown) => Number(value) >= 0 || 'must be >= 0';
</script>

<DataTable
  interactionMode="grid"
  bind:data={rows}
  oncelleditcommit={(p) => console.log('cell commit', p)}
  onroweditcommit={(p) => console.log('row commit', p)}
>
  <Column field="name" header="Name" editable editor="text" />
  <Column field="qty" header="Qty" editable editor="number" validate={validateQty} />
  <Column field="status" header="Status" editable editor="select" editorOptions={statusOptions} />
  <Column field="active" header="Active" editable editor="checkbox" />
  <Column field="score" header="Score" editable editor="custom" />

  <!-- The #editor scoped slot is a snippet on Svelte; it replaces the built-in editor. -->
  {#snippet editor({ columnId, value, commit, cancel })}
    {#if columnId === 'score'}
      <span>
        <button type="button" onclick={() => commit(Number(value) - 1)}>−</button>
        <button type="button" onclick={() => commit(Number(value) + 1)}>+</button>
        <button type="button" onclick={() => cancel()}>esc</button>
      </span>
    {/if}
  {/snippet}
</DataTable>

Expandable rows (#detail slot + nested sub-rows)

<script lang="ts">
  import DataTable, { Column } from '@rozie-ui/data-table-svelte';

  const rows = [
    { id: 1, name: 'Engineering', headcount: 12, children: [
      { id: 11, name: 'Frontend', headcount: 5 },
      { id: 12, name: 'Backend',  headcount: 7 },
    ] },
    { id: 2, name: 'Sales', headcount: 8 },
  ];
  // bind:expanded keeps MULTIPLE rows open; getSubRows yields depth-indented child rows.
  let expanded = $state<Record<string, boolean>>({});
  const getSubRows = (row: { children?: unknown[] }) => row.children;
</script>

<DataTable data={rows} expandable bind:expanded {getSubRows}>
  <Column field="name" header="Name" />
  <Column field="headcount" header="Headcount" />

  <!-- The #detail scoped slot is a snippet on Svelte; it renders under each open row. -->
  {#snippet detail({ row })}
    <aside class="detail">More about {row.name}</aside>
  {/snippet}
</DataTable>

Grouping + aggregation (headless #groupBar)

<script lang="ts">
  import DataTable, { Column } from '@rozie-ui/data-table-svelte';

  const rows = [
    { id: 1, region: 'North', category: 'Hardware', units: 3, score: 41 },
    { id: 2, region: 'North', category: 'Hardware', units: 5, score: 67 },
    { id: 3, region: 'North', category: 'Software', units: 2, score: 90 },
    { id: 4, region: 'South', category: 'Hardware', units: 7, score: 60 },
  ];
  let grouping = $state<string[]>([]);
  const scoreRange = (columnId: string, leafRows: { getValue: (id: string) => number }[]) => {
    const v = leafRows.map((r) => Number(r.getValue(columnId)));
    return v.length ? Math.max(...v) - Math.min(...v) : 0;
  };
</script>

<!-- groupable enables grouping; the model is an ORDERED column-id list; the event is
     `group-change` → ongroupchange. aggregationFn rolls leaf values into the group header. -->
<DataTable data={rows} groupable bind:grouping ongroupchange={(g) => console.log('grouping', g)}>
  <Column field="region" header="Region" />
  <Column field="category" header="Category" />
  <Column field="units" header="Units" aggregationFn="sum" />
  <Column field="score" header="Score" aggregationFn={scoreRange} />

  <!-- #groupBar is HEADLESS — build the bar from its props (NO built-in drag UI). -->
  {#snippet groupBar({ grouping, groupableColumns, applyGrouping, clearGrouping })}
    <div class="group-bar">
      <button type="button" onclick={() => applyGrouping(['region', 'category'])}>Group region → category</button>
      <button type="button" onclick={() => clearGrouping()}>Clear</button>
      <span>{grouping.join(' → ') || 'ungrouped'} ({groupableColumns.length} groupable)</span>
    </div>
  {/snippet}
</DataTable>

Faceted filtering exposure (headless #filter)

<script lang="ts">
  import DataTable, { Column } from '@rozie-ui/data-table-svelte';

  // Faceting is HEADLESS + read-only (NO event, NO built-in control). The #filter slot
  // hands you `uniqueValues` (keys, cross-filtered) + numeric `minMax`.
  const rows = [
    { id: 1, name: 'Alpha',   category: 'Hardware', price: 30 },
    { id: 2, name: 'Beta',    category: 'Software', price: 90 },
    { id: 3, name: 'Gamma',   category: 'Hardware', price: 10 },
    { id: 4, name: 'Delta',   category: 'Service',  price: 50 },
  ];
  let columnFilters = $state<{ id: string; value: unknown }[]>([]);
</script>

<DataTable data={rows} bind:columnFilters>
  <Column field="name" header="Name" />
  <Column field="category" header="Category" filterable />
  <Column field="price" header="Price" filterable />

  {#snippet filter({ columnId, uniqueValues, minMax })}
    {#if columnId === 'category'}
      <fieldset>
        {#each uniqueValues as v}<label><input type="checkbox" /> {v}</label>{/each}
      </fieldset>
    {:else}
      <input type="range" min={minMax[0]} max={minMax[1]} />
    {/if}
  {/snippet}
</DataTable>

Drop-in editor components (#editor)

<script lang="ts">
  import DataTable, {
    Column, EditorText, EditorNumber, EditorSelect, EditorCheckbox, EditorDate,
  } from '@rozie-ui/data-table-svelte';

  // OPT-IN drop-in editors fill the #editor snippet — DataTable stays the headless
  // DEFAULT export; the editors are additive named exports. Spread the snippet scope
  // through to each drop-in ({ columnId, column, row, value, commit, cancel });
  // EditorSelect also takes `options`. Use them as-is, or fork one as a template.
  let rows = $state([
    { id: 1, name: 'Alpha', qty: 3, status: 'active',   active: true,  score: 41 },
    { id: 2, name: 'Beta',  qty: 7, status: 'archived', active: false, score: 92 },
  ]);
  const statusOptions = [
    { value: 'active',   label: 'Active' },
    { value: 'archived', label: 'Archived' },
    { value: 'pending',  label: 'Pending' },
  ];
</script>

<DataTable
  interactionMode="grid"
  bind:data={rows}
  oncelleditcommit={(p) => console.log('cell commit', p)}
>
  <Column field="name" header="Name" editable editor="custom" />
  <Column field="qty" header="Qty" editable editor="custom" />
  <Column field="status" header="Status" editable editor="custom" />
  <Column field="active" header="Active" editable editor="custom" />
  <Column field="score" header="Score" editable editor="custom" />

  <!-- One #editor snippet, dispatched by columnId, wiring the drop-in editors. -->
  {#snippet editor(scope)}
    {#if scope.columnId === 'name'}
      <EditorText {...scope} />
    {:else if scope.columnId === 'qty'}
      <EditorNumber {...scope} />
    {:else if scope.columnId === 'status'}
      <EditorSelect {...scope} options={statusOptions} />
    {:else if scope.columnId === 'active'}
      <EditorCheckbox {...scope} />
    {:else if scope.columnId === 'score'}
      <EditorDate {...scope} />
    {/if}
  {/snippet}
</DataTable>

Drop-in filter components (#filter)

<script lang="ts">
  import DataTable, {
    Column, FilterText, FilterNumberRange, FilterSelect,
  } from '@rozie-ui/data-table-svelte';

  // OPT-IN drop-in filters fill the #filter snippet — DataTable stays the headless
  // DEFAULT export; the filters are additive named exports. Mark each column filterable
  // (the #filter snippet only renders for filterable columns) and spread the snippet
  // scope ({ columnId, column, value, setFilter, uniqueValues, minMax }) through.
  let rows = $state([
    { id: 1, name: 'Alpha',   category: 'Hardware', price: 30 },
    { id: 2, name: 'Beta',    category: 'Software', price: 90 },
    { id: 3, name: 'Gamma',   category: 'Hardware', price: 10 },
    { id: 4, name: 'Delta',   category: 'Service',  price: 50 },
  ]);
  let columnFilters = $state<{ id: string; value: unknown }[]>([]);
</script>

<DataTable bind:data={rows} bind:columnFilters>
  <Column field="name" header="Name" filterable />
  <Column field="category" header="Category" filterable />
  <Column field="price" header="Price" filterable />

  <!-- One #filter snippet, dispatched by columnId, wiring the drop-in filters. -->
  {#snippet filter(scope)}
    {#if scope.columnId === 'name'}
      <FilterText {...scope} />
    {:else if scope.columnId === 'category'}
      <FilterSelect {...scope} />
    {:else if scope.columnId === 'price'}
      <FilterNumberRange {...scope} />
    {/if}
  {/snippet}
</DataTable>

Drop-in group bar + detail panel (#groupBar / #detail)

<script lang="ts">
  import DataTable, {
    Column, GroupBar, DetailPanel,
  } from '@rozie-ui/data-table-svelte';

  // OPT-IN drop-ins fill the #groupBar + #detail snippets — DataTable stays the headless
  // DEFAULT export; both are additive named exports. Spread the snippet scope through to
  // each (GroupBar gets { grouping, groupableColumns, applyGrouping, clearGrouping };
  // DetailPanel gets { row }). Use them as-is, or fork either as a starter.
  let rows = $state([
    { id: 1, region: 'North', category: 'Hardware', units: 3, score: 41 },
    { id: 2, region: 'North', category: 'Hardware', units: 5, score: 67 },
    { id: 3, region: 'North', category: 'Software', units: 2, score: 90 },
    { id: 4, region: 'South', category: 'Hardware', units: 7, score: 60 },
  ]);
  let grouping = $state<string[]>([]);
  let expanded = $state<Record<string, boolean>>({});
</script>

<DataTable data={rows} groupable expandable bind:grouping bind:expanded>
  <Column field="region" header="Region" />
  <Column field="category" header="Category" />
  <Column field="units" header="Units" aggregationFn="sum" />

  <!-- GroupBar fills #groupBar; DetailPanel fills #detail. -->
  {#snippet groupBar(scope)}
    <GroupBar {...scope} />
  {/snippet}
  {#snippet detail(scope)}
    <DetailPanel {...scope} />
  {/snippet}
</DataTable>

Theming

Every visual value is a --rozie-data-table-* CSS custom property — override any of them at any ancestor scope. Ready-made design-system bridges ship in the package (import base.css first, then a bridge):

import '@rozie-ui/data-table-svelte/themes/base.css';
import '@rozie-ui/data-table-svelte/themes/shadcn.css';    // or material.css, bootstrap.css

Props

| Name | Type | Default | Two-way (model) | Required | Description | | --- | --- | --- | :---: | :---: | --- | | data | Array | | ✓ | ✓ | The row data — model: true, so a committed cell/row edit writes a fresh array back through r-model:data (uncontrolled fallback dataDefault). A stable reference per Rozie's setup-once model — fed directly into table-core (never map/cloned in the watcher). | | columns | Array | [] | | | Config-array column fallback (lower precedence than <Column> children). Each entry: { id?, field, header?, sortable?, filterable?, pinned?, width? }. Columns may come from this array, from <Column> children, or both (id-keyed last-write-wins union). | | selectionMode | String | "none" | | | Row-selection mode: 'none' | 'single' | 'multiple'. 'multiple' auto-injects a leading checkbox column with a select-all header. | | sorting | Array | [] | ✓ | | SortingState[{ id, desc }]. Uncontrolled fallback when unbound. Two-way: writes funnel a fresh value through the sort-change event regardless of binding. | | globalFilter | String | '' | ✓ | | The global search string — narrows all columns. Feeds getFilteredRowModel(). Surfaces through filter-change. Two-way: fires filter-change regardless of binding. | | columnFilters | Array | [] | ✓ | | ColumnFiltersState[{ id, value }] per-column narrowing (gated by each column's filterable). Two-way: whole-array replace on write, fires filter-change. | | pagination | Object | {…} | ✓ | | { pageIndex, pageSize }. Defaults to { pageIndex: 0, pageSize: 10 }; feeds the prev/next + page-size chrome (and getPaginationRowModel()). Two-way: funnels a fresh object through page-change. | | manual | Boolean | false | | | Server-side hook: sets manualPagination / manualFiltering / manualSorting so table-core trusts the consumer-supplied rows and only emits the change events (the consumer fetches each page). | | rowCount | Number | null | | | Total server-side row count for manual pagination; lets the table compute page count when it doesn't hold the full dataset. | | pageCount | Number | null | | | Explicit total page count for manual pagination; overrides rowCount-derived count. | | expandable | Boolean | false | | | Opt-in expandable rows. When true, a leading chevron expander column auto-injects (after the select column) and getExpandedRowModel activates; default false is byte-identical-off. Every row can expand to reveal a #detail panel unless getSubRows is supplied (then only rows with children expand). Bind :expandable="true" (a bare attr only coerces on Vue+Lit). | | expanded | any | null | ✓ | | ExpandedState{ [rowId]: true }, or the true literal after expandAll (declared type: [Object, Boolean]). Multi-expand (multiple rows open at once). Surfaces through expand-change; uncontrolled fallback ($data.expandedDefault) when unbound — the default is null so the uncontrolled fallback AND the grouping auto-expand default are reachable (a non-null default would short-circuit them). When grouping is active and expanded is untouched, group subtrees auto-expand. | | getSubRows | Function | null | | | Table-level child-row accessor (originalRow, index) => TData[] \| undefined that drives nested sub-rows. When supplied (with expandable), table-core flattens the hierarchy and the expand seam reveals depth-indented child rows. Null → the #detail scoped slot is the expand mode. | | groupable | Boolean | false | | | Opt-in gate for the headless #groupBar host region. Default false is byte-identical-off. getGroupedRowModel is wired unconditionally (inert when grouping is empty), so grouping is driven by the grouping model; this flag only gates the consumer-facing group-bar surface (the component ships no built-in drag UI). | | grouping | Array | null | ✓ | | GroupingState — an ordered string[] of column ids (multi-column → nested groups, e.g. ['region','category']). An empty/unbound list is ungrouped (byte-identical-off). Group-header rows are collapsible (they ride the expand model). Surfaces through group-change; uncontrolled fallback ($data.groupingDefault, default []) when unbound — the default is null (mirroring expanded) so the uncontrolled fallback is reachable and the grouping auto-expand default can activate when a consumer applies grouping without binding r-model:grouping (a non-null [] default would short-circuit it). All reads are null-guarded, so table-core still receives an array. | | rowSelection | Object | {} | ✓ | | RowSelectionState{ [rowId]: true }. Checkbox-only toggle (the row body does not select). Driven by the selectionMode chrome. Two-way: fires selection-change regardless of binding. | | columnVisibility | Object | {} | ✓ | | VisibilityState{ [colId]: boolean }. Hidden columns drop automatically from header + body. Two-way: funnels a fresh object through visibility-change. | | columnSizing | Object | {} | ✓ | | ColumnSizingState{ [colId]: number }. Driven live by the pointer-drag resize handle (columnResizeMode: 'onChange'). Two-way: fires resize-change. | | columnOrder | Array | [] | ✓ | | ColumnOrderStatestring[]. A fresh order array on reorder (never an in-place splice). Two-way: fires reorder-change. | | columnPinning | Object | {…} | ✓ | | ColumnPinningState{ left: string[], right: string[] }. Pinned columns get position: sticky + computed offsets. Defaults to { left: [], right: [] }. Two-way: fires pin-change. | | stickyHeader | Boolean | false | | | Pure-CSS sticky header: the <thead> sticks to the top of the scroll container. | | interactionMode | String | "table" | | | 'table' (default, row-oriented, byte-behaviorally identical to a plain accessible table) | 'grid' (GA since Phase 63) — lights up the full WAI-ARIA grid interaction mode: role="grid", a roving single tab-stop, 2-D APG arrow-key cell navigation, range selection, and clipboard support. | | singleClickEdit | Boolean | false | | | Grid mode only. When true, a plain click on an editable cell opens its editor immediately (single-click-to-edit) instead of just activating the cell. Default false keeps click-to-activate (double-click opens the editor). Shift+click (range selection) and clicks on non-editable cells are unaffected. | | undoable | Boolean | false | | | Grid mode. When true, every committed data mutation (cell/row edit, paste, fill, cut, clear) becomes one undo step: Ctrl/Cmd+Z undoes, Ctrl/Cmd+Y or Ctrl/Cmd+Shift+Z redoes. Default false records no history and Ctrl+Z/Y are inert. | | undoLimit | Number | 100 | | | The maximum number of undo steps retained (oldest evicted past this depth). Only consulted when undoable is true. | | virtual | Boolean | false | | | Opt-in vertical row windowing. When true, only the visible slice of rows renders inside a bounded rdt-scroll container (with leading/trailing spacer rows preserving total scroll height), windowing over the full filtered + sorted (pre-pagination) model and suppressing the client pagination chrome. Default false is byte-identical to a non-virtual table. | | estimateRowHeight | Number | 40 | | | Estimated row height (px) seeding the windowing engine before measureElement refines actual heights. Only consulted when virtual is on. | | maxHeight | String | '' | | | A CSS length string bounding the rdt-scroll container when virtual is on (e.g. '400px'). Mirrored to the --rozie-data-table-max-height custom property; the prop wins, the token is the fallback. |

Events

| Event | Description | | --- | --- | | sort-change | Fired when the sort state changes (header click / shift-click multi-sort / a sortColumn/clearSorting call). Payload is the fresh SortingState array [{ id, desc }]. | | expand-change | Fired (phase 50) when the expanded-row set changes (an expander chevron toggle — click / Enter / Space — or a toggleRowExpanded/expandAll/collapseAll call). Fires exactly once per change (the echo-guarded write funnel dedups the React multi-render re-entry, D-07) and REGARDLESS of whether r-model:expanded is bound. Payload is the fresh ExpandedState — a { [rowId]: true } object, or the true literal after expandAll (Pitfall 2: it is passed through verbatim — never Object.keys it without a === true guard). Named expand-change (not expanded-change): the model:true expanded prop owns the onExpandedChange callback on the React/Solid Props interface, so the event stems off a distinct name to avoid a duplicate-identifier collision (the house convention every slice follows). | | group-change | Fired (phase 50) when the grouping state changes (a consumer-built #groupBar apply/clear, mutating the grouping model, or an applyGrouping/clearGrouping call). Fires exactly once per change (the echo-guarded write funnel dedups the React multi-render re-entry, D-07) and REGARDLESS of whether r-model:grouping is bound. Payload is the fresh GroupingState — an ORDERED string[] of column ids (e.g. ["region","category"] for nested groups), or [] when cleared. Named group-change (not grouping-change): the model:true grouping prop owns the onGroupingChange callback on the React/Solid Props interface, so the event stems off a distinct name to avoid a duplicate-identifier collision (the house convention every slice follows — sorting→sort-change, expanded→expand-change). | | filter-change | Fired when a filter changes. Payload is { globalFilter } for the global search box or { columnFilters } (the fresh ColumnFiltersState [{ id, value }]) for a per-column filter — both surface through this one event. | | page-change | Fired when pagination changes (prev/next, a page-size change, or a setPage/setRowsPerPage call). Payload is the fresh { pageIndex, pageSize } object. | | selection-change | Fired when the row selection changes (a row/select-all checkbox toggle or a toggleAllRows/clearSelection call). Payload is the fresh RowSelectionState { [rowId]: true } object. | | visibility-change | Fired when a column is shown/hidden (the column-toggle menu or a toggleColumnVisibility call). Payload is the fresh VisibilityState { [colId]: boolean } object. | | resize-change | Fired live during a column resize drag (columnResizeMode: "onChange"). Payload is the fresh ColumnSizingState { [colId]: number } object. | | reorder-change | Fired when the column order changes (an applyColumnOrder call or a header reorder). Payload is the fresh ColumnOrderState string[]. | | pin-change | Fired when a column is pinned/unpinned (the per-header pin buttons or a pinColumn call). Payload is the fresh ColumnPinningState { left: string[], right: string[] } object. | | history-change | Fired (260709-8ct, grid-wide undo/redo) when undo/redo AVAILABILITY changes — EDGE-triggered (only when canUndo/canRedo actually flip, not on every recorded edit) so a toolbar can enable/disable its buttons without polling. Only meaningful when undoable is true. Payload is { canUndo, canRedo } booleans. No history model prop exists, so there is no model-prop==emit-name duplicate-identifier collision. | | activecell-change | Fired (grid interaction mode only) whenever the active cell moves — by keyboard navigation or a focusCell call. One-way notification; payload is { rowIndex, colIndex } integers over the visible model. Fires on every move including arrow keypresses (D-02). | | range-change | Fired (Phase 51 req-7 / D-07) whenever the rectangular cell-range selection changes — extended by Shift+Arrow / Shift+Click from the single extendRange/setRangeFocus call sites (React multi-emit dedup, D-07). One-way notification (the range is NOT a model:true slice — the model:true count stays at 10, leaving the Angular multi-model-CVA condition untouched); it is a SEPARATE layer from the row-selection slice and the two never corrupt each other. Payload is getSelectedRange(){ anchor, focus } where each corner is a { rowIndex, colIndex } index pair over the visible model (integers only — no row data, no DOM node), or null when no range is set. | | cell-edit-commit | Fired (Phase 51) when an editable cell commits a NEW, validated value — once per committed cell, from the single commitEdit call site (a rejected/invalid commit fires nothing — D-01). Pairs with the r-model:data write (one fresh-array replace per commit). Payload is { rowId, columnId, oldValue, newValue }. cancel/Escape and a validation failure emit nothing. | | row-edit-commit | Fired (Phase 51 req-6 / D-06) when a FULL-ROW edit commits — once per row save, from the single commitRow call site (React multi-emit dedup, D-07). A row enters full-row edit via Shift+F2 or the editRow verb (every editable cell edits at once); one save validates every edited cell (D-01 — a single failure blocks the whole row and emits nothing) then writes the bound r-model:data ONCE (a single fresh-array row-object replace with all changes applied) and fires this event ONCE. Escape reverts the whole row as a unit and emits nothing. Payload is { rowId, changes } where changes is [{ columnId, oldValue, newValue }] for ONLY the columns whose value actually changed. |

Imperative handle

Beyond props, the component exposes imperative methods (declared once in the Rozie source via $expose). Grab a handle with the native ref mechanism and call them directly:

| Method | Description | | --- | --- | | sortColumn | Toggle (or set) the sort for a column — sortColumn(colId, desc?). Drives table-core so sort-change fires with the fresh SortingState. | | clearSorting | Clear all sorting — clearSorting(). Resets to the unsorted core row model and fires sort-change. | | toggleRowExpanded | Toggle ONE row's expanded state (phase 50 req-3) — toggleRowExpanded(rowId) where rowId is the consumer's row id (the data field) OR the table-core row id (both resolve). Scans the core flat-row set so a collapsed parent is still resolvable. Drives table-core so expand-change fires with the fresh ExpandedState. Multi-expand: it does not collapse other open rows. | | expandAll | Open every expandable row (phase 50 req-3) — expandAll(). Drives table-core (toggleAllRowsExpanded(true)) so expand-change fires; the payload may be the true expand-all literal (Pitfall 2). | | collapseAll | Collapse every row (phase 50 req-3) — collapseAll(). Resets the expanded set to a blank state (resetExpanded(true){}) and fires expand-change with {}. | | getExpandedRows | Return the original row data for the currently-expanded rows (phase 50 req-3) — getExpandedRows()unknown[] (empty when nothing is expanded). The read-verb twin of the expand-change event. | | applyGrouping | Set the full grouping — applyGrouping(cols) where cols is a fresh ORDERED string[] of column ids (multi-column → nested groups, e.g. ["region","category"]). Drives table-core (table.setGrouping) so group-change fires with the fresh GroupingState. (Named applyGrouping, not setGrouping: a set<ModelProp> verb collides with Reacts auto-generated groupinguseState setter and an $expose verb is rename-protected — ROZ524; sameapplyColumnOrderprecedent.) | |clearGrouping| Clear all grouping —clearGrouping(). Resets to the ungrouped (flat) row model (table.setGrouping([])) and fires group-changewith[]. | | getFacetedUniqueValues| Return a column's CROSS-FILTERED distinct values (phase 50 reqs 8-9, D-03) —getFacetedUniqueValues(colId)unknown[]of distinct values (KEYS ONLY — occurrence counts are deliberately NOT exposed). Resolves the column viatable.getColumn(colId)and reads table-core's faceted unique-value map, returningArray.from(map.keys()). Cross-filtered: the values reflect rows passing all OTHER active column filters and update when an upstream filter changes. Empty array when the column/table is missing. Inert (the faceted models stay off-path) until this verb or the #filterslot reads a facet. | |getFacetedMinMaxValues| Return a numeric column's CROSS-FILTERED[min, max]range (phase 50 reqs 8-9, D-03) —getFacetedMinMaxValues(colId)[number, number] | null. Resolves the column via table.getColumn(colId)and reads table-core's faceted min/max. Cross-filtered (reflects rows passing all OTHER active column filters) and updates when an upstream filter changes.nullwhen unavailable. The read twin handed to the#filterscoped slot so a consumer builds a numeric range slider purely from exposed values. | |getColumnDefs| Return the resolvedColumnDef[](the id-keyed LWW union of the:columnsconfig array and thechildren) —getColumnDefs(). | | toggleAllRows| Select or clear all (filtered) rows —toggleAllRows(value). Drives table-core so selection-changefires with the freshRowSelectionState. | | clearSelection| Clear the row selection —clearSelection(). Fires selection-changewith{}. | | getSelectedRows| Return the original row data for the currently-selected rows —getSelectedRows()unknown[](empty when nothing is selected). | |setPage| Go to a 0-based page index —setPage(idx). Drives table-core so page-changefires with the fresh{ pageIndex, pageSize }. | | setRowsPerPage| Set the page size —setRowsPerPage(size). Fires page-changewith the fresh pagination object. | |toggleColumnVisibility| Show/hide a column —toggleColumnVisibility(colId). Drives table-core so visibility-changefires with the freshVisibilityState. | | applyColumnOrder| Set the full column order —applyColumnOrder(order)whereorderis a freshstring[]. Fires reorder-change. (Named applyColumnOrder, not setColumnOrder: a set verb collides with Reacts auto-generated columnOrder setter and an $expose verb is rename-protected — ROZ524.) | | resetColumnSizing | Reset all column widths to their defaults — resetColumnSizing(). Fires resize-change. | | pinColumn | Pin a column to a side or unpin it — pinColumn(colId, side) where side is 'left' | 'right' | false. Fires pin-change with the fresh ColumnPinningState. | | focusCell | Move + focus the active cell (grid interaction mode) — focusCell(rowIndex, colIndex). rowIndex is the ABSOLUTE display-order position in getPrePaginationRowModel().rows (filter+sort+expand applied, BEFORE pagination/windowing), in BOTH paginated and virtual modes (C1): a paginated focusCell(abs) switches to Math.floor(abs/pageSize) then focuses; a virtual focusCell(abs) scrolls-then-focuses. Args coerced to integers + clamped to bounds. Fires activecell-change with the absolute rowIndex. (Named focusCell, not focus: a bare focus verb shadows the inherited HTMLElement.focus on Lit — ROZ137.) | | getActiveCell | Return the current active-cell position — getActiveCell(){ rowIndex, colIndex, isHeader }. For a body cell rowIndex is the ABSOLUTE display-order index (C1, matching focusCell / activecell-change) and isHeader is false; for a header-active cell rowIndex is null and isHeader is true (integers only — no row data, no DOM node). | | clearActiveCell | Reset the roving active-cell position to the entry cell and exit interaction mode — clearActiveCell(). The next Tab-in re-enters at the entry cell (D-01). (Named clearActiveCell, not clear: distinct from the listbox clear selection verb.) | | getRowIndexRelativeToPage | Convert an ABSOLUTE display-order row index (the focusCell / getActiveCell / activecell-change space, C1) to the PAGE-RELATIVE index — getRowIndexRelativeToPage(absRow?)number. With no argument it converts the current active cell (abs - pageIndex*pageSize). In virtual mode (no pagination) it returns the absolute index unchanged. Mirrors MUI getRowIndexRelativeToVisibleRows. (Named getRowIndexRelativeToPage: collision-clean against the verb/event/prop and Lit ROZ137 reserved sets.) | | editCell | Programmatically open the editor on a cell (Phase 51) — editCell(rowIndex, colIndex), addressed by index over the visible model (args coerced to integers + clamped). No-op on a non-editable cell. (Named editCell, not edit: collision-clean against the verb/event/prop and Lit ROZ137 reserved sets.) | | commitEditing | Programmatically commit the open editor (Phase 51) — commitEditing(). Runs the column validator; on success writes the bound r-model:data and fires one cell-edit-commit; on a validation failure keeps the editor open (D-01). No-op when no cell is editing. (Named commitEditing, not commit.) | | editRow | Programmatically enter FULL-ROW edit on a body row (Phase 51 req-6 / D-06) — editRow(rowIndex), addressed by index over the visible model (args coerced to integers + clamped). The API twin of the Shift+F2 shortcut: every editable cell in the row enters edit at once. A later save commits the whole row in one r-model:data write + one row-edit-commit; Escape reverts the row as a unit. No-op on a row with no editable columns. (Named editRow, not edit/editColumn: collision-clean against the verb/event/prop and Lit ROZ137 reserved sets.) | | getSelectedRange | Return the current rectangular cell-range selection (Phase 51 req-7 / D-07) — getSelectedRange(){ anchor, focus } where each corner is a { rowIndex, colIndex } index pair over the visible model (integers only — no row data, no DOM node, T-49-02), or { anchor: null, focus: null } when no range is set. The range is extended by Shift+Arrow / Shift+Click and is ONE-WAY (this read verb + the range-change event), NOT a model:true slice (D-07). (Named getSelectedRange, not getRange/getSelection: collision-clean against getSelectedRows, the verb/event/prop, and the Lit ROZ137 reserved sets.) | | cut | Cut the current cell-range selection (Phase 63 C3) — cut(). Copies the range to the clipboard as TSV (the same escaped serialization Ctrl+C produces) THEN clears the source cells through the bound r-model:data write-funnel in a single writeData (each cell coerced to its column empty — null on a numeric column, the empty string on text — under the editable + validator skip rule, firing one cell-edit-commit per cleared cell). The API twin of the Ctrl/Cmd+X shortcut. A no-op while a header cell is active (the B11 clipboard guard). (Named cut, the spreadsheet-standard verb: collision-clean against the verb/event/prop, the React auto-setter set, and the Lit ROZ137 reserved DOM members — cut is not on HTMLElement.) | | undo | Undo the most recent data mutation (260709-8ct, grid-wide undo/redo) — undo(). Only meaningful when undoable is true. Pops the most recent pre-mutation snapshot, pushes the current data onto the redo stack, and replays the snapshot through the SAME writeData funnel every edit/paste/fill/cut/clear commits through — so the two-way r-model:data writeback keeps working with zero new code. Fires history-change once per call. A no-op when nothing is undoable (canUndo() is false). The API twin of the Ctrl/Cmd+Z shortcut. | | redo | Redo the most recently undone data mutation (260709-8ct) — redo(). Symmetric to undo(): pops the redo stack, pushes the current data back onto the undo stack, replays through the same writeData seam, fires history-change once. A no-op when nothing is redoable (canRedo() is false). The API twin of the Ctrl/Cmd+Y / Ctrl/Cmd+Shift+Z shortcuts. | | canUndo | Return whether an undo step is available (260709-8ct) — canUndo()boolean. Reflects the undo stack depth; use alongside the history-change event to drive a toolbar buttons disabled state without polling. | | canRedo| Return whether a redo step is available (260709-8ct) —canRedo()boolean. Reflects the redo stack depth; cleared by any new edit after an undo (standard undo semantics). | | clearHistory| Discard all undo/redo history (260709-8ct) —clearHistory(). Empties both stacks (canUndo()/canRedo()becomefalse`). Also called internally on an external data-reference swap (a dataset the component itself did not write), since undoing across a dataset boundary is incoherent. |

<script>
  let tbl;                  // component instance via bind:this
</script>

<DataTable bind:this={tbl} data={rows} />
<button onclick={() => tbl.clearSelection()}>Clear</button>
<button onclick={() => tbl.editRow(0)}>Edit row 0</button>
<button onclick={() => console.log(tbl.getSelectedRange())}>Read range</button>
<button onclick={() => tbl.expandAll()}>Expand all</button>
<button onclick={() => tbl.applyGrouping(['region'])}>Group by region</button>
<button onclick={() => console.log(tbl.getFacetedUniqueValues('category'))}>Facet keys</button>

Slots

All rendering slots live on the parent <DataTable> (a <Column> carries metadata only). The cell / colHeader slots are single renderers dispatched by columnId — switch on it to vary the render per column; a column the slot does not render shows the plain accessor value. (On React/Solid these are render-prop props — renderCell / renderColHeader / cellSlot / colHeaderSlot; on Lit they are the .cell / .colHeader properties — the documented cross-framework divergence.)

The detail (expandable rows), groupBar (grouping) and filter (faceted filtering) scoped slots follow the SAME render-prop convention: on React they are renderDetail / renderGroupBar / renderFilter; on Solid they are detailSlot / groupBarSlot / filterSlot; on Lit they are the .detail / .groupBar / .filter properties — the documented React render-prop edge (per the cross-framework compatibility bar). On Vue / Svelte / Angular they are ordinary named scoped slots (#detail / #groupBar / #filter). The groupBar and filter slots are HEADLESS — the component ships NO built-in group-bar / facet control, so the consumer builds the UI purely from the exposed slot props.

| Slot | Params | | --- | --- | | (default) | | | groupBar | grouping, groupableColumns, applyGrouping, clearGrouping | | selectAll | checked, indeterminate, toggle | | colHeader | columnId, column, label | | filter | columnId, value, uniqueValues, minMax, setFilter | | selectCell | row, checked, toggle | | cell | columnId, column, row, value | | editor | columnId, column, row, value, commit, cancel, autofocus | | detail | row |