Data table <mv-data-table>
Full-featured data table around a .mv-table: column sorting, accent-insensitive search, value filters, a column visibility menu, row selection (indeterminate header checkbox, Shift + click for ranges) with a bulk-actions bar, pagination with rows per page, an empty state, a totals footer aligned on the columns, and rows that turn into stacked cards (title, subtitle, amount on the right) in containers narrower than 40rem.
How it works
Full-featured data table around a .mv-table: column sorting, accent-insensitive search, value filters, a column visibility menu, row selection (indeterminate header checkbox, Shift + click for ranges) with a bulk-actions bar, pagination with rows per page, an empty state, a totals footer aligned on the columns, and rows that turn into stacked cards (title, subtitle, amount on the right) in containers narrower than 40rem. Works on authored rows in the browser, or in mode="server" where the app loads each page from sort, search and page events and provides the total. Known limits: the card threshold is fixed at 40rem of container width; several subtitle or detail columns stack one per line; server mode selection spans pages only for rows with a data-id.
| Category | Blocks |
|---|---|
| Type | Web Component (<mv-data-table>) |
| Status | stable |
| Also installs | checkbox, popover, menu, pagination, table |
| Keywords | data-table, datagrid, table, sort, filter, search, pagination, selection, bulk-actions, columns, server-side, totals, tfoot, responsive, cards |
When to use
- Users must sort, search, filter and paginate a list of records in an admin or back-office screen
- Rows need multi-selection with Shift + click ranges and a bulk-actions bar
- Thousands of records live in an API and each page is fetched on demand, with the total known by the server
- An invoice or statement shows subtotal, tax and total under the amount column and must read well on a phone
Avoid when
- The data is a small static table that needs no selection or column controls → use Table instead
- Users scroll through tens of thousands of rows without paging; the table renders every row of a page in the DOM
Install
node scripts/add.mjs data-table --out ./src/marvelousAI agent with the Marvelous UI MCP server: install_components({ slugs: ["data-table"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Files copied (dependencies included): tokens/tokens.css, core/base.css, components/checkbox/checkbox.css, core/dom.js, core/element.js, core/focus.js, core/i18n.js, core/motion.js, core/position.js, core/svg.js, components/popover/popover.js, components/popover/popover.css, core/dismiss.js, components/menu/menu.js, components/menu/menu.css, components/pagination/pagination.js, components/pagination/pagination.css, components/table/table.css, components/table/table.js, components/data-table/data-table.js, components/data-table/data-table.css.
Usage
Canonical markup, to start from and customize with attributes, data-* and CSS variables:
<div id="dt-demo" style="width:min(100%,64rem);margin-inline:auto">
<mv-data-table page-size="8" label="Recent orders">
<div class="mv-data-table-toolbar">
<div class="mv-input-group" data-size="sm">
<label class="mv-input-group-addon" for="dt-demo-q"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg><span class="mv-sr-only">Search</span></label>
<input id="dt-demo-q" class="mv-input" type="search" placeholder="Search orders or customers…" data-dt-search autocomplete="off">
</div>
<select class="mv-select" data-size="sm" data-dt-filter="status" aria-label="Filter by status">
<option value="">All statuses</option>
<option value="paid">Paid</option>
<option value="pending">Pending</option>
<option value="shipped">Shipped</option>
<option value="refunded">Refunded</option>
<option value="failed">Failed</option>
</select>
<div class="mv-data-table-toolbar-end">
<mv-menu data-dt-columns placement="bottom-end">
<button class="mv-button" data-variant="outline" data-size="sm" type="button"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"/><path d="M9 4v16M15 4v16" opacity=".5"/></svg>Columns</button>
<div data-content style="--mv-menu-width:12rem"></div>
</mv-menu>
<button class="mv-button" data-size="sm" type="button" data-dt-demo-export><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12m-5-5 5 5 5-5M5 21h14"/></svg><span data-dt-label>Export</span></button>
</div>
</div>
<div class="mv-table-wrap">
<table class="mv-table">
<thead>
<tr>
<th scope="col" data-dt-select></th>
<th scope="col" data-key="id" data-sort="text" data-card="subtitle">Order</th>
<th scope="col" data-key="client" data-sort="text" data-card="title">Customer</th>
<th scope="col" data-key="status" data-sort="text" data-card="meta">Status</th>
<th scope="col" data-key="channel" data-sort="text" data-card="hide">Channel</th>
<th scope="col" data-key="date" data-sort="date" data-card="subtitle">Date</th>
<th scope="col" data-key="items" data-sort="number" data-align="end" data-card="hide">Items</th>
<th scope="col" data-key="amount" data-sort="number" data-align="end" data-card="amount">Amount</th>
<th scope="col" data-card="action"><span class="mv-sr-only">Actions</span></th>
</tr>
</thead>
<tbody>
<tr data-id="ORD-4821">
<td></td>
<td><span class="mv-data-table-mono">ORD-4821</span></td>
<td data-value="Camila Rossi"><div class="mv-data-table-person"><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:200" aria-hidden="true"><span class="mv-avatar-fallback">CR</span></span><div><b>Camila Rossi</b><small>[email protected]</small></div></div></td>
<td data-value="paid"><span class="mv-badge" data-variant="success" data-size="sm">Paid</span></td>
<td class="mv-table-muted">Online store</td>
<td data-value="2026-09-22" class="mv-table-muted"><time datetime="2026-09-22">Sep 22, 2026</time></td>
<td data-align="end" data-value="6">6</td>
<td data-align="end" data-value="111.83"><b>$111.83</b></td>
<td data-align="end"><button class="mv-button" data-variant="ghost" data-size="icon" type="button" aria-label="Actions for ORD-4821" data-dt-demo-row><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg></button></td>
</tr>
<tr data-id="ORD-4820">
<td></td>
<td><span class="mv-data-table-mono">ORD-4820</span></td>
<td data-value="Isabel Moreno"><div class="mv-data-table-person"><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:280" aria-hidden="true"><span class="mv-avatar-fallback">IM</span></span><div><b>Isabel Moreno</b><small>[email protected]</small></div></div></td>
<td data-value="paid"><span class="mv-badge" data-variant="success" data-size="sm">Paid</span></td>
<td class="mv-table-muted">Online store</td>
<td data-value="2026-09-22" class="mv-table-muted"><time datetime="2026-09-22">Sep 22, 2026</time></td>
<td data-align="end" data-value="3">3</td>
<td data-align="end" data-value="351.32"><b>$351.32</b></td>
<td data-align="end"><button class="mv-button" data-variant="ghost" data-size="icon" type="button" aria-label="Actions for ORD-4820" data-dt-demo-row><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg></button></td>
</tr>
<tr data-id="ORD-4819">
<td></td>
<td><span class="mv-data-table-mono">ORD-4819</span></td>
<td data-value="Kwame Boateng"><div class="mv-data-table-person"><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:160" aria-hidden="true"><span class="mv-avatar-fallback">KB</span></span><div><b>Kwame Boateng</b><small>[email protected]</small></div></div></td>
<td data-value="refunded"><span class="mv-badge" data-variant="secondary" data-size="sm">Refunded</span></td>
<td class="mv-table-muted">London store</td>
<td data-value="2026-09-21" class="mv-table-muted"><time datetime="2026-09-21">Sep 21, 2026</time></td>
<td data-align="end" data-value="1">1</td>
<td data-align="end" data-value="151.54"><b>$151.54</b></td>
<td data-align="end"><button class="mv-button" data-variant="ghost" data-size="icon" type="button" aria-label="Actions for ORD-4819" data-dt-demo-row><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg></button></td>
</tr>
<tr data-id="ORD-4818">
<td></td>
<td><span class="mv-data-table-mono">ORD-4818</span></td>
<td data-value="Sofia Nguyen"><div class="mv-data-table-person"><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:320" aria-hidden="true"><span class="mv-avatar-fallback">SN</span></span><div><b>Sofia Nguyen</b><small>[email protected]</small></div></div></td>
<td data-value="pending"><span class="mv-badge" data-variant="warning" data-size="sm">Pending</span></td>
<td class="mv-table-muted">London store</td>
<td data-value="2026-09-20" class="mv-table-muted"><time datetime="2026-09-20">Sep 20, 2026</time></td>
<td data-align="end" data-value="5">5</td>
<td data-align="end" data-value="167.69"><b>$167.69</b></td>
<td data-align="end"><button class="mv-button" data-variant="ghost" data-size="icon" type="button" aria-label="Actions for ORD-4818" data-dt-demo-row><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg></button></td>
</tr>
<tr data-id="ORD-4817">
<td></td>
<td><span class="mv-data-table-mono">ORD-4817</span></td>
<td data-value="Tom Grant"><div class="mv-data-table-person"><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:240" aria-hidden="true"><span class="mv-avatar-fallback">TG</span></span><div><b>Tom Grant</b><small>[email protected]</small></div></div></td>
<td data-value="paid"><span class="mv-badge" data-variant="success" data-size="sm">Paid</span></td>
<td class="mv-table-muted">Online store</td>
<td data-value="2026-09-20" class="mv-table-muted"><time datetime="2026-09-20">Sep 20, 2026</time></td>
<td data-align="end" data-value="2">2</td>
<td data-align="end" data-value="369.71"><b>$369.71</b></td>
<td data-align="end"><button class="mv-button" data-variant="ghost" data-size="icon" type="button" aria-label="Actions for ORD-4817" data-dt-demo-row><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg></button></td>
</tr>
<tr data-id="ORD-4816">
<td></td>
<td><span class="mv-data-table-mono">ORD-4816</span></td>
<td data-value="Lena Fischer"><div class="mv-data-table-person"><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:20" aria-hidden="true"><span class="mv-avatar-fallback">LF</span></span><div><b>Lena Fischer</b><small>[email protected]</small></div></div></td>
<td data-value="shipped"><span class="mv-badge" data-variant="info" data-size="sm">Shipped</span></td>
<td class="mv-table-muted">Online store</td>
<td data-value="2026-09-19" class="mv-table-muted"><time datetime="2026-09-19">Sep 19, 2026</time></td>
<td data-align="end" data-value="2">2</td>
<td data-align="end" data-value="264.74"><b>$264.74</b></td>
<td data-align="end"><button class="mv-button" data-variant="ghost" data-size="icon" type="button" aria-label="Actions for ORD-4816" data-dt-demo-row><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg></button></td>
</tr>
<tr data-id="ORD-4815">
<td></td>
<td><span class="mv-data-table-mono">ORD-4815</span></td>
<td data-value="Hugo Larsen"><div class="mv-data-table-person"><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:110" aria-hidden="true"><span class="mv-avatar-fallback">HL</span></span><div><b>Hugo Larsen</b><small>[email protected]</small></div></div></td>
<td data-value="shipped"><span class="mv-badge" data-variant="info" data-size="sm">Shipped</span></td>
<td class="mv-table-muted">Mobile app</td>
<td data-value="2026-09-18" class="mv-table-muted"><time datetime="2026-09-18">Sep 18, 2026</time></td>
<td data-align="end" data-value="4">4</td>
<td data-align="end" data-value="100.83"><b>$100.83</b></td>
<td data-align="end"><button class="mv-button" data-variant="ghost" data-size="icon" type="button" aria-label="Actions for ORD-4815" data-dt-demo-row><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg></button></td>
</tr>
<tr data-id="ORD-4814">
<td></td>
<td><span class="mv-data-table-mono">ORD-4814</span></td>
<td data-value="Chloe Martin"><div class="mv-data-table-person"><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:350" aria-hidden="true"><span class="mv-avatar-fallback">CM</span></span><div><b>Chloe Martin</b><small>[email protected]</small></div></div></td>
<td data-value="shipped"><span class="mv-badge" data-variant="info" data-size="sm">Shipped</span></td>
<td class="mv-table-muted">Mobile app</td>
<td data-value="2026-09-17" class="mv-table-muted"><time datetime="2026-09-17">Sep 17, 2026</time></td>
<td data-align="end" data-value="5">5</td>
<td data-align="end" data-value="373.11"><b>$373.11</b></td>
<td data-align="end"><button class="mv-button" data-variant="ghost" data-size="icon" type="button" aria-label="Actions for ORD-4814" data-dt-demo-row><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg></button></td>
</tr>
<tr data-id="ORD-4813">
<td></td>
<td><span class="mv-data-table-mono">ORD-4813</span></td>
<td data-value="Mehdi Haddad"><div class="mv-data-table-person"><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:60" aria-hidden="true"><span class="mv-avatar-fallback">MH</span></span><div><b>Mehdi Haddad</b><small>[email protected]</small></div></div></td>
<td data-value="paid"><span class="mv-badge" data-variant="success" data-size="sm">Paid</span></td>
<td class="mv-table-muted">Marketplace</td>
<td data-value="2026-09-17" class="mv-table-muted"><time datetime="2026-09-17">Sep 17, 2026</time></td>
<td data-align="end" data-value="2">2</td>
<td data-align="end" data-value="373.29"><b>$373.29</b></td>
<td data-align="end"><button class="mv-button" data-variant="ghost" data-size="icon" type="button" aria-label="Actions for ORD-4813" data-dt-demo-row><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg></button></td>
</tr>
<tr data-id="ORD-4812">
<td></td>
<td><span class="mv-data-table-mono">ORD-4812</span></td>
<td data-value="Zoe Brooks"><div class="mv-data-table-person"><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:300" aria-hidden="true"><span class="mv-avatar-fallback">ZB</span></span><div><b>Zoe Brooks</b><small>[email protected]</small></div></div></td>
<td data-value="paid"><span class="mv-badge" data-variant="success" data-size="sm">Paid</span></td>
<td class="mv-table-muted">Online store</td>
<td data-value="2026-09-16" class="mv-table-muted"><time datetime="2026-09-16">Sep 16, 2026</time></td>
<td data-align="end" data-value="5">5</td>
<td data-align="end" data-value="460.93"><b>$460.93</b></td>
<td data-align="end"><button class="mv-button" data-variant="ghost" data-size="icon" type="button" aria-label="Actions for ORD-4812" data-dt-demo-row><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg></button></td>
</tr>
<tr data-id="ORD-4811">
<td></td>
<td><span class="mv-data-table-mono">ORD-4811</span></td>
<td data-value="James Miller"><div class="mv-data-table-person"><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:190" aria-hidden="true"><span class="mv-avatar-fallback">JM</span></span><div><b>James Miller</b><small>[email protected]</small></div></div></td>
<td data-value="shipped"><span class="mv-badge" data-variant="info" data-size="sm">Shipped</span></td>
<td class="mv-table-muted">London store</td>
<td data-value="2026-09-15" class="mv-table-muted"><time datetime="2026-09-15">Sep 15, 2026</time></td>
<td data-align="end" data-value="5">5</td>
<td data-align="end" data-value="326.77"><b>$326.77</b></td>
<td data-align="end"><button class="mv-button" data-variant="ghost" data-size="icon" type="button" aria-label="Actions for ORD-4811" data-dt-demo-row><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg></button></td>
</tr>
<tr data-id="ORD-4810">
<td></td>
<td><span class="mv-data-table-mono">ORD-4810</span></td>
<td data-value="Emma Park"><div class="mv-data-table-person"><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:140" aria-hidden="true"><span class="mv-avatar-fallback">EP</span></span><div><b>Emma Park</b><small>[email protected]</small></div></div></td>
<td data-value="shipped"><span class="mv-badge" data-variant="info" data-size="sm">Shipped</span></td>
<td class="mv-table-muted">Marketplace</td>
<td data-value="2026-09-14" class="mv-table-muted"><time datetime="2026-09-14">Sep 14, 2026</time></td>
<td data-align="end" data-value="4">4</td>
<td data-align="end" data-value="307.6"><b>$307.60</b></td>
<td data-align="end"><button class="mv-button" data-variant="ghost" data-size="icon" type="button" aria-label="Actions for ORD-4810" data-dt-demo-row><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg></button></td>
</tr>
<tr data-id="ORD-4809">
<td></td>
<td><span class="mv-data-table-mono">ORD-4809</span></td>
<td data-value="Noah Reyes"><div class="mv-data-table-person"><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:230" aria-hidden="true"><span class="mv-avatar-fallback">NR</span></span><div><b>Noah Reyes</b><small>[email protected]</small></div></div></td>
<td data-value="paid"><span class="mv-badge" data-variant="success" data-size="sm">Paid</span></td>
<td class="mv-table-muted">Mobile app</td>
<td data-value="2026-09-14" class="mv-table-muted"><time datetime="2026-09-14">Sep 14, 2026</time></td>
<td data-align="end" data-value="6">6</td>
<td data-align="end" data-value="512.1"><b>$512.10</b></td>
<td data-align="end"><button class="mv-button" data-variant="ghost" data-size="icon" type="button" aria-label="Actions for ORD-4809" data-dt-demo-row><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg></button></td>
</tr>
<tr data-id="ORD-4808">
<td></td>
<td><span class="mv-data-table-mono">ORD-4808</span></td>
<td data-value="Maya Das"><div class="mv-data-table-person"><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:10" aria-hidden="true"><span class="mv-avatar-fallback">MD</span></span><div><b>Maya Das</b><small>[email protected]</small></div></div></td>
<td data-value="paid"><span class="mv-badge" data-variant="success" data-size="sm">Paid</span></td>
<td class="mv-table-muted">Marketplace</td>
<td data-value="2026-09-13" class="mv-table-muted"><time datetime="2026-09-13">Sep 13, 2026</time></td>
<td data-align="end" data-value="4">4</td>
<td data-align="end" data-value="204.75"><b>$204.75</b></td>
<td data-align="end"><button class="mv-button" data-variant="ghost" data-size="icon" type="button" aria-label="Actions for ORD-4808" data-dt-demo-row><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg></button></td>
</tr>
<tr data-id="ORD-4807">
<td></td>
<td><span class="mv-data-table-mono">ORD-4807</span></td>
<td data-value="Yusuf Celik"><div class="mv-data-table-person"><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:260" aria-hidden="true"><span class="mv-avatar-fallback">YC</span></span><div><b>Yusuf Celik</b><small>[email protected]</small></div></div></td>
<td data-value="shipped"><span class="mv-badge" data-variant="info" data-size="sm">Shipped</span></td>
<td class="mv-table-muted">Online store</td>
<td data-value="2026-09-12" class="mv-table-muted"><time datetime="2026-09-12">Sep 12, 2026</time></td>
<td data-align="end" data-value="1">1</td>
<td data-align="end" data-value="197.1"><b>$197.10</b></td>
<td data-align="end"><button class="mv-button" data-variant="ghost" data-size="icon" type="button" aria-label="Actions for ORD-4807" data-dt-demo-row><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg></button></td>
</tr>
<tr data-id="ORD-4806">
<td></td>
<td><span class="mv-data-table-mono">ORD-4806</span></td>
<td data-value="Mehdi Haddad"><div class="mv-data-table-person"><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:60" aria-hidden="true"><span class="mv-avatar-fallback">MH</span></span><div><b>Mehdi Haddad</b><small>[email protected]</small></div></div></td>
<td data-value="pending"><span class="mv-badge" data-variant="warning" data-size="sm">Pending</span></td>
<td class="mv-table-muted">London store</td>
<td data-value="2026-09-11" class="mv-table-muted"><time datetime="2026-09-11">Sep 11, 2026</time></td>
<td data-align="end" data-value="2">2</td>
<td data-align="end" data-value="488.94"><b>$488.94</b></td>
<td data-align="end"><button class="mv-button" data-variant="ghost" data-size="icon" type="button" aria-label="Actions for ORD-4806" data-dt-demo-row><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg></button></td>
</tr>
<tr data-id="ORD-4805">
<td></td>
<td><span class="mv-data-table-mono">ORD-4805</span></td>
<td data-value="Hugo Larsen"><div class="mv-data-table-person"><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:110" aria-hidden="true"><span class="mv-avatar-fallback">HL</span></span><div><b>Hugo Larsen</b><small>[email protected]</small></div></div></td>
<td data-value="paid"><span class="mv-badge" data-variant="success" data-size="sm">Paid</span></td>
<td class="mv-table-muted">Marketplace</td>
<td data-value="2026-09-11" class="mv-table-muted"><time datetime="2026-09-11">Sep 11, 2026</time></td>
<td data-align="end" data-value="5">5</td>
<td data-align="end" data-value="433.63"><b>$433.63</b></td>
<td data-align="end"><button class="mv-button" data-variant="ghost" data-size="icon" type="button" aria-label="Actions for ORD-4805" data-dt-demo-row><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg></button></td>
</tr>
<tr data-id="ORD-4804">
<td></td>
<td><span class="mv-data-table-mono">ORD-4804</span></td>
<td data-value="Lena Fischer"><div class="mv-data-table-person"><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:20" aria-hidden="true"><span class="mv-avatar-fallback">LF</span></span><div><b>Lena Fischer</b><small>[email protected]</small></div></div></td>
<td data-value="shipped"><span class="mv-badge" data-variant="info" data-size="sm">Shipped</span></td>
<td class="mv-table-muted">London store</td>
<td data-value="2026-09-10" class="mv-table-muted"><time datetime="2026-09-10">Sep 10, 2026</time></td>
<td data-align="end" data-value="5">5</td>
<td data-align="end" data-value="387.7"><b>$387.70</b></td>
<td data-align="end"><button class="mv-button" data-variant="ghost" data-size="icon" type="button" aria-label="Actions for ORD-4804" data-dt-demo-row><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg></button></td>
</tr>
<tr data-id="ORD-4803">
<td></td>
<td><span class="mv-data-table-mono">ORD-4803</span></td>
<td data-value="Isabel Moreno"><div class="mv-data-table-person"><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:280" aria-hidden="true"><span class="mv-avatar-fallback">IM</span></span><div><b>Isabel Moreno</b><small>[email protected]</small></div></div></td>
<td data-value="shipped"><span class="mv-badge" data-variant="info" data-size="sm">Shipped</span></td>
<td class="mv-table-muted">Online store</td>
<td data-value="2026-09-09" class="mv-table-muted"><time datetime="2026-09-09">Sep 9, 2026</time></td>
<td data-align="end" data-value="4">4</td>
<td data-align="end" data-value="605.59"><b>$605.59</b></td>
<td data-align="end"><button class="mv-button" data-variant="ghost" data-size="icon" type="button" aria-label="Actions for ORD-4803" data-dt-demo-row><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg></button></td>
</tr>
<tr data-id="ORD-4802">
<td></td>
<td><span class="mv-data-table-mono">ORD-4802</span></td>
<td data-value="Camila Rossi"><div class="mv-data-table-person"><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:200" aria-hidden="true"><span class="mv-avatar-fallback">CR</span></span><div><b>Camila Rossi</b><small>[email protected]</small></div></div></td>
<td data-value="shipped"><span class="mv-badge" data-variant="info" data-size="sm">Shipped</span></td>
<td class="mv-table-muted">London store</td>
<td data-value="2026-09-08" class="mv-table-muted"><time datetime="2026-09-08">Sep 8, 2026</time></td>
<td data-align="end" data-value="5">5</td>
<td data-align="end" data-value="210.58"><b>$210.58</b></td>
<td data-align="end"><button class="mv-button" data-variant="ghost" data-size="icon" type="button" aria-label="Actions for ORD-4802" data-dt-demo-row><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg></button></td>
</tr>
<tr data-id="ORD-4801">
<td></td>
<td><span class="mv-data-table-mono">ORD-4801</span></td>
<td data-value="Tom Grant"><div class="mv-data-table-person"><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:240" aria-hidden="true"><span class="mv-avatar-fallback">TG</span></span><div><b>Tom Grant</b><small>[email protected]</small></div></div></td>
<td data-value="shipped"><span class="mv-badge" data-variant="info" data-size="sm">Shipped</span></td>
<td class="mv-table-muted">Online store</td>
<td data-value="2026-09-08" class="mv-table-muted"><time datetime="2026-09-08">Sep 8, 2026</time></td>
<td data-align="end" data-value="3">3</td>
<td data-align="end" data-value="569.74"><b>$569.74</b></td>
<td data-align="end"><button class="mv-button" data-variant="ghost" data-size="icon" type="button" aria-label="Actions for ORD-4801" data-dt-demo-row><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg></button></td>
</tr>
<tr data-id="ORD-4800">
<td></td>
<td><span class="mv-data-table-mono">ORD-4800</span></td>
<td data-value="Chloe Martin"><div class="mv-data-table-person"><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:350" aria-hidden="true"><span class="mv-avatar-fallback">CM</span></span><div><b>Chloe Martin</b><small>[email protected]</small></div></div></td>
<td data-value="paid"><span class="mv-badge" data-variant="success" data-size="sm">Paid</span></td>
<td class="mv-table-muted">Online store</td>
<td data-value="2026-09-07" class="mv-table-muted"><time datetime="2026-09-07">Sep 7, 2026</time></td>
<td data-align="end" data-value="4">4</td>
<td data-align="end" data-value="397.99"><b>$397.99</b></td>
<td data-align="end"><button class="mv-button" data-variant="ghost" data-size="icon" type="button" aria-label="Actions for ORD-4800" data-dt-demo-row><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg></button></td>
</tr>
<tr data-id="ORD-4799">
<td></td>
<td><span class="mv-data-table-mono">ORD-4799</span></td>
<td data-value="Sofia Nguyen"><div class="mv-data-table-person"><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:320" aria-hidden="true"><span class="mv-avatar-fallback">SN</span></span><div><b>Sofia Nguyen</b><small>[email protected]</small></div></div></td>
<td data-value="shipped"><span class="mv-badge" data-variant="info" data-size="sm">Shipped</span></td>
<td class="mv-table-muted">London store</td>
<td data-value="2026-09-06" class="mv-table-muted"><time datetime="2026-09-06">Sep 6, 2026</time></td>
<td data-align="end" data-value="2">2</td>
<td data-align="end" data-value="98.45"><b>$98.45</b></td>
<td data-align="end"><button class="mv-button" data-variant="ghost" data-size="icon" type="button" aria-label="Actions for ORD-4799" data-dt-demo-row><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg></button></td>
</tr>
<tr data-id="ORD-4798">
<td></td>
<td><span class="mv-data-table-mono">ORD-4798</span></td>
<td data-value="Hugo Larsen"><div class="mv-data-table-person"><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:110" aria-hidden="true"><span class="mv-avatar-fallback">HL</span></span><div><b>Hugo Larsen</b><small>[email protected]</small></div></div></td>
<td data-value="refunded"><span class="mv-badge" data-variant="secondary" data-size="sm">Refunded</span></td>
<td class="mv-table-muted">London store</td>
<td data-value="2026-09-05" class="mv-table-muted"><time datetime="2026-09-05">Sep 5, 2026</time></td>
<td data-align="end" data-value="2">2</td>
<td data-align="end" data-value="326.83"><b>$326.83</b></td>
<td data-align="end"><button class="mv-button" data-variant="ghost" data-size="icon" type="button" aria-label="Actions for ORD-4798" data-dt-demo-row><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg></button></td>
</tr>
<tr data-id="ORD-4797">
<td></td>
<td><span class="mv-data-table-mono">ORD-4797</span></td>
<td data-value="Hugo Larsen"><div class="mv-data-table-person"><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:110" aria-hidden="true"><span class="mv-avatar-fallback">HL</span></span><div><b>Hugo Larsen</b><small>[email protected]</small></div></div></td>
<td data-value="shipped"><span class="mv-badge" data-variant="info" data-size="sm">Shipped</span></td>
<td class="mv-table-muted">Marketplace</td>
<td data-value="2026-09-05" class="mv-table-muted"><time datetime="2026-09-05">Sep 5, 2026</time></td>
<td data-align="end" data-value="4">4</td>
<td data-align="end" data-value="567.46"><b>$567.46</b></td>
<td data-align="end"><button class="mv-button" data-variant="ghost" data-size="icon" type="button" aria-label="Actions for ORD-4797" data-dt-demo-row><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg></button></td>
</tr>
<tr data-id="ORD-4796">
<td></td>
<td><span class="mv-data-table-mono">ORD-4796</span></td>
<td data-value="Emma Park"><div class="mv-data-table-person"><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:140" aria-hidden="true"><span class="mv-avatar-fallback">EP</span></span><div><b>Emma Park</b><small>[email protected]</small></div></div></td>
<td data-value="pending"><span class="mv-badge" data-variant="warning" data-size="sm">Pending</span></td>
<td class="mv-table-muted">Mobile app</td>
<td data-value="2026-09-04" class="mv-table-muted"><time datetime="2026-09-04">Sep 4, 2026</time></td>
<td data-align="end" data-value="4">4</td>
<td data-align="end" data-value="241.16"><b>$241.16</b></td>
<td data-align="end"><button class="mv-button" data-variant="ghost" data-size="icon" type="button" aria-label="Actions for ORD-4796" data-dt-demo-row><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg></button></td>
</tr>
<tr data-id="ORD-4795">
<td></td>
<td><span class="mv-data-table-mono">ORD-4795</span></td>
<td data-value="Kwame Boateng"><div class="mv-data-table-person"><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:160" aria-hidden="true"><span class="mv-avatar-fallback">KB</span></span><div><b>Kwame Boateng</b><small>[email protected]</small></div></div></td>
<td data-value="paid"><span class="mv-badge" data-variant="success" data-size="sm">Paid</span></td>
<td class="mv-table-muted">Mobile app</td>
<td data-value="2026-09-03" class="mv-table-muted"><time datetime="2026-09-03">Sep 3, 2026</time></td>
<td data-align="end" data-value="6">6</td>
<td data-align="end" data-value="112.11"><b>$112.11</b></td>
<td data-align="end"><button class="mv-button" data-variant="ghost" data-size="icon" type="button" aria-label="Actions for ORD-4795" data-dt-demo-row><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg></button></td>
</tr>
</tbody>
</table>
</div>
<div class="mv-data-table-footer">
<span data-dt-selection aria-live="polite"></span>
<label>Rows per page
<select class="mv-select" data-size="sm" data-dt-page-size>
<option>5</option><option>8</option><option>10</option><option>20</option>
</select>
</label>
<span data-dt-page-info></span>
<mv-pagination data-size="sm" hide-labels siblings="0" data-dt-always label="Orders pagination"></mv-pagination>
</div>
<div class="mv-data-table-bulk" role="toolbar" aria-label="Bulk actions" hidden>
<span data-dt-bulk-count></span>
<button class="mv-button" data-variant="ghost" data-size="sm" type="button" data-dt-action="ship"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h11v10H3zM14 10h4l3 3v3h-7"/><circle cx="7" cy="18" r="1.8"/><circle cx="17.5" cy="18" r="1.8"/></svg><span data-dt-label>Mark as shipped</span></button>
<button class="mv-button" data-variant="ghost" data-size="sm" type="button" data-dt-action="export"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12m-5-5 5 5 5-5M5 21h14"/></svg><span data-dt-label>Export</span></button>
<button class="mv-button" data-variant="ghost" data-size="sm" type="button" data-dt-action="delete" style="color:var(--mv-danger)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3"/></svg><span data-dt-label>Delete</span></button>
<button class="mv-button" data-variant="ghost" data-size="icon" type="button" data-dt-clear aria-label="Clear selection"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
</div>
</mv-data-table>
<p id="dt-demo-log" role="status" style="margin:.75rem 0 0;min-height:1.25rem;color:var(--mv-fg-muted);font-size:.75rem;text-align:center"></p>
</div>
<div id="dt-statement" style="width:min(100%,64rem);margin:3rem auto 0">
<p style="margin:0 0 .75rem;font-weight:600">Usage statement, September 2026</p>
<mv-data-table mode="server" page-size="5" label="Usage statement, September 2026" loading>
<div class="mv-data-table-toolbar">
<div class="mv-input-group" data-size="sm">
<label class="mv-input-group-addon" for="dt-statement-q"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg><span class="mv-sr-only">Search</span></label>
<input id="dt-statement-q" class="mv-input" type="search" placeholder="Search resources…" data-dt-search autocomplete="off">
</div>
<select class="mv-select" data-size="sm" data-dt-filter="service" aria-label="Filter by service">
<option value="">All services</option>
<option>Compute</option>
<option>Storage</option>
<option>Bandwidth</option>
<option>Database</option>
<option>Functions</option>
</select>
</div>
<div class="mv-table-wrap">
<table class="mv-table">
<thead>
<tr>
<th scope="col" data-dt-select></th>
<th scope="col" data-key="resource" data-sort="text" data-card="title">Resource</th>
<th scope="col" data-key="service" data-sort="text" data-card="subtitle">Service</th>
<th scope="col" data-key="date" data-sort="date" data-card="meta">Date</th>
<th scope="col" data-key="usage" data-sort="number" data-align="end">Usage</th>
<th scope="col" data-key="amount" data-sort="number" data-align="end" data-card="amount">Amount</th>
</tr>
</thead>
<tbody></tbody>
<tfoot>
<tr><th scope="row">Subtotal</th><td data-key="amount" data-dt-statement="subtotal"></td></tr>
<tr><th scope="row">Sales tax (8.875%)</th><td data-key="amount" data-dt-statement="tax"></td></tr>
<tr><th scope="row">Total due</th><td data-key="amount" data-dt-statement="total"></td></tr>
</tfoot>
</table>
</div>
<div class="mv-data-table-footer">
<span data-dt-selection aria-live="polite"></span>
<label>Rows per page
<select class="mv-select" data-size="sm" data-dt-page-size>
<option>5</option><option>10</option><option>25</option>
</select>
</label>
<span data-dt-page-info></span>
<mv-pagination data-size="sm" hide-labels siblings="0" label="Statement pagination"></mv-pagination>
</div>
</mv-data-table>
</div>
<script type="module">
// A fake API: 312 usage lines, filtered, sorted and paged "on the server", 350 ms away.
const root = document.getElementById("dt-statement");
const table = root?.querySelector("mv-data-table");
const SERVICES = [
{ name: "Compute", unit: "vCPU-hours", rate: 0.0416, resources: ["api-gateway-1", "api-gateway-2", "worker-us-east", "worker-eu-west", "batch-runner"] },
{ name: "Storage", unit: "GB-months", rate: 0.023, resources: ["media-bucket", "nightly-backups", "logs-archive"] },
{ name: "Bandwidth", unit: "GB", rate: 0.09, resources: ["cdn-edge-us", "cdn-edge-eu", "cdn-edge-apac"] },
{ name: "Database", unit: "instance-hours", rate: 0.171, resources: ["orders-db-primary", "orders-db-replica", "analytics-warehouse"] },
{ name: "Functions", unit: "million requests", rate: 0.2, resources: ["image-resize", "webhook-relay", "email-digest"] },
];
let seed = 7;
const rand = () => ((seed = (seed * 16807) % 2147483647) - 1) / 2147483646;
const LINES = Array.from({ length: 312 }, (_, i) => {
const s = SERVICES[Math.floor(rand() * SERVICES.length)];
const day = 1 + Math.floor(rand() * 30);
const usage = Math.round((40 + rand() * 1800) * 10) / 10;
return { id: `LN-${String(i + 1).padStart(4, "0")}`, resource: s.resources[Math.floor(rand() * s.resources.length)], service: s.name, unit: s.unit, date: `2026-09-${String(day).padStart(2, "0")}`, usage, amount: Math.round(usage * s.rate * 100) / 100 };
});
const money = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
const number = new Intl.NumberFormat("en-US");
const day = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: "UTC" });
function fakeApi({ page, pageSize, sort, direction, query, filters }) {
const q = query.toLowerCase();
let rows = LINES.filter((l) => (!q || `${l.resource} ${l.service}`.toLowerCase().includes(q)) && (!filters.service || l.service === filters.service));
if (sort) {
const sign = direction === "descending" ? -1 : 1;
rows = [...rows].sort((a, b) => sign * (typeof a[sort] === "number" ? a[sort] - b[sort] : String(a[sort]).localeCompare(String(b[sort]))));
}
const subtotal = rows.reduce((sum, l) => sum + l.amount, 0);
const start = (page - 1) * pageSize;
return new Promise((done) => setTimeout(() => done({ rows: rows.slice(start, start + pageSize), total: rows.length, subtotal }), 350));
}
function cell(text, attrs = {}) {
const td = document.createElement("td");
for (const [k, v] of Object.entries(attrs)) td.setAttribute(k, v);
td.append(text);
return td;
}
function rowFor(line) {
const tr = document.createElement("tr");
tr.dataset.id = line.id;
tr.dataset.dtName = `${line.resource}, ${day.format(new Date(line.date))}`;
const resource = document.createElement("span");
resource.className = "mv-data-table-mono";
resource.style.color = "var(--mv-fg)";
resource.textContent = line.resource;
const amount = document.createElement("b");
amount.textContent = money.format(line.amount);
tr.append(
cell(""),
cell(resource),
cell(line.service, { class: "mv-table-muted" }),
cell(day.format(new Date(line.date)), { "data-value": line.date, class: "mv-table-muted" }),
cell(`${number.format(line.usage)} ${line.unit}`, { "data-value": String(line.usage), "data-align": "end" }),
cell(amount, { "data-value": String(line.amount), "data-align": "end" }),
);
return tr;
}
let ticket = 0;
table?.addEventListener("mv-request", async (e) => {
const mine = ++ticket;
table.loading = true;
const res = await fakeApi(e.detail);
if (mine !== ticket) return; // a newer request is on its way
table.setRows(res.rows.map(rowFor), res.total);
const tax = Math.round(res.subtotal * 0.08875 * 100) / 100;
root.querySelector('[data-dt-statement="subtotal"]').textContent = money.format(res.subtotal);
root.querySelector('[data-dt-statement="tax"]').textContent = money.format(tax);
root.querySelector('[data-dt-statement="total"]').textContent = money.format(res.subtotal + tax);
});
customElements.whenDefined("mv-data-table").then(() => table?.refresh());
</script>
<script type="module">
const root = document.getElementById("dt-demo");
const table = root?.querySelector("mv-data-table");
const log = root?.querySelector("#dt-demo-log");
table?.addEventListener("mv-action", (e) => {
const { action, rows } = e.detail;
const n = rows.length;
const s = n > 1 ? "s" : "";
if (action === "delete") {
table.removeRows(rows);
log.textContent = `${n} order${s} deleted.`;
} else if (action === "ship") {
for (const row of rows) {
const cell = row.cells[3];
cell.dataset.value = "shipped";
cell.innerHTML = '<span class="mv-badge" data-variant="info" data-size="sm">Shipped</span>';
}
table.clearSelection();
log.textContent = `${n} order${s} marked as shipped.`;
} else if (action === "export") {
log.textContent = `CSV export of ${n} order${s} ready (demo).`;
}
});
root?.querySelector("[data-dt-demo-export]")?.addEventListener("click", () => { log.textContent = "Export of all orders ready (demo)."; });
root?.addEventListener("click", (e) => {
const b = e.target.closest("[data-dt-demo-row]");
if (b) log.textContent = `Actions menu: ${b.closest("tr").dataset.id}`;
});
</script>API
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
page-size | number | 10 | Rows per page (kept in sync with select[data-dt-page-size]). |
label | string | Accessible name for the table (aria-label) if it has none. | |
mode | client | server | client | client: sorts, searches, filters and pages the authored rows in the browser. server: the app owns the data; the element keeps the sort, page, search, filters, selection and visible columns, emits mv-request on each change and shows the rows the app puts in the tbody (or passes to setRows). Set it before the element connects. |
total | number | Server mode: total row count across all pages, from the app (drives the page count, the range text and “n of N rows selected”). Without it, the rows in the tbody are the total. | |
loading | boolean | Set by the app while a page loads: aria-busy on the table, dimmed rows, a thin progress bar, and a “Loading…” row when the tbody is empty. setRows() clears it. | |
data-selecting | boolean | Set while at least one row is selected. | |
data-cards | boolean | Set by the element when at least one th has data-card: rows become stacked cards in containers narrower than 40rem. |
Properties
| Name | Type | Description |
|---|---|---|
selectedRows | HTMLTableRowElement[] | Selected rows present in the DOM (client mode: across all pages). |
selectedIds | string[] | data-id of the selected rows. In server mode the selection is kept by data-id across pages, including rows no longer loaded. Settable, also before the element is defined. |
page | number | Current page (1-based). Setting it re-renders without emitting mv-request (restore a page from the URL, then load it). |
state | { page, pageSize, sort, direction, query, filters } | Read-only: what the app needs to load the current page (the mv-request detail without reason). |
strings | object | Override any default text: selectAll, selectRow ({name}), columns, empty, emptyHint ({reset}), reset, loading, range ({start}, {end}, {total}), results ({ one, other } with {n}), selection ({ one, other } with {count}, {total}), bulkCount ({count}), sorted ({column}, {direction}), ascending, descending, unsorted. Numbers, plurals and text sorting follow the nearest lang (Intl). Can be set before the element is defined. |
Methods
| Name | Description |
|---|---|
sortBy(key, direction?) | Sorts by data-key ('ascending' | 'descending'); null clears the sort. Server mode: back to page 1 and mv-request. |
setColumnVisible(key, visible) | Shows or hides a column (header, rows, rows loaded later and the totals footer). |
clearSelection() | Clears the selection (every page). |
resetFilters() | Clears the search and filters. |
setRows(rows, total?) | Replaces the rows with new <tr> elements. Server mode: the current page, plus the total when given; clears loading. Client mode: the whole data set. |
refresh() | Server mode: emits mv-request again with the current state (first load, retry, after a bulk change). |
removeRows(rows) | Removes rows (e.g. after a bulk delete). |
render() | Re-renders after cells change. |
Events
| Name | Description |
|---|---|
mv-request | Server mode: load this page. detail: { page, pageSize, sort, direction, query, filters, reason } with reason sort | page | pageSize | search (300 ms after typing stops) | filter | reset | refresh. |
mv-action | A [data-dt-action] button was clicked. detail: { action, rows, ids }. |
mv-sort | Sort changed. detail: { key, direction }. |
mv-page | The user changed page. detail: { page, pageSize }. |
mv-columns | Column visibility changed. detail: { key, visible }. |
mv-render | After each render. detail: { total, page, pages, rows } (client mode: rows matching the search and filters, all pages; server mode: the loaded rows). |
Content structure
| Name | Description |
|---|---|
table.mv-table | Plain HTML table: th[data-key] names the column, th[data-sort=text|number|date] makes it sortable, th[data-dt-select] adds the checkbox column (named after tr[data-dt-name], else the next cell); td[data-value] provides the raw value for sorting and filtering; tr[data-id] keys the selection in server mode. |
th[data-card] | Where the column goes when rows become cards (container under 40rem): title, subtitle (muted, under the title), amount (top right), meta (under the amount), action (far right), detail (label and value line, the default for columns without a role) or hide. One th[data-card] turns the layout on; the header row becomes a bar with select all and the sort buttons. |
tfoot | Totals footer: each row has one label cell (th scope=row) then cells with data-key="column". The label spans the columns before the first value, each value sits under its column, gaps are filled and recomputed when columns are hidden; the last of several rows is styled as the grand total. In cards, label left and value right. The app writes the values. |
[data-dt-search] | Search field (matches row text, ignoring accents and case). |
select[data-dt-filter="key"] | Exact-match filter on a column value (empty option = all). |
mv-menu[data-dt-columns] | Menu whose [data-content] panel is filled with column checkboxes (th[data-dt-hideable=false] opts out). |
[data-dt-selection] / [data-dt-page-info] | Auto-filled text: “n of N rows selected”, “1-8 of 27”. |
select[data-dt-page-size] | Rows-per-page picker. |
mv-pagination | Controlled pagination (hidden when there is only one page, unless data-dt-always). |
.mv-data-table-bulk | Floating bulk-actions bar, shown as soon as a row is checked; [data-dt-bulk-count], [data-dt-action], [data-dt-clear]. |
CSS classes
| Name | Description |
|---|---|
mv-data-table-toolbar / -toolbar-end / -footer | Bars above and below the table. |
[data-dt-label] | Button label visually hidden in narrow containers (still the accessible name). |
mv-data-table-person / -mono | Avatar + name + email cells, monospace IDs. |
Accessibility
Sorting uses real buttons in the headers with aria-sort; checkboxes are named after their row (“Select ORD-4821”) and the header checkbox supports the indeterminate state; rows carry aria-selected; the result count is announced (role=status) after a search or filter, and so are header sorts (“Sorted by Amount, ascending”) and Shift + click ranges; the actions bar is a named role=toolbar; the column menu uses menuitemcheckbox items and stays open; pagination is keyboard navigable; when the bulk bar or the empty state disappears, focus moves back into the table instead of the page; forced colors get system focus outlines. Tables with card roles state role=table, rowgroup, row, columnheader, rowheader and cell explicitly, so the card layout keeps its table semantics in every browser; the column name shown before a detail value is generated content hidden from assistive tech (the cell header already names it). In server mode the table carries aria-busy while loading, and the result count is announced once the new data has arrived after a search or filter. Totals rows use a th scope=row label.