Data Table
<cw-data-table> | CwDataTable
Data tables display structured tabular data with support for sorting, row selection, and custom cell rendering.
<cw-data-table id="preview-table" caption="Team members"> <cw-data-table-column name="name" label="Name" sortable></cw-data-table-column> <cw-data-table-column name="role" label="Role"></cw-data-table-column> <cw-data-table-column name="status" label="Status" align="center"></cw-data-table-column> </cw-data-table> <script> const table = document.getElementById('preview-table'); table.data = [ { name: 'Alice Nguyen', role: 'Engineering Lead', status: 'Active' }, { name: 'Bob Okafor', role: 'Designer', status: 'Active' }, { name: 'Carol Smith', role: 'Product Manager', status: 'On Leave' }, { name: 'Dan Petrov', role: 'Engineer', status: 'Active' } ]; </script>
import CwDataTable from '@cordwainer/cw-elements/dist/react/data-table'; import CwDataTableColumn from '@cordwainer/cw-elements/dist/react/data-table-column'; const data = [ { name: 'Alice Nguyen', role: 'Engineering Lead', status: 'Active' }, { name: 'Bob Okafor', role: 'Designer', status: 'Active' }, { name: 'Carol Smith', role: 'Product Manager', status: 'On Leave' }, { name: 'Dan Petrov', role: 'Engineer', status: 'Active' } ]; const App = () => ( <CwDataTable data={data} caption="Team members"> <CwDataTableColumn name="name" label="Name" sortable /> <CwDataTableColumn name="role" label="Role" /> <CwDataTableColumn name="status" label="Status" align="center" /> </CwDataTable> );
Examples
Basic Usage
Pass an array of objects to the data property (this must be set via JavaScript — arrays cannot
be serialized as HTML attributes). Add <cw-data-table-column> children to define which
fields to display and how to label them.
<cw-data-table id="basic-table" caption="Inventory"> <cw-data-table-column name="product" label="Product"></cw-data-table-column> <cw-data-table-column name="category" label="Category"></cw-data-table-column> <cw-data-table-column name="stock" label="Stock" align="end"></cw-data-table-column> </cw-data-table> <script> document.getElementById('basic-table').data = [ { product: 'Widget Pro', category: 'Hardware', stock: 142 }, { product: 'Gadget Plus', category: 'Electronics', stock: 57 }, { product: 'Thingamajig', category: 'Hardware', stock: 0 } ]; </script>
Sorting
Add the sortable attribute to any column to make its header clickable. Clicking once sorts
ascending; clicking again sorts descending. The table sorts client-side by default.
<cw-data-table id="sort-table" caption="Employees"> <cw-data-table-column name="name" label="Name" sortable></cw-data-table-column> <cw-data-table-column name="department" label="Department" sortable></cw-data-table-column> <cw-data-table-column name="salary" label="Salary" align="end" sortable></cw-data-table-column> </cw-data-table> <script> document.getElementById('sort-table').data = [ { name: 'Carol Smith', department: 'Engineering', salary: 95000 }, { name: 'Alice Nguyen', department: 'Design', salary: 88000 }, { name: 'Bob Okafor', department: 'Engineering', salary: 102000 }, { name: 'Dan Petrov', department: 'Product', salary: 91000 } ]; </script>
Server-Side Sorting
Add server-sort to disable client-side reordering. The table will still emit
cw-sort-change — listen for it and provide freshly sorted data.
Last sort: —
<cw-data-table id="server-sort-table" caption="Orders" server-sort> <cw-data-table-column name="order" label="Order #" sortable></cw-data-table-column> <cw-data-table-column name="customer" label="Customer" sortable></cw-data-table-column> <cw-data-table-column name="total" label="Total" align="end"></cw-data-table-column> </cw-data-table> <p>Last sort: <span id="last-sort">—</span></p> <script> const serverSortTable = document.getElementById('server-sort-table'); const lastSort = document.getElementById('last-sort'); const allOrders = [ { order: 'ORD-001', customer: 'Acme Corp', total: '$1,200' }, { order: 'ORD-002', customer: 'Globex Inc', total: '$340' }, { order: 'ORD-003', customer: 'Acme Corp', total: '$870' } ]; serverSortTable.data = [...allOrders]; serverSortTable.addEventListener('cw-sort-change', event => { const { column, direction } = event.detail; lastSort.textContent = `${column} ${direction}`; serverSortTable.data = [...allOrders].sort((a, b) => { const cmp = String(a[column]).localeCompare(String(b[column]), undefined, { numeric: true }); return direction === 'asc' ? cmp : -cmp; }); }); </script>
Single Row Selection
Set selection="single" to allow clicking a row to select it. Click the same row again to
deselect. Always set row-id to the field that uniquely identifies each row.
Selected: None
<cw-data-table id="single-select-table" caption="Users" selection="single" row-id="id"> <cw-data-table-column name="name" label="Name"></cw-data-table-column> <cw-data-table-column name="email" label="Email"></cw-data-table-column> </cw-data-table> <p>Selected: <span id="single-select-output">None</span></p> <script> const singleTable = document.getElementById('single-select-table'); const singleOutput = document.getElementById('single-select-output'); singleTable.data = [ { id: 'u1', name: 'Alice Nguyen', email: 'alice@example.com' }, { id: 'u2', name: 'Bob Okafor', email: 'bob@example.com' }, { id: 'u3', name: 'Carol Smith', email: 'carol@example.com' } ]; singleTable.addEventListener('cw-row-selection-change', event => { const rows = event.detail.selectedRows; singleOutput.textContent = rows.length ? rows[0].name : 'None'; }); </script>
Multiple Row Selection
Set selection="multiple" to add a checkbox column. A select-all checkbox in the header selects
or deselects all currently visible rows.
Selected: 0 rows
<cw-data-table id="multi-select-table" caption="Tasks" selection="multiple" row-id="id"> <cw-data-table-column name="title" label="Task"></cw-data-table-column> <cw-data-table-column name="assignee" label="Assignee"></cw-data-table-column> <cw-data-table-column name="priority" label="Priority" align="center"></cw-data-table-column> </cw-data-table> <p>Selected: <span id="multi-select-output">0 rows</span></p> <script> const multiTable = document.getElementById('multi-select-table'); const multiOutput = document.getElementById('multi-select-output'); multiTable.data = [ { id: 't1', title: 'Design new logo', assignee: 'Alice', priority: 'High' }, { id: 't2', title: 'Write release notes', assignee: 'Bob', priority: 'Medium' }, { id: 't3', title: 'Fix login bug', assignee: 'Carol', priority: 'High' }, { id: 't4', title: 'Update dependencies', assignee: 'Dan', priority: 'Low' } ]; multiTable.addEventListener('cw-row-selection-change', event => { const count = event.detail.selectedRows.length; multiOutput.textContent = count === 1 ? '1 row' : `${count} rows`; }); </script>
Custom Cell Rendering
Set a renderCell function on any <cw-data-table-column> to control how that
column’s cells are rendered. The function receives the cell value, the full row object, and the column
element. Return a Lit html template result or a plain string.
<cw-data-table id="render-table" caption="Projects"> <cw-data-table-column name="name" label="Project"></cw-data-table-column> <cw-data-table-column id="status-col" name="status" label="Status" align="center"></cw-data-table-column> <cw-data-table-column id="progress-col" name="progress" label="Progress" align="end"></cw-data-table-column> </cw-data-table> <script type="module"> import { html } from '/dist/cw-elements.js'; const table = document.getElementById('render-table'); table.data = [ { name: 'Alpha Launch', status: 'complete', progress: 100 }, { name: 'Beta Testing', status: 'active', progress: 62 }, { name: 'v2 Planning', status: 'pending', progress: 10 } ]; const variantMap = { complete: 'success', active: 'primary', pending: 'neutral' }; document.getElementById('status-col').renderCell = value => html`<cw-badge variant=${variantMap[value] ?? 'neutral'}>${value}</cw-badge>`; document.getElementById('progress-col').renderCell = value => html`<cw-progress-bar value=${value} style="min-width:80px"></cw-progress-bar>`; </script>
Empty State
When data is an empty array and loading is not set, the table shows an empty-state
message. Use empty-text to customize the message or the empty slot for richer
content.
<cw-data-table caption="Search results" empty-text="No results match your search."> <cw-data-table-column name="name" label="Name"></cw-data-table-column> <cw-data-table-column name="email" label="Email"></cw-data-table-column> </cw-data-table> <script> document.querySelector('cw-data-table[caption="Search results"]').data = []; </script>
Loading State
Set loading to show a spinner overlay while data is being fetched. The table structure remains
visible underneath the overlay so layout doesn’t jump when data arrives.
<cw-data-table id="loading-table" caption="Reports" loading> <cw-data-table-column name="name" label="Report"></cw-data-table-column> <cw-data-table-column name="date" label="Date"></cw-data-table-column> </cw-data-table> <script> const loadingTable = document.getElementById('loading-table'); loadingTable.data = []; setTimeout(() => { loadingTable.data = [ { name: 'Q1 Summary', date: '2026-03-31' }, { name: 'Q2 Summary', date: '2026-06-30' } ]; loadingTable.loading = false; }, 2000); </script>
Striped Rows
Add the striped attribute for alternating row background colors, which can improve readability
for wide tables.
<cw-data-table id="striped-table" caption="Logs" striped> <cw-data-table-column name="time" label="Time"></cw-data-table-column> <cw-data-table-column name="level" label="Level" align="center"></cw-data-table-column> <cw-data-table-column name="message" label="Message"></cw-data-table-column> </cw-data-table> <script> document.getElementById('striped-table').data = [ { time: '10:00:01', level: 'INFO', message: 'Server started' }, { time: '10:00:05', level: 'INFO', message: 'Database connected' }, { time: '10:01:12', level: 'WARN', message: 'High memory usage' }, { time: '10:02:44', level: 'ERROR', message: 'Request timeout' }, { time: '10:03:01', level: 'INFO', message: 'Retry succeeded' } ]; </script>
Bordered
Add the bordered attribute to show vertical dividers between columns.
<cw-data-table id="bordered-table" caption="Comparison" bordered> <cw-data-table-column name="feature" label="Feature"></cw-data-table-column> <cw-data-table-column name="basic" label="Basic" align="center"></cw-data-table-column> <cw-data-table-column name="pro" label="Pro" align="center"></cw-data-table-column> <cw-data-table-column name="enterprise" label="Enterprise" align="center"></cw-data-table-column> </cw-data-table> <script> document.getElementById('bordered-table').data = [ { feature: 'Users', basic: '5', pro: '50', enterprise: 'Unlimited' }, { feature: 'Storage', basic: '10 GB', pro: '100 GB', enterprise: '1 TB' }, { feature: 'API access', basic: '—', pro: '✓', enterprise: '✓' }, { feature: 'SSO', basic: '—', pro: '—', enterprise: '✓' } ]; </script>
Pairing with Pagination
<cw-data-table> does not manage pagination itself. Use
<cw-pagination> alongside it and slice your data to the current page.
<cw-data-table id="paged-table" caption="Records"></cw-data-table-column> <cw-data-table-column name="id" label="#" align="end"></cw-data-table-column> <cw-data-table-column name="name" label="Name"></cw-data-table-column> <cw-data-table-column name="city" label="City"></cw-data-table-column> </cw-data-table> <cw-pagination id="paged-pagination" total-items="20" page-size="5" page="1" style="margin-top: var(--cw-spacing-medium)"></cw-pagination> <script> const allRows = Array.from({ length: 20 }, (_, i) => ({ id: i + 1, name: `Person ${i + 1}`, city: ['New York', 'London', 'Tokyo', 'Paris'][i % 4] })); const pagedTable = document.getElementById('paged-table'); const pagination = document.getElementById('paged-pagination'); function renderPage(page) { const start = (page - 1) * 5; pagedTable.data = allRows.slice(start, start + 5); } renderPage(1); pagination.addEventListener('cw-change', () => renderPage(pagination.page)); </script>
Hidden Columns
Set hidden on a <cw-data-table-column> to hide it without removing it from
the DOM. This is useful for toggling column visibility dynamically.
<div style="display:flex; gap: var(--cw-spacing-small); margin-bottom: var(--cw-spacing-small)"> <cw-checkbox id="toggle-email" checked>Show Email</cw-checkbox> <cw-checkbox id="toggle-phone" checked>Show Phone</cw-checkbox> </div> <cw-data-table id="hidden-col-table" caption="Contacts"> <cw-data-table-column name="name" label="Name"></cw-data-table-column> <cw-data-table-column id="email-col" name="email" label="Email"></cw-data-table-column> <cw-data-table-column id="phone-col" name="phone" label="Phone"></cw-data-table-column> </cw-data-table> <script> document.getElementById('hidden-col-table').data = [ { name: 'Alice', email: 'alice@example.com', phone: '555-0100' }, { name: 'Bob', email: 'bob@example.com', phone: '555-0101' } ]; document.getElementById('toggle-email').addEventListener('cw-change', e => { document.getElementById('email-col').hidden = !e.target.checked; }); document.getElementById('toggle-phone').addEventListener('cw-change', e => { document.getElementById('phone-col').hidden = !e.target.checked; }); </script>
Roadmap
<cw-data-table> ships as an MVP covering the most common use cases. The following
enhancements are planned for future releases:
-
Row size / density — a
sizeattribute (compact/comfortable) to control row height and cell padding, following the same convention as other components. -
Select-all-data vs. select-all-visible — the current select-all checkbox operates on the rows present in
data(the visible page). A future enhancement will expose anindeterminateselect-all state and a “Select all N records” affordance for paginated server-side datasets, similar to Gmail’s bulk-selection pattern. Consumers can approximate this today by handlingcw-row-selection-changeand programmatically settingselectedRows. -
Column pinning — a
pinned="start"orpinned="end"attribute on<cw-data-table-column>to freeze columns viaposition: sticky, keeping key fields visible when scrolling wide tables horizontally. -
Column resizing — drag handles on column header borders to let users resize individual columns.
-
Column reordering — drag-and-drop column header reordering.
-
Rich header slot — a
labelslot on<cw-data-table-column>for placing icons or tooltips inside column headers, complementing the currentlabelattribute. -
Row grouping — group rows by a shared field value under a collapsible group header row.
-
Expandable rows — a toggle on each row to reveal a detail panel (nested content, description, related records).
-
Editable cells — inline editing via a
<cw-input>or<cw-select>rendered inside a cell on click, emitting acw-cell-changeevent. -
Virtualized rendering — render only the rows visible in the viewport for very large datasets. Scheduled for v1.4 alongside virtual scrolling for
<cw-select>and<cw-tree>.
Importing
If you’re using the autoloader or the traditional loader, you can ignore this section. Otherwise, feel free to use any of the following snippets to cherry pick this component.
To import this component from the CDN using a script tag:
<script type="module" src="https://cdn.jsdelivr.net/npm/@cordwainer/cw-elements@1.2.3/cdn/components/data-table/data-table.js"></script>
To import this component from the CDN using a JavaScript import:
import 'https://cdn.jsdelivr.net/npm/@cordwainer/cw-elements@1.2.3/cdn/components/data-table/data-table.js';
To import this component using a bundler:
import '@cordwainer/cw-elements/dist/components/data-table/data-table.js';
To import this component as a React component:
import CwDataTable from '@cordwainer/cw-elements/dist/react/data-table';
Slots
| Name | Description |
|---|---|
| (default) | Place <cw-data-table-column> elements here to define columns. |
empty
|
Content shown when data is empty and not loading. |
Learn more about using slots.
Properties
| Name | Description | Reflects | Type | Default |
|---|---|---|---|---|
data
|
The array of row data objects to display. This is a property only and cannot be set as an attribute.
Each object’s keys correspond to column name values.
|
Record
|
[]
|
|
rowId
row-id
|
The key in each row object used as a stable row identity for selection. Falls back to row index when not set. |
string
|
''
|
|
selection
|
Controls row selection behavior. |
|
'none' | 'single' | 'multiple'
|
'none'
|
sortColumn
sort-column
|
The column currently used for sorting, identified by its name. |
|
string
|
''
|
sortDirection
sort-direction
|
The direction of the current sort. |
|
'asc' | 'desc'
|
'asc'
|
serverSort
server-sort
|
Disables client-side sorting. When set, the component emits cw-sort-change but does not
reorder data itself — the consumer is responsible for fetching/providing sorted data.
|
|
boolean
|
false
|
loading
|
Shows a loading spinner overlay over the table body. |
|
boolean
|
false
|
striped
|
Applies alternating row background colors. |
|
boolean
|
false
|
bordered
|
Adds borders between cells. |
|
boolean
|
false
|
caption
|
An accessible caption for the table, rendered as a visually hidden <caption>.
|
string
|
''
|
|
emptyText
empty-text
|
Text shown in the empty state when data is empty and loading is false.
|
string
|
'No data found'
|
|
selectedRows
|
Returns the currently selected row objects. Set this property to programmatically control selection. |
Record
|
- | |
updateComplete |
A read-only promise that resolves when the component has finished updating. |
Learn more about attributes and properties.
Events
| Name | React Event | Description | Event Detail |
|---|---|---|---|
cw-sort-change |
onCwSortChange |
Emitted when the user clicks a sortable column header.
detail: { column: string, direction: 'asc' | 'desc' }.
|
SortChangeEvent
|
cw-row-selection-change |
onCwRowSelectionChange |
Emitted when row selection changes. detail: { selectedRows: object[] }. |
CwRowSelectionChangeEvent
|
cw-row-click |
onCwRowClick |
Emitted when a body row is clicked. detail: { row: object, index: number }. |
RowClickEvent
|
Learn more about events.
Custom Properties
| Name | Description | Default |
|---|---|---|
--cw-data-table-border-color |
Border color used when bordered is set. Default:
var(--cw-color-neutral-200).
|
|
--cw-data-table-stripe-color |
Alternate row background color when striped is set. Default:
var(--cw-color-neutral-50).
|
|
--cw-data-table-row-hover-color |
Row background on hover. Default: var(--cw-color-neutral-100). |
|
--cw-data-table-selected-row-color |
Selected row background. Default: var(--cw-color-primary-50). |
|
--cw-data-table-header-background |
Header row background. Default: var(--cw-color-neutral-50). |
Learn more about customizing CSS custom properties.
Parts
| Name | Description |
|---|---|
base |
The outer scrollable wrapper. |
table |
The <table> element. |
header-row |
The <tr> in <thead>. |
header-cell |
Each <th> element. |
sort-button |
The clickable button inside a sortable <th>. |
sort-icon |
The sort direction icon inside a sortable <th>. |
body-row |
Each <tr> in <tbody>. |
body-row--selected |
Added to body rows that are selected. |
body-cell |
Each <td> element. |
checkbox-cell |
The <th> or <td> that holds the selection checkbox. |
loading |
The loading overlay shown when loading is true. |
empty |
The empty-state container shown when data is empty. |
Learn more about customizing CSS parts.
Dependencies
This component automatically imports the following dependencies.
<cw-checkbox><cw-icon><cw-spinner>