Detent web components
By Shane Logsdon

Data & tables

Data Table

Implemented

Sortable table with filters and expandable rows

detent-data-table

Port status
Catalog status
implemented
Origin
upstream
Phase
3
Wave
phase-3-data
Group
Data & tables

14.5 KB gzip for the class module

Variants

A local sample dataset: cards appear automatically below 640px; use the view button to switch between cards and the semantic table. Sort the Name or Score column, search, filter, select rows, expand a detail, hide a column, or change the page size. Bulk Delete only reports selected ids — nothing is removed, and nothing is fetched.

No bulk action has run yet.

Recorded states

  • loading recorded state; no setter
  • empty recorded state; no setter
  • expanded recorded state; no setter
  • indeterminate recorded state; no setter
  • cards recorded state; no setter

Usage

Import

import '/components/data-table/define.js';

// or register it yourself
import { DetentDataTable } from '/components/data-table/data-table.js';

Attributes

  • default-page-size
  • default-view-mode
  • empty-description
  • empty-title
  • error
  • is-loading
  • search-placeholder
  • title

Properties

  • columns
  • data
  • getRowId
  • pageSizeOptions
  • renderSubComponent
  • onBulkDelete
  • onBulkExport

Slots

No slots are recorded for this component.

Events

  • detent-bulk-delete — {}
  • detent-bulk-export — {}

Example

Runnable example: site/examples/data-table.ts. The snippet below is generated from the catalogued attributes.

<detent-data-table
  default-page-size="10"
  default-view-mode="auto"
  empty-description
  empty-title
  error
  is-loading
  search-placeholder="Search records..."
  title
></detent-data-table>

Property-only values

const element = document.querySelector('detent-data-table');
if (element !== null) {
  // data: T[]
  element.data = [];
  // columns: ColumnDef<T>[]
  element.columns = [];
  // getRowId: (row: T, index: number) => string
  element.getRowId = () => {};
  // renderSubComponent: (row: T) => ReactNode
  element.renderSubComponent = () => {};
  // onBulkDelete: (selectedIds: string[]) => void
  element.onBulkDelete = () => {};
  // onBulkExport: (selectedIds: string[]) => void
  element.onBulkExport = () => {};
}

Source

The pinned upstream source for this row:

  • src/components/ui/AdvancedDataTable.tsx sha256 54ce9725f5f7…

Native files

The native module and its artifacts:

  • src/components/data-table/data-table.ts
  • src/components/data-table/styles.css
  • src/components/data-table/define.ts
  • docs/components/data-table.md
  • site/examples/data-table.ts
  • tests/browser/data-table.spec.ts
  • evidence/components/data-table.md

API

Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.

Attributes

Attributes
AttributePropertyTypeDefaultNotes
default-page-sizedefaultPageSizenumber10Kebab-case attribute; camelCase property.
default-view-modedefaultViewModeDataTableViewMode'auto'Kebab-case attribute; camelCase property.
empty-description———Recorded in the catalog as an attribute.
empty-title———Recorded in the catalog as an attribute.
errorerrorstring | nullnullKebab-case attribute; camelCase property.
is-loadingisLoadingbooleanfalsePresence semantics: present means true, absent means false.
search-placeholdersearchPlaceholderstring'Search records...'Kebab-case attribute; camelCase property.
titletitlestringundefinedKebab-case attribute; camelCase property.

Properties

Properties
PropertyTypeDefaultNotes
columnsColumnDef<T>[][]Property-only: assign from script; not accepted as an attribute.
dataT[][]Property-only: assign from script; not accepted as an attribute.
getRowId(row: T, index: number) => string(row, index) => row.id || `row-${index}`Property-only: assign from script; not accepted as an attribute.
pageSizeOptions——Recorded in the catalog without a top-level type; pinned from source when the row is ported.
renderSubComponent(row: T) => ReactNodeundefinedProperty-only: assign from script; not accepted as an attribute.
onBulkDelete(selectedIds: string[]) => voidundefinedProperty-only: assign from script; not accepted as an attribute.
onBulkExport(selectedIds: string[]) => voidundefinedProperty-only: assign from script; not accepted as an attribute.

Events

Events
EventUpstream propPayload typeNotes
detent-bulk-deleteonBulkDelete(selectedIds: string[]) => voidComposed, bubbles; fires only for a user action.
detent-bulk-exportonBulkExport(selectedIds: string[]) => voidComposed, bubbles; fires only for a user action.

Slots

No slots are recorded for this component.

CSS customisation

  • React `children`/`ReactNode` props become DOM slots; caller content stays light DOM.
  • `className` prop is replaced by documented CSS parts/custom properties; no Tailwind in consumers.
  • React callback props become typed composed CustomEvents; programmatic state assignment emits none.
  • Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
  • React state/hooks replaced by element-owned state with pre-upgrade property preservation.
  • The compound exports (DataTable, useDataTable, DataTableToolbar, DataTableFilters, DataTableContent, DataTablePagination) ship as parts of the single detent-data-table tag, because PC-2.1 is one-to-one and the catalog row records one exportName/tag.
  • data, columns, pageSizeOptions and renderSubComponent are properties, not attributes; nothing is parsed from JSON. onBulkDelete/onBulkExport become the composed events detent-bulk-delete/detent-bulk-export, and programmatic writes emit none (PC-6.5).
  • renderSubComponent/columns[].cell return a Node or a string rendered as text. Upstream returned ReactNode; a custom element never parses a consumer string as markup (R-1.1, R-1.3).
  • defaultViewMode reflects as default-view-mode; auto selects cards below 640px and a semantic table at 640px or wider, and viewport resize reapplies auto. A manual choice persists through resize for explicit modes, but auto reapplies its breakpoint choice.
  • getRowId is a property-only callback. The native fallback uses row.id or the original data index; duplicate or invalid IDs warn once and fall back to unique source-index IDs.
  • Stable source-index fallback IDs intentionally improve on upstream's page-local getRowId index, which could change identity when a row moved between pages.
  • A search term now returns to page 1. Upstream kept the current page, which could show an empty page even though rows matched.
  • The page-size <select> always offers the configured defaultPageSize. Upstream rendered a blank selection when that value was not in pageSizeOptions.
  • The bulk bar renders both Export and Delete. Upstream rendered each only when the matching callback prop existed; a custom element cannot detect listeners reliably, so the consumer decides what to do with the event.
  • Rows are never mutated: Delete reports selectedIds and leaves data alone (the row's features describe bulk actions, not local deletion).
  • lucide-react icons become local text glyphs marked aria-hidden. SPRING_PHYSICS (300/32/0.6) drives the bulk bar's enter/exit height and opacity and an expanded drawer's entry on the shared spring helper; collapsing a drawer stays instant, as upstream (no AnimatePresence). The table row's chevron turns 90 degrees on SPRING_PHYSICS through the shared spring helper (the card view's text Expand/Collapse button no longer rotates, which a shared CSS rule had done); the motion.button press feedback remains a CSS transition. Reduced motion jumps the reveals with no frame and removes the pulse.
  • Upstream's semi-transparent palette (bg-surface/50 + border/70) is replaced by the shared theme tokens so both colour schemes meet AA.

What it does

A data table with three-state column sorting, global search, faceted filter chips, per-page selection with a bulk-action bar, expandable rows, pagination with a page-size dropdown and first/last jumps, a column-visibility menu, and skeleton/error/empty states.

Upstream behaviour
  • Multi-state column sorting with clean directional indicator icons
  • Responsive automatic table/cards views with a user-controlled toggle
  • Global live search with instant filtering across all data fields
  • Faceted filter chips with multi-selection and reset counters
  • Indeterminate select-all checkbox, row selection, and animated bulk actions drawer
  • Smooth Framer Motion expandable rows for nested data and telemetry inspection
  • Configurable pagination with page size dropdown and first/last page jump buttons
  • Dynamic column visibility toggle menu to hide or show columns on demand
  • Polished skeleton loading and informative empty state with clear filters trigger
Differences from upstream
  • React `children`/`ReactNode` props become DOM slots; caller content stays light DOM.
  • `className` prop is replaced by documented CSS parts/custom properties; no Tailwind in consumers.
  • React callback props become typed composed CustomEvents; programmatic state assignment emits none.
  • Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
  • React state/hooks replaced by element-owned state with pre-upgrade property preservation.
  • The compound exports (DataTable, useDataTable, DataTableToolbar, DataTableFilters, DataTableContent, DataTablePagination) ship as parts of the single detent-data-table tag, because PC-2.1 is one-to-one and the catalog row records one exportName/tag.
  • data, columns, pageSizeOptions and renderSubComponent are properties, not attributes; nothing is parsed from JSON. onBulkDelete/onBulkExport become the composed events detent-bulk-delete/detent-bulk-export, and programmatic writes emit none (PC-6.5).
  • renderSubComponent/columns[].cell return a Node or a string rendered as text. Upstream returned ReactNode; a custom element never parses a consumer string as markup (R-1.1, R-1.3).
  • defaultViewMode reflects as default-view-mode; auto selects cards below 640px and a semantic table at 640px or wider, and viewport resize reapplies auto. A manual choice persists through resize for explicit modes, but auto reapplies its breakpoint choice.
  • getRowId is a property-only callback. The native fallback uses row.id or the original data index; duplicate or invalid IDs warn once and fall back to unique source-index IDs.
  • Stable source-index fallback IDs intentionally improve on upstream's page-local getRowId index, which could change identity when a row moved between pages.
  • A search term now returns to page 1. Upstream kept the current page, which could show an empty page even though rows matched.
  • The page-size <select> always offers the configured defaultPageSize. Upstream rendered a blank selection when that value was not in pageSizeOptions.
  • The bulk bar renders both Export and Delete. Upstream rendered each only when the matching callback prop existed; a custom element cannot detect listeners reliably, so the consumer decides what to do with the event.
  • Rows are never mutated: Delete reports selectedIds and leaves data alone (the row's features describe bulk actions, not local deletion).
  • lucide-react icons become local text glyphs marked aria-hidden. SPRING_PHYSICS (300/32/0.6) drives the bulk bar's enter/exit height and opacity and an expanded drawer's entry on the shared spring helper; collapsing a drawer stays instant, as upstream (no AnimatePresence). The table row's chevron turns 90 degrees on SPRING_PHYSICS through the shared spring helper (the card view's text Expand/Collapse button no longer rotates, which a shared CSS rule had done); the motion.button press feedback remains a CSS transition. Reduced motion jumps the reveals with no frame and removes the pulse.
  • Upstream's semi-transparent palette (bg-surface/50 + border/70) is replaced by the shared theme tokens so both colour schemes meet AA.
Previous: Activity Heatmap Next: AI Agent Activity

Docs

Components

Project