ClickorKeyboard shortcut Alt + L

Dragto resize

Nord Design System SearchDeveloperDeveloperGetting startedUsing Web ComponentsWebfontsCDNLocalizationCSS Framework Legacy ESLint PluginWorking with AIFAQDesignDesignGetting startedFoundationsAccessibilityColor SystemColor UtilitiesPrinciplesFigma ToolkitGridIconographyNamingNordhealth BrandTypographyThemesBrand assetsToolsComponentsComponentsAccordion New Aside New Autocomplete New AvatarBadgeBanner Updated ButtonButton GroupCalendar Updated Card Updated Chart New Checkbox Updated Collapsible New Combobox New Command MenuData Table New Date Picker Updated Date Range Picker New DividerDrawer Updated DropdownEmpty StateField New FieldsetFilter Bar New FooterHeaderIconInput Updated Input Group New Item New Kbd New LayoutMessageMeter New Modal Updated NavigationNotificationNumber Field New Otp Field New Outline New Overflow List New Pagination New Popout Updated ProgressProgress BarQrcodeRadio Updated Range Updated Resizable New Rich Text Editor New Scroll Area New Segmented ControlSelect Updated SkeletonSpinnerStackTabTableTagTextarea Updated Time Picker New Timestamp New ToastToggle Updated TooltipTop BarTruncate New Visually HiddenDesign TokensDesign TokensTailwind CSSTailwind CSSBlocksBlocksTemplatesTemplatesIconsIconsPlaygroundPlaygroundLatest UpdatesLatest UpdatesChangelogChangelogMigration guidesMigration guides
GitHub

Clickto expandKeyboard shortcut Alt + L

Data Table New Alpha

Open in Storybook

Renders tabular data from a TanStack column model with sorting, selection, pagination and filtering — your app owns the data, Nord owns the table.

This component is considered alpha. We're still finalising the API, so it may include a breaking change at any time — use it at your own risk.
Loading...

Usage

Data Table renders tabular data from a TanStack Table column model and adds sorting, row selection, pagination and a declarative filter bar on top of Table. It is controlled and headless: you feed it columns, data and (optionally) state, and it emits a *-change event whenever the user interacts. It owns no data fetching, serialization or persistence — your app does that and feeds the next page of rows back in.

Use it when you have a list/collection screen and want sorting, filtering, selection and paging without re-implementing the table chrome each time. For a static or hand-authored table, use Table directly; for just the filter toolbar, see Data Table Filter Bar.

Import the component — this registers the custom element:

import "@nordhealth/components/lib/DataTable"

Then give it a caption (for accessibility), a column model and a page of rows. Everything is set as a JS property, not an attribute, because columns and data are objects/arrays:

The headless contract

Data Table never fetches, sorts, filters or paginates your data behind your back. The contract is one-way and explicit:

  1. You provide columns, the current page of data, and (optionally) the controlled state.
  2. The user interacts — clicks a sort header, ticks a checkbox, changes a filter, moves a page.
  3. The table emits an event describing the new intent (sorting-change, pagination-change, …) — it does not silently mutate anything you own.
  4. Your app reacts — updates its query state and/or URL, fetches the matching rows, and assigns the new data (and state) back onto the element.

In client mode (the default) the table also keeps its own internal copy of any state slice you leave uncontrolled, and runs TanStack's client-side sort/filter/paginate row models over the data you gave it — handy for small, already-loaded datasets and demos. In server mode (manual) those client row models are skipped entirely: the table renders exactly the data you pass and relies on you to do the sorting, filtering and paging on the server in response to its events.

<!-- Server-driven: render exactly the rows we pass, emit intent only -->
<nord-data-table manual row-count="240"></nord-data-table>

Because the events carry intent (the new SortingState, the new PaginationState, …) rather than resolved rows, the same event handlers work whether you resolve them client-side against an in-memory array or turn them into an API request. That is what makes the component equally at home in a small settings screen and a server-paged, hundreds-of-thousands-of-rows list.

Composition

Data Table is a single custom element that assembles several Nord components into one collection view. You typically place it inside a Card so it sits on a padded surface with a heading. Internally it embeds the Data Table Filter Bar as a toolbar, a Table for the rows, and a Pagination control beneath — all wired to the same TanStack table instance so state stays in sync.

nord-card
└── nord-data-table
    ├── nord-data-table-filter-bar   (toolbar — from `filters`)
    ├── nord-table                    (header + rows — from `columns` + `data`)
    │   └── <table> / <thead> / <tbody> / <tfoot>
    └── nord-pagination               (paginator — from `pagination`)
<nord-card padding="none">
  <h2 slot="header">Patients</h2>
  <nord-data-table caption="Patients" id="patients"></nord-data-table>
</nord-card>

<script type="module">
  import "@nordhealth/components/lib/DataTable"
  const table = document.querySelector("#patients")
  table.columns = [
    { accessorKey: "id", header: "ID", cell: info => info.getValue() },
    { accessorKey: "name", header: "Name", cell: info => info.getValue() },
    { accessorKey: "species", header: "Species", cell: info => info.getValue() },
  ]
  table.data = [
    { id: 101, name: "Bella", species: "Dog" },
    { id: 102, name: "Milo", species: "Cat" },
  ]
</script>

Card

Card is the surrounding surface. It is not required, but it gives the table a heading, padding and a visual container. Use padding="none" so the table's own chrome (filter bar, rows, paginator) sits flush against the card edges, and put the screen title in the card's header slot.

Data Table

Data Table is the orchestrator. It builds and owns the TanStack table instance, renders the parts below, forwards state between them, and emits the *-change events. Almost all configuration is set as JS properties on this one element — columns, data, filters, pagination, state — and the sub-parts are generated from them.

Data Table Filter Bar

Data Table Filter Bar is the toolbar rendered above the rows from the filters descriptor list. Each descriptor maps to a Nord filter control (search, dropdown, date range, boolean, segmented, tag group, or a custom slot). It emits filters-change and filter-reset. Opt out with no-filter-bar and supply your own via the filter-bar slot, or add extra controls beside it with the toolbar-end slot.

Table

Table renders the actual header, body and footer from columns and data. Data Table forwards density, striped and hide-header straight through to it, and layers sorting affordances, selection checkboxes, sizing presets and sticky headers on top. Cell renderers in your column model compose other Nord components (badges, links, dropdowns) into the cells.

Pagination

Pagination is the paginator rendered beneath the rows from the pagination config. It emits pagination-change intent. Opt out with no-pagination and provide your own via the pagination slot, or drop it entirely in favour of infinite-scroll / manual-infinite-scroll.

Examples

Overview

The canonical table: accessor columns, a numeric preset, plain-string cells, a Nord badge cell and a display (no-accessor) actions column.

Cell renderers

Cells can return a string or any DOM node, so you can compose links, badges, formatted dates and an actions dropdown. Ready-made helpers are exported for framework code that can render Lit templates:

  • Presentation: textCell, badgeCell, linkCell, emailCell (mailto:), phoneCell (tel:), actionsCell.
  • Formatting (locale/currency injectable — never hard-coded): numberCell, currencyCell, percentCell, dateCell all take consumer-supplied Intl options.
  • Server HTML: htmlCell renders a trusted HTML string, or set meta: { html: true } on a column to render its value as HTML with no cell function (server-rendered / Django tables).

For sizing, set meta.cellType to a preset (text, number, date, id/smallId/longId, actions, selection, icon, wide). Set meta.textAlign once on the column definition; the same alignment is applied to its header (including the sort control), body cells and footer.

Sorting

Mark columns sortable in the model and the headers become real <button>s with aria-sort. In client mode the table sorts its own row model; listen to sorting-change to mirror the sort into your URL or a server query.

Multi-sort

Shift-click additional headers to sort by more than one column at once. The sorting-change detail is the ordered SortingState array of every active sort.

Row selection

Add enable-row-selection for a select-all / per-row checkbox column. Selection is keyed by getRowId, and each change emits row-selection-change with the selection map.

Bulk actions

While rows are selected, a header-row takeover shows the selected count, a selection-options dropdown (select all on page / deselect all), and your bulk actions. Compose the actions by placing a <nord-data-table-bulk-actions> element inside the table with your action buttons; the table renders it in the takeover row. The toolbar stays pinned to the visible start edge while the table scrolls horizontally, so the actions remain reachable in wide tables.

<nord-data-table id="patients" enable-row-selection>
  <nord-data-table-bulk-actions>
    <nord-button size="s">Export</nord-button>
    <nord-button size="s" variant="danger">Delete</nord-button>
  </nord-data-table-bulk-actions>
</nord-data-table>

Single select

Use enable-row-single-selection for radio-style selection where picking a row replaces the previous one and no select-all control is shown.

Filtering

Provide a filters descriptor list to render the embedded filter bar. Changing a filter emits filters-change with the full value map so you can re-fetch the matching rows. The reset-filters button only appears once at least one filter holds a non-default value.

All filter types

The filter bar supports search, dropdown, date range, boolean, segmented, tag group and custom filters — one descriptor per control.

Client pagination

Pass a pagination config and, in client mode, the table paginates its own row model. Page changes emit pagination-change.

Server-side pagination

Pair manual with row-count (the total number of server-side rows) and resolve pagination-change into an API request, assigning the returned page back onto data.

Infinite scroll

Instead of a numbered paginator, use infinite-scroll (client) or manual-infinite-scroll (server) to advance/append rows as the body scrolls near the bottom. The table also emits reached-bottom.

Tree expansion

Provide getSubRows to render nested child rows with an expand/collapse control. Expansion state emits expanded-change.

Master / detail

Provide renderSubRow to render a full-width detail panel beneath an expanded row — useful for inline detail views without navigating away.

Custom rows

Column meta hints (bodyColspan, bodySkipByColspan) let a row span cells for grouped or summary presentations.

Column pinning

Pin columns to the start or end so they stay visible while the rest of the table scrolls horizontally. Pinned columns cast a shadow at the scroll boundary.

Column sizing

A column's width comes from its explicit size, then a meta.cellType preset (selection, expand, icon, actions, number, date, text, wide), then TanStack's computed size.

Column resizing

Add enable-column-resizing for drag-to-resize handles in the header. Set enableResizing: false on an individual column definition to keep that column fixed. Sizing changes emit column-sizing-change.

Columns menu

Add columns-menu for a built-in column-visibility menu in the toolbar; add enable-column-reorder for drag-to-reorder handles. These emit column-visibility-change and column-order-change.

Add sticky-header so the header row stays pinned while the body scrolls. Set sticky-header-offset to clear fixed app chrome.

Loading skeleton

While loading with no data yet, the table renders skeleton-rows placeholder rows instead of an empty overlay, so layout does not jump when data arrives.

Empty and no-results

The empty state differs by cause: no data at all uses empty-message (or the empty slot), while active filters returning nothing show a distinct "no results" state with a clear-filters affordance.

Row errors

Set rowErrors (keyed by row id) to render a danger banner beneath each affected row listing its validation messages — useful for inline-edit failures.

Column footers

Give any visible column a footer and the table renders a <tfoot> totals row — handy for sums and counts.

Clickable rows

Set isRowClickable to gate which rows are activatable. Clickable rows emit row-click on click, Enter or Space with the row id and original data.

Server-side complete

A complete server-driven collection view: manual mode with server sorting, filtering and pagination wired together through the events.

API reference

Properties

None of these are attributes — set them with element.columns = … (or .columns=${…} / :columns in a framework). TanStack Table types are re-exported from @nordhealth/components, so you can import them without adding @tanstack/table-core yourself:

import type {
  ColumnDef,
  SortingState,
  PaginationState,
  RowSelectionState,
  ExpandedState,
  Row,
  TableState,
} from "@nordhealth/components"
import type { DataTablePaginationConfig, FilterDescriptor } from "@nordhealth/components"
PropertyTypeDescription
columnsColumnDef<TData, any>[]TanStack column definitions. Each cell returns a Lit template, string, or DOM node.
dataTData[]The current page of rows. In client mode the full dataset to sort/filter/paginate locally; in manual mode exactly the rows to display.
statePartial<TableState>Controlled state (sorting, pagination, rowSelection, columnFilters, expanded, …). Any slice left out is tracked internally.
filtersFilterDescriptor[]Declarative config for the embedded filter bar.
filterValuesFilterValuesControlled value map for the embedded filter bar.
paginationDataTablePaginationConfigPaginator config: total, pageSize, pageSizeOptions, type (numbers / simple).
tableTanStackTable<TData>Escape hatch: render a consumer-built TanStack Table instance verbatim.
getRowId(row, index, parent?) => stringStable row id used by selection and expansion.
getSubRows(row) => TData[] | undefinedEnables tree expansion.
getRowCanExpand(row: Row<TData>) => booleanGates which rows can expand.
renderSubRow(row: Row<TData>) => TemplateResultFull-width detail panel for an expanded row (master/detail).
rowCountnumberTotal row count for server-side pagination (attribute row-count).
rowErrorsRecord<string, string[]>Per-row validation messages keyed by row id; each renders a danger banner.
isRowClickable(row: Row<TData>) => booleanGates which rows are clickable and emit row-click.
rowGroupingRowGroupingConfigGroup contiguous rows under collapsible group-header rows: { column, collapsible?, collapsedByDefault? }. Rows must arrive ordered by column.

Columns can also opt into rendering their value as trusted HTML with meta: { html: true } — for server-rendered tables (e.g. Django) whose cells arrive as pre-rendered HTML strings, with no cell function and no framework import. See Cell renderers.

Attributes

AttributeTypeDescription
captionstringAccessible caption (visually hidden, required).
manualbooleanServer-side mode: skip the client sort/filter/paginate row models.
densitycondensed / default / relaxedRow density, forwarded to the inner Table.
stripedbooleanZebra striping.
hide-headerbooleanHide the column header row.
loadingbooleanShow the loading overlay / skeleton.
empty-messagestringOverride the default empty-state message.
sticky-headerbooleanPin the header row while the body scrolls.
sticky-header-offsetnumber / autoTop offset (px) for the sticky header to clear fixed chrome.
skeleton-rowsnumberPlaceholder rows rendered while loading with no data (default 5).
no-filter-barbooleanOpt out of the embedded filter bar (use the filter-bar slot instead).
no-paginationbooleanOpt out of the embedded paginator (use the pagination slot instead).
infinite-scrollbooleanClient-side infinite scroll: auto-advance the page near the bottom.
manual-infinite-scrollbooleanServer-side infinite scroll: emit reached-bottom near the bottom.
enable-row-selectionbooleanAdd the multi-select select-all / per-row checkbox column.
enable-row-single-selectionbooleanRadio-style single-row selection (no select-all).
enable-select-all-matchingbooleanOffer a "Select all N matching" affordance when the page is fully selected and rowCount exceeds it; emits select-all-matching (intent only — you resolve the ids).
enable-column-resizingbooleanDrag-to-resize column handles in the header.
columns-menubooleanRender the built-in column-visibility menu in the toolbar.
enable-column-reorderbooleanAdd drag-to-reorder handles to the columns menu.

Events

Every interaction emits an unprefixed event whose detail is the new value for that state slice. None of these change what the table renders by themselves — re-render by assigning new data/state in response.

EventdetailFires when
sorting-changeSortingStateA sortable header is toggled.
pagination-changePaginationStateThe page or page size changes.
column-filters-changeColumnFiltersStateColumn filters change.
global-filter-changeunknownThe global filter value changes.
row-selection-changeRowSelectionStateRows are selected or deselected.
select-all-matching{ total }The user chose "Select all N matching" across pages (opt-in via enable-select-all-matching). Resolve the ids server-side and apply the selection.
expanded-changeExpandedStateRows are expanded or collapsed.
column-visibility-changeVisibilityStateColumn visibility changes.
column-order-changeColumnOrderStateColumn order changes.
column-pinning-changeColumnPinningStateColumn pinning changes.
column-sizing-changeColumnSizingStateColumn sizing changes.
state-changeTableStateAny state slice changes (fully-controlled escape hatch).
row-click{ id, row }A clickable row is activated (click / Enter / Space).
reached-bottomThe body is scrolled near the bottom (infinite scroll).

The embedded filter bar additionally emits filters-change (with { values, changed }), filter-reset and filter-add. See Data Table Filter Bar for details.

Slots & override renderers

This element is light-DOM, so consumer content placed with slot="…" (or the dedicated <nord-data-table-bulk-actions> element) is captured and re-projected into the rendered table.

SlotDescription
bulk-actions (or <nord-data-table-bulk-actions>)Actions shown in the header-row takeover while rows are selected.
toolbar-endExtra controls beside the embedded filter bar.
filter-barReplaces the embedded filter bar (pair with no-filter-bar).
paginationReplaces the embedded paginator (pair with no-pagination).

For the in-grid states, use the render-prop properties instead of slots:

PropertyDescription
renderEmpty() => TemplateResult — custom empty state (replaces the default message + clear-filters action).
renderLoading() => TemplateResult — custom loading overlay (replaces the default spinner).
renderSubRow(row) => TemplateResult — master/detail content for an expanded row.

Framework-owned rows

<nord-data-table> is the ready-made Lit renderer. In React or Vue applications that need native JSX, render functions or scoped slots for cells, let the framework own the semantic <table> markup instead. Use the framework's TanStack adapter (@tanstack/react-table or @tanstack/vue-table) with the same column metadata, sizing presets and localization exported by @nordhealth/components.

Load the reusable class stylesheet once, put .n-data-table on the nord-table shell, and use the n-data-table-* classes on sorting, resizing and state affordances:

import "@nordhealth/components/lib/Table"
import "@nordhealth/components/lib/data-table.css"
import {
  applyColumnSizingDefaults,
  CELL_TYPE_SIZES,
  getColumnSize,
} from "@nordhealth/components"

Pass applyColumnSizingDefaults(columns) to the framework adapter before creating the table so meta.cellType presets are installed before TanStack resolves its 150px fallback.

<nord-table class="n-data-table">
  <table><!-- React or Vue owns thead, tbody, rows and cells. --></table>
</nord-table>

This is the “own your rows on the core” pattern:

  • Shared core: TanStack column definitions, ColumnMeta, sizing presets, filter descriptors and localization.
  • React: useReactTable + flexRender; cells are ordinary JSX/render props.
  • Vue: useVueTable + FlexRender; cells are ordinary Vue templates, scoped slots or VNodes.
  • Lit or no-framework consumers: <nord-data-table> remains the complete config-driven renderer.

The repository's React and Vue playgrounds contain side-by-side framework-owned and raw Lit examples. Framework renderers should consume these shared primitives rather than re-deriving column sizes or copying component CSS.

Renderer and application boundary

Every renderer stops at presentation and user intent. Application workflows stay in the consuming application:

ResponsibilityOwner
Render headers, rows, cells, loading/empty/error statesRenderer
Emit sorting, filtering, pagination and selection intentRenderer
Data fetching and request cancellationApplication
API request/response mappingApplication
URL sync and navigation stateApplication
User preference persistence and saved viewsApplication
CSV/PDF/print export and other domain actionsApplication

This boundary is the same for the Lit, React, Vue and plain-JavaScript renderers. A renderer emits the next state; the application updates its URL or saved state, performs fetching, and supplies the resulting rows.

Usage with plain JS (jQuery / Django)

Data Table is a standard custom element, so it works anywhere you can run JavaScript — no framework required. Render the empty element from your template, then set its properties from a script and listen for events to re-fetch. This pattern fits a Django template that ships JSON in a <script type="application/json"> tag, or a jQuery-driven page.

The Django adapter belongs in the application, not in the design-system package. Provet's legacy aoData parameters, iSortCol_N visual-to-data column remapping, { data, recordsTotal, recordsFiltered } responses, filter serialization and saved-filter endpoints are application contracts. Keep that adapter beside the Django API and translate its result into the renderer's .data and rowCount properties. The design system intentionally contains no Provet endpoint, persistence or legacy-wire knowledge.

<nord-data-table caption="Patients" id="patients" manual row-count="0"></nord-data-table>

<script id="patients-data" type="application/json">
  {{ patients_json|safe }}
</script>

<script type="module">
  import "@nordhealth/components/lib/DataTable"

  const table = document.querySelector("#patients")

  table.columns = [
    { accessorKey: "id", header: "ID", cell: info => info.getValue(), meta: { textAlign: "end", cellType: "number" } },
    { accessorKey: "name", header: "Name", cell: info => info.getValue() },
    { accessorKey: "species", header: "Species", cell: info => info.getValue() },
  ]

  table.filters = [
    { type: "search", key: "q", label: "Search", placeholder: "Search patients" },
  ]

  // Initial page rendered by Django.
  const initial = JSON.parse(document.getElementById("patients-data").textContent)
  table.data = initial.results
  table.rowCount = initial.count

  // Re-fetch whenever the user sorts, pages, or filters. A new intent aborts
  // the previous request so a slow response cannot overwrite newer rows.
  let pending
  async function reload(params) {
    pending?.abort()
    pending = new AbortController()
    const res = await fetch("/api/patients/?" + new URLSearchParams(params), {
      signal: pending.signal,
    })
    const page = await res.json()
    table.data = page.results
    table.rowCount = page.count
  }

  let query = {}
  table.addEventListener("sorting-change", e => {
    const [sort] = e.detail
    query = { ...query, ordering: sort ? (sort.desc ? "-" : "") + sort.id : "" }
    reload(query)
  })
  table.addEventListener("pagination-change", e => {
    query = { ...query, page: e.detail.pageIndex + 1, page_size: e.detail.pageSize }
    reload(query)
  })
  table.addEventListener("filters-change", e => {
    query = { ...query, ...e.detail.values, page: 1 }
    reload(query)
  })
</script>

With jQuery the only difference is how you reach the element and bind events — the property/event contract is identical:

const table = $("#patients")[0]
table.columns = columns
table.data = rows

$(table).on("sorting-change", e => {
  reload({ ordering: toOrdering(e.originalEvent.detail) })
})

Because columns and data are object/array properties, always assign them with JS (table.columns = …) — they cannot be passed as HTML attributes.

Usage with Vue

In Vue, bind the object/array properties with .prop (or v-bind of a ref) and listen for the kebab-case events with @. Vue assigns non-string bindings as DOM properties automatically when the value is non-primitive, but .prop makes the intent explicit. The TanStack types come straight from @nordhealth/components.

<script setup lang="ts">
import { ref } from "vue"
import { badgeCell, type BadgeVariant } from "@nordhealth/components"
import type { ColumnDef, SortingState, PaginationState } from "@nordhealth/components"
import "@nordhealth/components/lib/DataTable"

interface Patient {
  id: number
  name: string
  species: string
  status: "active" | "inactive"
}

const variant: Record<Patient["status"], BadgeVariant> = {
  active: "success",
  inactive: "warning",
}

const columns: ColumnDef<Patient, any>[] = [
  { accessorKey: "name", header: "Name", cell: info => info.getValue() },
  { accessorKey: "species", header: "Species", cell: info => info.getValue() },
  {
    accessorKey: "status",
    header: "Status",
    cell: info => badgeCell(info.getValue(), variant[info.getValue() as Patient["status"]]),
  },
]

const rows = ref<Patient[]>([])
const total = ref(0)
const state = ref({ sorting: [] as SortingState, pagination: { pageIndex: 0, pageSize: 20 } as PaginationState })

async function reload() {
  const res = await fetch(`/api/patients/?${new URLSearchParams({
    page: String(state.value.pagination.pageIndex + 1),
    page_size: String(state.value.pagination.pageSize),
  })}`)
  const page = await res.json()
  rows.value = page.results
  total.value = page.count
}

function onSorting(e: CustomEvent<SortingState>) {
  state.value = { ...state.value, sorting: e.detail }
  reload()
}

function onPagination(e: CustomEvent<PaginationState>) {
  state.value = { ...state.value, pagination: e.detail }
  reload()
}

reload()
</script>

<template>
  <nord-data-table
    caption="Patients"
    manual
    :row-count="total"
    .columns="columns"
    .data="rows"
    .state="state"
    @sorting-change="onSorting"
    @pagination-change="onPagination"
  />
</template>

If you use @vue/compiler-dom's custom-element handling (isCustomElement), the .columns / .data / .state bindings forward to DOM properties so the table gets real objects, not stringified attributes.

Accessibility

  • Always set a caption. It is rendered as a visually-hidden <caption> so screen-reader users hear what the table contains, while sighted users rely on the surrounding heading.
  • The host has role="region", so screen-reader users can jump to the table as a landmark; give it a heading or aria-label if a page has more than one.
  • Sortable headers are real <button>s with aria-sort reflecting the current direction, so the sort state is announced.
  • The selection checkboxes have accessible names (localised "Select row" / "Select all rows"), and the select-all control reports an indeterminate state when only some rows on the page are selected.
  • Clickable rows are keyboard-operable (Enter / Space) and expose the row action to assistive technology; only rows allowed by isRowClickable are focusable and activatable.
  • The loading overlay uses a labelled Spinner; the empty and no-results states are plain, localisable text you can override via empty-message or the empty slot.
  • All visible strings — sort labels, selection labels, expand/collapse, resize/reorder handles, pagination controls, and loading/empty text — are localised through the design system's localization, so set the document language and provide translations as usual.

API reference

DataTable

Data Table renders tabular data from a TanStack column model with sorting, selection, pagination and filtering. It is **controlled and headless**: feed it columns, data and state and react to its *-change events — it owns no data fetching, serialization or persistence. Filters and pagination are declared as config (filters, pagination) and rendered by the table. Consumer content for the slots below is captured from light-DOM slot="…" children and re-projected into the rendered table (this element is light-DOM, so native <slot> projection isn't available).

<nord-data-table></nord-data-table>

Props

PropertyAttribute Description TypeDefault
rowCountrow-countTotal row count for server-side pagination.number | undefined
manualmanualServer-side mode: skip client sort/filter/paginate row models.booleanfalse
captioncaptionAccessible caption (required).string''
densitydensityRow density, forwarded to the inner nord-table.'condensed' | 'default' | 'relaxed''default'
stripedstripedZebra striping.booleanfalse
hideHeaderhide-headerHide the column header row.booleanfalse
loadingloadingShow the loading overlay.booleanfalse
emptyMessageempty-messageOverride the default empty-state message.string | undefined
stickyHeadersticky-headerPin the header row to the top of the scroll container while the body scrolls.booleanfalse
stickyHeaderOffsetsticky-header-offsetTop offset (px) for the sticky header. 'auto' keeps the header flush with the scroll container; a number sets an explicit inset-block-start so the header can clear a fixed app chrome.number | 'auto''auto'
skeletonRowsskeleton-rowsNumber of placeholder rows rendered while loading with no data yet.number5
noFilterBarno-filter-barOpt out of rendering the embedded filter bar.booleanfalse
noPaginationno-paginationOpt out of rendering the embedded paginator.booleanfalse
infiniteScrollinfinite-scrollClient-side infinite scroll: reveal the next page of rows automatically as the body is scrolled near the bottom (also emits reached-bottom). Mutually exclusive with a numbered paginator.booleanfalse
manualInfiniteScrollmanual-infinite-scrollServer-side infinite scroll: emit reached-bottom near the bottom so the consumer can append the next page. Does not advance the page itself.booleanfalse
enableRowSelectionenable-row-selectionEnable multi-select row checkboxes.booleanfalse
enableRowSingleSelectionenable-row-single-selectionEnable single-select row radios instead of checkboxes: selecting a row replaces any previous selection (at most one row selected at a time) and no select-all control is rendered. Implies row selection.booleanfalse
enableSelectAllMatchingenable-select-all-matchingOpt into a "select all N matching" affordance in the bulk-actions bar when every row on the current page is selected but more rows match than are on the page (rowCount exceeds the page). The component only emits select-all-matching with the total; the consumer resolves the ids.booleanfalse
enableColumnResizingenable-column-resizingEnable drag-to-resize column handles in the header (resize mode onChange).booleanfalse
columnsMenucolumns-menuRender the built-in column-visibility (and reorder) menu in the toolbar.booleanfalse
enableColumnReorderenable-column-reorderAdd drag-to-reorder handles to the columns menu (updates columnOrder).booleanfalse

Slots

Slot name Description
toolbar-endExtra controls beside the embedded filter bar.
bulk-actionsActions shown in the header-row takeover while rows are selected.
filter-barReplaces the embedded filter bar (pair with no-filter-bar).
paginationReplaces the embedded paginator (pair with no-pagination).
EventDetail TypeDescription
sorting-changeNordDataTableSortingChangeEventSorting changed.
pagination-changeNordDataTablePaginationChangeEventPagination changed.
column-filters-changeNordDataTableColumnFiltersChangeEventColumn filters changed.
global-filter-changeNordDataTableGlobalFilterChangeEventThe global (search) filter changed.
filters-changeNordDataTableFiltersChangeEventThe embedded filter bar committed a change (re-emitted from the table).
filter-resetNordDataTableFilterResetEventFilters were reset (via the filter bar or the empty-state clear action).
row-selection-changeNordDataTableRowSelectionChangeEventRow selection changed.
expanded-changeNordDataTableExpandedChangeEventExpanded rows changed.
row-clickNordDataTableRowClickEventA clickable row was activated.
reached-bottomNordDataTableReachedBottomEventThe table body was scrolled near the bottom (infinite scroll).

Parts

This component is made up of the following parts.

Data Table Bulk Actions

Actions for a Data Table's bulk-selection state. Place it as a direct child of nord-data-table and put your action buttons inside; the table captures it and renders it in the header-row takeover that appears while rows are selected (next to the "N selected" count). It's a light-DOM marker (styled display: contents by the parent table) — it hosts your controls without adding a box, so they sit inline in the takeover.

<nord-data-table-bulk-actions></nord-data-table-bulk-actions>

Slots

Slot name Description
Default slotThe bulk action controls (typically nord-buttons).

Data Table Filter Bar

Data Table Filter Bar renders a row of filter controls from a declarative filters config — the filter equivalent of a column model. Each descriptor maps to the matching nord-filter-* control; the bar tracks the value map and emits a single filters-change event. It owns no data fetching or serialization. Used embedded by Data Table, or standalone (bind to a table via for=).

<nord-data-table-filter-bar></nord-data-table-filter-bar>

Props

PropertyAttribute Description TypeDefault
alignalignCross-axis alignment, forwarded to nord-filter-bar.'start' | 'center' | 'end''center'
forforId of the <nord-data-table> this bar controls when used standalone.string | undefined
collapseBreakpointcollapse-breakpointInline-size below which segmented and tag filters use dropdown presentation.number640
EventDetail TypeDescription
search-changeCustomEventAn async dropdown search changed; detail contains its filter key and value.
load-moreCustomEventAn async dropdown requested another options page; detail contains its filter key.
filters-changeNordDataTableFiltersChangeEventA filter committed a change.
filter-resetNordDataTableFilterResetEventThe reset affordance was used.
filter-addNordDataTableFilterAddEventAn optional filter was added from the add-filter menu.

Dependencies

This component is internally dependent on the following components: