Data displayExtension
DataTable
Search, sort, page, select and export the rows of a native table already in your HTML.
| Class name | Type | Description |
|---|---|---|
datatableplf:datatable | Component | Wraps a Table and its controls |
datatable-toolbarplf:datatable-toolbar | Part | The search and settings row above the table |
datatable-filtersplf:datatable-filters | Part | The column filter fields |
datatable-filterplf:datatable-filter | Part | The search field |
datatable-filter-countplf:datatable-filter-count | Part | The Filters disclosure's active count |
datatable-filter-chipsplf:datatable-filter-chips | Part | The row of active filter buttons |
datatable-filter-chipplf:datatable-filter-chip | Part | An active filter button your script adds |
datatable-settingsplf:datatable-settings | Part | The toolbar's settings and export controls |
datatable-densityplf:datatable-density | Part | The density label and select |
datatable-disclosureplf:datatable-disclosure | Part | A compact Collapsible for filters or column layout |
datatable-columns-panelplf:datatable-columns-panel | Part | The columns Popover panel |
datatable-columns-headingplf:datatable-columns-heading | Part | The columns panel's title row |
datatable-column-settingsplf:datatable-column-settings | Part | A list of column settings |
datatable-column-settingplf:datatable-column-setting | Part | One column's width and move controls |
datatable-column-choiceplf:datatable-column-choice | Part | A column's show or hide checkbox |
datatable-column-actionsplf:datatable-column-actions | Part | A column's width range and move buttons |
datatable-column-togglesplf:datatable-column-toggles | Part | The column checkboxes |
datatable-views-panelplf:datatable-views-panel | Part | The saved views Popover panel |
datatable-views-headingplf:datatable-views-heading | Part | The views panel's title row |
datatable-view-actionsplf:datatable-view-actions | Part | The views panel's buttons |
datatable-view-saveplf:datatable-view-save | Part | The full-width Save view button |
datatable-export-panelplf:datatable-export-panel | Part | The export Popover panel |
datatable-export-headerplf:datatable-export-header | Part | The export panel's title row |
datatable-export-optionsplf:datatable-export-options | Part | The download format buttons |
datatable-export-utilitiesplf:datatable-export-utilities | Part | The Copy and Print buttons |
datatable-export-extensionplf:datatable-export-extension | Part | A format's file extension |
datatable-export-actionplf:datatable-export-action | Part | A full-width download format button |
datatable-export-statusplf:datatable-export-status | Part | The export message, hidden while empty |
datatable-bulkplf:datatable-bulk | Part | The bar shown while rows are selected |
datatable-headingplf:datatable-heading | Part | The sort button and rank the script adds to a header |
datatable-sort-buttonplf:datatable-sort-button | Part | A header's sort button the script adds |
datatable-sort-iconplf:datatable-sort-icon | Part | The sort direction arrow the script adds |
datatable-sort-rankplf:datatable-sort-rank | Part | A column's place in a multi-sort, which the script adds |
datatable-emptyplf:datatable-empty | Part | The no-results message |
datatable-statusplf:datatable-status | Part | The muted text with the counters |
datatable-footerplf:datatable-footer | Part | The status and paging row below the table |
datatable-paginationplf:datatable-pagination | Part | The page size and page buttons |
datatable-page-sizeplf:datatable-page-size | Part | The rows per page label and select |
datatable-page-jumpplf:datatable-page-jump | Part | The page input and page count |
datatable-page-inputplf:datatable-page-input | Part | The page number input |
datatable-pagesplf:datatable-pages | Part | The paging buttons' navigation |
datatable-numbered-pagesplf:datatable-numbered-pages | Part | The numbered page buttons' container |
datatable-pageplf:datatable-page | Part | A numbered page button the script adds from the template |
datatable-previous-iconplf:datatable-previous-icon | Part | Your own first or previous page icon, in place of the drawn chevrons: a forward one, mirrored to point back |
datatable-next-iconplf:datatable-next-icon | Part | Your own next or last page icon, in place of the drawn chevrons, mirrored in right-to-left text |
Default
Author the data-plf-datatable-controls groups hidden: the script shows them and adds a sort button to each data-plf-datatable-sortable header, so without JavaScript the table still lists every row.
| Name | Revenue | Status | |
|---|---|---|---|
| Avery Stone | avery@example.test | $316.00 | Customer |
| Blair Chen | blair@example.test | $242.00 | Customer |
| Casey Rivera | casey@example.test | $837.00 | Customer |
| Devon Patel | devon@example.test | $874.00 | Customer |
| Finley Hart | finley@example.test | $99.00 | Trial |
| Noah West | noah@example.test | −$15.00 | Refunded |
| Parker Young | parker@example.test | — | Trial |
<div class="datatable" data-plf-datatable data-plf-page-size="5">
<div class="datatable-toolbar" data-plf-datatable-controls hidden>
<div class="field datatable-filter">
<label class="field-label" for="contacts-filter">Search contacts</label>
<div class="input-group input-group-sm">
<span class="icon icon-search"></span>
<input class="input-group-control" id="contacts-filter" type="search" placeholder="Name or email">
</div>
</div>
</div>
<div class="table-wrapper" tabindex="0" role="region" aria-labelledby="contacts-caption">
<table class="table table-bordered table-zebra" style="--plf-table-min-width: 48rem">
<caption id="contacts-caption">Contacts</caption>
<thead>
<tr>
<th scope="col" data-plf-column="name" data-plf-searchable data-plf-datatable-sortable>Name</th>
<th scope="col" data-plf-column="email" data-plf-searchable data-plf-datatable-sortable>Email</th>
<th scope="col" data-plf-column="revenue" data-plf-type="number" data-plf-datatable-sortable>Revenue</th>
<th scope="col" data-plf-column="status">Status</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Avery Stone</th>
<td>avery@example.test</td>
<td data-plf-value="316">$316.00</td>
<td>Customer</td>
</tr>
<tr>
<th scope="row">Blair Chen</th>
<td>blair@example.test</td>
<td data-plf-value="242">$242.00</td>
<td>Customer</td>
</tr>
<tr>
<th scope="row">Casey Rivera</th>
<td>casey@example.test</td>
<td data-plf-value="837">$837.00</td>
<td>Customer</td>
</tr>
<tr>
<th scope="row">Devon Patel</th>
<td>devon@example.test</td>
<td data-plf-value="874">$874.00</td>
<td>Customer</td>
</tr>
<tr>
<th scope="row">Finley Hart</th>
<td>finley@example.test</td>
<td data-plf-value="99">$99.00</td>
<td>Trial</td>
</tr>
<tr>
<th scope="row">Noah West</th>
<td>noah@example.test</td>
<td data-plf-value="-15">−$15.00</td>
<td>Refunded</td>
</tr>
<tr>
<th scope="row">Parker Young</th>
<td>parker@example.test</td>
<td>—</td>
<td>Trial</td>
</tr>
</tbody>
</table>
</div>
<div class="datatable-empty" data-plf-datatable-empty hidden>No contacts match your search. Try another name or email.</div>
<div class="datatable-footer" data-plf-datatable-controls hidden>
<p class="datatable-status" role="status"><span data-plf-datatable-count="from">0</span>–<span data-plf-datatable-count="to">0</span> of <span data-plf-datatable-count="matched">0</span> contacts<span class="sr-only"> · Page <span data-plf-datatable-count="page">0</span> of <span data-plf-datatable-count="pages">0</span></span></p>
<div class="datatable-pagination">
<nav class="datatable-pages" aria-label="Contact result pages">
<button class="btn btn-outline btn-sm btn-square" aria-label="First page" data-plf-datatable-first></button>
<button class="btn btn-outline btn-sm btn-square" aria-label="Previous page" data-plf-datatable-previous></button>
<span class="datatable-page-jump">
<label class="sr-only" for="contacts-page">Page</label>
<input class="input input-sm datatable-page-input" id="contacts-page" type="number" inputmode="numeric">
<span>of <span data-plf-datatable-count="pages">0</span></span>
</span>
<button class="btn btn-outline btn-sm btn-square" aria-label="Next page" data-plf-datatable-next></button>
<button class="btn btn-outline btn-sm btn-square" aria-label="Last page" data-plf-datatable-last></button>
</nav>
</div>
</div>
</div><div class="plf:datatable" data-plf-datatable data-plf-page-size="5">
<div class="plf:datatable-toolbar" data-plf-datatable-controls hidden>
<div class="plf:field plf:datatable-filter">
<label class="plf:field-label" for="contacts-filter">Search contacts</label>
<div class="plf:input-group plf:input-group-sm">
<span class="plf:icon plf:icon-search"></span>
<input class="plf:input-group-control" id="contacts-filter" type="search" placeholder="Name or email">
</div>
</div>
</div>
<div class="plf:table-wrapper" tabindex="0" role="region" aria-labelledby="contacts-caption">
<table class="plf:table plf:table-bordered plf:table-zebra" style="--plf-table-min-width: 48rem">
<caption id="contacts-caption">Contacts</caption>
<thead>
<tr>
<th scope="col" data-plf-column="name" data-plf-searchable data-plf-datatable-sortable>Name</th>
<th scope="col" data-plf-column="email" data-plf-searchable data-plf-datatable-sortable>Email</th>
<th scope="col" data-plf-column="revenue" data-plf-type="number" data-plf-datatable-sortable>Revenue</th>
<th scope="col" data-plf-column="status">Status</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Avery Stone</th>
<td>avery@example.test</td>
<td data-plf-value="316">$316.00</td>
<td>Customer</td>
</tr>
<tr>
<th scope="row">Blair Chen</th>
<td>blair@example.test</td>
<td data-plf-value="242">$242.00</td>
<td>Customer</td>
</tr>
<tr>
<th scope="row">Casey Rivera</th>
<td>casey@example.test</td>
<td data-plf-value="837">$837.00</td>
<td>Customer</td>
</tr>
<tr>
<th scope="row">Devon Patel</th>
<td>devon@example.test</td>
<td data-plf-value="874">$874.00</td>
<td>Customer</td>
</tr>
<tr>
<th scope="row">Finley Hart</th>
<td>finley@example.test</td>
<td data-plf-value="99">$99.00</td>
<td>Trial</td>
</tr>
<tr>
<th scope="row">Noah West</th>
<td>noah@example.test</td>
<td data-plf-value="-15">−$15.00</td>
<td>Refunded</td>
</tr>
<tr>
<th scope="row">Parker Young</th>
<td>parker@example.test</td>
<td>—</td>
<td>Trial</td>
</tr>
</tbody>
</table>
</div>
<div class="plf:datatable-empty" data-plf-datatable-empty hidden>No contacts match your search. Try another name or email.</div>
<div class="plf:datatable-footer" data-plf-datatable-controls hidden>
<p class="plf:datatable-status" role="status"><span data-plf-datatable-count="from">0</span>–<span data-plf-datatable-count="to">0</span> of <span data-plf-datatable-count="matched">0</span> contacts<span class="plf:sr-only"> · Page <span data-plf-datatable-count="page">0</span> of <span data-plf-datatable-count="pages">0</span></span></p>
<div class="plf:datatable-pagination">
<nav class="plf:datatable-pages" aria-label="Contact result pages">
<button class="plf:btn plf:btn-outline plf:btn-sm plf:btn-square" aria-label="First page" data-plf-datatable-first></button>
<button class="plf:btn plf:btn-outline plf:btn-sm plf:btn-square" aria-label="Previous page" data-plf-datatable-previous></button>
<span class="plf:datatable-page-jump">
<label class="plf:sr-only" for="contacts-page">Page</label>
<input class="plf:input plf:input-sm plf:datatable-page-input" id="contacts-page" type="number" inputmode="numeric">
<span>of <span data-plf-datatable-count="pages">0</span></span>
</span>
<button class="plf:btn plf:btn-outline plf:btn-sm plf:btn-square" aria-label="Next page" data-plf-datatable-next></button>
<button class="plf:btn plf:btn-outline plf:btn-sm plf:btn-square" aria-label="Last page" data-plf-datatable-last></button>
</nav>
</div>
</div>
</div>Selection and export
A header’s checkbox selects the page, and the checkbox in its column selects each row; author them hidden, as the script shows them. Your code handles data-plf-datatable-action buttons, such as Review selected, in a plf:datatable:action listener.
| Select rows | Invoice | Customer | Amount |
|---|---|---|---|
| INV-001 | Avery Stone | $0.50 | |
| INV-002 | Blair Chen | $137.50 | |
| INV-003 | Casey Rivera | $274.50 | |
| INV-004 | Devon Patel | $411.50 | |
| INV-005 | Elliot Brooks | $548.50 | |
| INV-006 | Finley Hart | $685.50 |
<div class="datatable" id="invoices" data-plf-datatable>
<div class="datatable-toolbar" data-plf-datatable-controls hidden>
<div class="field datatable-filter">
<label class="field-label" for="invoices-filter">Search invoices</label>
<div class="input-group input-group-sm">
<span class="icon icon-search"></span>
<input class="input-group-control" id="invoices-filter" type="search" placeholder="Invoice or customer">
</div>
</div>
<div class="datatable-settings">
<div class="popover" data-plf-popover data-plf-align="end">
<button class="btn btn-outline btn-sm">
<span class="icon icon-download"></span>
<span>Export</span>
</button>
<div class="popover-content datatable-export-panel" hidden>
<div class="datatable-export-header">
<h4 class="popover-title">Export table</h4>
<button class="btn btn-ghost btn-sm btn-square" aria-label="Close export" data-plf-popover-close>
<span class="icon icon-x"></span>
</button>
</div>
<p class="popover-description">Choose which rows to download, copy or print.</p>
<div class="field">
<label class="field-label" for="invoices-export-scope">Rows to export</label>
<select class="select select-sm" id="invoices-export-scope" data-plf-select>
<option value="filtered">Matching rows</option>
<option value="all">All rows</option>
<option value="selected">Selected rows</option>
</select>
</div>
<div class="datatable-export-options" role="group" aria-label="Download format">
<button class="btn btn-ghost datatable-export-action" data-plf-datatable-export-format="csv">
<span class="icon icon-file-text"></span>
<span>CSV</span>
<span class="datatable-export-extension" dir="ltr" aria-hidden="true">.csv</span>
</button>
<button class="btn btn-ghost datatable-export-action" data-plf-datatable-export-format="xlsx">
<span class="icon icon-file-spreadsheet"></span>
<span>Excel</span>
<span class="datatable-export-extension" dir="ltr" aria-hidden="true">.xlsx</span>
</button>
<button class="btn btn-ghost datatable-export-action" data-plf-datatable-export-format="pdf">
<span class="icon icon-file-text"></span>
<span>PDF</span>
<span class="datatable-export-extension" dir="ltr" aria-hidden="true">.pdf</span>
</button>
</div>
<div class="datatable-export-utilities" role="group" aria-label="Other export actions">
<button class="btn btn-outline btn-sm" data-plf-datatable-export-format="copy">
<span class="icon icon-clipboard"></span>
<span>Copy</span>
</button>
<button class="btn btn-outline btn-sm" data-plf-datatable-export-format="print">
<span class="icon icon-printer"></span>
<span>Print</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="datatable-bulk" data-plf-datatable-bulk hidden>
<span><span data-plf-datatable-count="selected">0</span> selected</span>
<button class="btn btn-outline btn-sm" data-plf-datatable-select-filtered>Select all matching</button>
<button class="btn btn-outline btn-sm" data-plf-datatable-clear-selection>Clear selection</button>
<button class="btn btn-outline btn-sm" data-plf-datatable-action="review">Review selected</button>
</div>
<div class="table-wrapper" tabindex="0" role="region" aria-labelledby="invoices-caption">
<table class="table table-bordered table-zebra" style="--plf-table-min-width: 38rem">
<caption id="invoices-caption">Invoices</caption>
<thead>
<tr>
<th scope="col" data-plf-column="selection" data-plf-no-export data-plf-width="64">
<span class="sr-only">Select rows</span>
<input class="checkbox" type="checkbox" hidden aria-label="Select all rows on the current invoice page">
</th>
<th scope="col" data-plf-column="invoice" data-plf-width="150" data-plf-searchable data-plf-datatable-sortable>Invoice</th>
<th scope="col" data-plf-column="name" data-plf-width="220" data-plf-searchable data-plf-datatable-sortable>Customer</th>
<th scope="col" data-plf-column="amount" data-plf-type="number" data-plf-width="130" data-plf-datatable-sortable>Amount</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<input class="checkbox" type="checkbox" hidden aria-label="Select invoice 1">
</td>
<th scope="row">INV-001</th>
<td>Avery Stone</td>
<td data-plf-value="0.5">$0.50</td>
</tr>
<tr>
<td>
<input class="checkbox" type="checkbox" hidden aria-label="Select invoice 2">
</td>
<th scope="row">INV-002</th>
<td>Blair Chen</td>
<td data-plf-value="137.5">$137.50</td>
</tr>
<tr>
<td>
<input class="checkbox" type="checkbox" hidden aria-label="Select invoice 3">
</td>
<th scope="row">INV-003</th>
<td>Casey Rivera</td>
<td data-plf-value="274.5">$274.50</td>
</tr>
<tr>
<td>
<input class="checkbox" type="checkbox" hidden aria-label="Select invoice 4">
</td>
<th scope="row">INV-004</th>
<td>Devon Patel</td>
<td data-plf-value="411.5">$411.50</td>
</tr>
<tr>
<td>
<input class="checkbox" type="checkbox" hidden aria-label="Select invoice 5">
</td>
<th scope="row">INV-005</th>
<td>Elliot Brooks</td>
<td data-plf-value="548.5">$548.50</td>
</tr>
<tr>
<td>
<input class="checkbox" type="checkbox" hidden aria-label="Select invoice 6">
</td>
<th scope="row">INV-006</th>
<td>Finley Hart</td>
<td data-plf-value="685.5">$685.50</td>
</tr>
</tbody>
</table>
</div>
<div class="datatable-empty" data-plf-datatable-empty hidden>No invoices match this search.</div>
<div class="datatable-footer" data-plf-datatable-controls hidden>
<p class="datatable-status" role="status"><span data-plf-datatable-count="from">0</span>–<span data-plf-datatable-count="to">0</span> of <span data-plf-datatable-count="matched">0</span> invoices<span class="sr-only"> · Page <span data-plf-datatable-count="page">0</span> of <span data-plf-datatable-count="pages">0</span></span></p>
<div class="datatable-pagination">
<label class="datatable-page-size" for="invoices-size">
Rows per page
<select class="select select-sm" id="invoices-size" data-plf-datatable-page-size-control data-plf-select>
<option>10</option>
<option>25</option>
<option>50</option>
<option>100</option>
</select>
</label>
<nav class="datatable-pages" aria-label="Invoice result pages">
<button class="btn btn-outline btn-sm btn-square" aria-label="First page" data-plf-datatable-first></button>
<button class="btn btn-outline btn-sm btn-square" aria-label="Previous page" data-plf-datatable-previous></button>
<span class="datatable-page-jump">
<label class="sr-only" for="invoices-page">Page</label>
<input class="input input-sm datatable-page-input" id="invoices-page" type="number" inputmode="numeric">
<span>of <span data-plf-datatable-count="pages">0</span></span>
</span>
<button class="btn btn-outline btn-sm btn-square" aria-label="Next page" data-plf-datatable-next></button>
<button class="btn btn-outline btn-sm btn-square" aria-label="Last page" data-plf-datatable-last></button>
</nav>
</div>
</div>
</div>
<script>
document.getElementById('invoices').addEventListener('plf:datatable:action', (event) => {
const { action, rows } = event.detail;
if (action === 'review') {
// Send the selected <tr> rows to your application.
}
});
</script><div class="plf:datatable" id="invoices" data-plf-datatable>
<div class="plf:datatable-toolbar" data-plf-datatable-controls hidden>
<div class="plf:field plf:datatable-filter">
<label class="plf:field-label" for="invoices-filter">Search invoices</label>
<div class="plf:input-group plf:input-group-sm">
<span class="plf:icon plf:icon-search"></span>
<input class="plf:input-group-control" id="invoices-filter" type="search" placeholder="Invoice or customer">
</div>
</div>
<div class="plf:datatable-settings">
<div class="plf:popover" data-plf-popover data-plf-align="end">
<button class="plf:btn plf:btn-outline plf:btn-sm">
<span class="plf:icon plf:icon-download"></span>
<span>Export</span>
</button>
<div class="plf:popover-content plf:datatable-export-panel" hidden>
<div class="plf:datatable-export-header">
<h4 class="plf:popover-title">Export table</h4>
<button class="plf:btn plf:btn-ghost plf:btn-sm plf:btn-square" aria-label="Close export" data-plf-popover-close>
<span class="plf:icon plf:icon-x"></span>
</button>
</div>
<p class="plf:popover-description">Choose which rows to download, copy or print.</p>
<div class="plf:field">
<label class="plf:field-label" for="invoices-export-scope">Rows to export</label>
<select class="plf:select plf:select-sm" id="invoices-export-scope" data-plf-select>
<option value="filtered">Matching rows</option>
<option value="all">All rows</option>
<option value="selected">Selected rows</option>
</select>
</div>
<div class="plf:datatable-export-options" role="group" aria-label="Download format">
<button class="plf:btn plf:btn-ghost plf:datatable-export-action" data-plf-datatable-export-format="csv">
<span class="plf:icon plf:icon-file-text"></span>
<span>CSV</span>
<span class="plf:datatable-export-extension" dir="ltr" aria-hidden="true">.csv</span>
</button>
<button class="plf:btn plf:btn-ghost plf:datatable-export-action" data-plf-datatable-export-format="xlsx">
<span class="plf:icon plf:icon-file-spreadsheet"></span>
<span>Excel</span>
<span class="plf:datatable-export-extension" dir="ltr" aria-hidden="true">.xlsx</span>
</button>
<button class="plf:btn plf:btn-ghost plf:datatable-export-action" data-plf-datatable-export-format="pdf">
<span class="plf:icon plf:icon-file-text"></span>
<span>PDF</span>
<span class="plf:datatable-export-extension" dir="ltr" aria-hidden="true">.pdf</span>
</button>
</div>
<div class="plf:datatable-export-utilities" role="group" aria-label="Other export actions">
<button class="plf:btn plf:btn-outline plf:btn-sm" data-plf-datatable-export-format="copy">
<span class="plf:icon plf:icon-clipboard"></span>
<span>Copy</span>
</button>
<button class="plf:btn plf:btn-outline plf:btn-sm" data-plf-datatable-export-format="print">
<span class="plf:icon plf:icon-printer"></span>
<span>Print</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="plf:datatable-bulk" data-plf-datatable-bulk hidden>
<span><span data-plf-datatable-count="selected">0</span> selected</span>
<button class="plf:btn plf:btn-outline plf:btn-sm" data-plf-datatable-select-filtered>Select all matching</button>
<button class="plf:btn plf:btn-outline plf:btn-sm" data-plf-datatable-clear-selection>Clear selection</button>
<button class="plf:btn plf:btn-outline plf:btn-sm" data-plf-datatable-action="review">Review selected</button>
</div>
<div class="plf:table-wrapper" tabindex="0" role="region" aria-labelledby="invoices-caption">
<table class="plf:table plf:table-bordered plf:table-zebra" style="--plf-table-min-width: 38rem">
<caption id="invoices-caption">Invoices</caption>
<thead>
<tr>
<th scope="col" data-plf-column="selection" data-plf-no-export data-plf-width="64">
<span class="plf:sr-only">Select rows</span>
<input class="plf:checkbox" type="checkbox" hidden aria-label="Select all rows on the current invoice page">
</th>
<th scope="col" data-plf-column="invoice" data-plf-width="150" data-plf-searchable data-plf-datatable-sortable>Invoice</th>
<th scope="col" data-plf-column="name" data-plf-width="220" data-plf-searchable data-plf-datatable-sortable>Customer</th>
<th scope="col" data-plf-column="amount" data-plf-type="number" data-plf-width="130" data-plf-datatable-sortable>Amount</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<input class="plf:checkbox" type="checkbox" hidden aria-label="Select invoice 1">
</td>
<th scope="row">INV-001</th>
<td>Avery Stone</td>
<td data-plf-value="0.5">$0.50</td>
</tr>
<tr>
<td>
<input class="plf:checkbox" type="checkbox" hidden aria-label="Select invoice 2">
</td>
<th scope="row">INV-002</th>
<td>Blair Chen</td>
<td data-plf-value="137.5">$137.50</td>
</tr>
<tr>
<td>
<input class="plf:checkbox" type="checkbox" hidden aria-label="Select invoice 3">
</td>
<th scope="row">INV-003</th>
<td>Casey Rivera</td>
<td data-plf-value="274.5">$274.50</td>
</tr>
<tr>
<td>
<input class="plf:checkbox" type="checkbox" hidden aria-label="Select invoice 4">
</td>
<th scope="row">INV-004</th>
<td>Devon Patel</td>
<td data-plf-value="411.5">$411.50</td>
</tr>
<tr>
<td>
<input class="plf:checkbox" type="checkbox" hidden aria-label="Select invoice 5">
</td>
<th scope="row">INV-005</th>
<td>Elliot Brooks</td>
<td data-plf-value="548.5">$548.50</td>
</tr>
<tr>
<td>
<input class="plf:checkbox" type="checkbox" hidden aria-label="Select invoice 6">
</td>
<th scope="row">INV-006</th>
<td>Finley Hart</td>
<td data-plf-value="685.5">$685.50</td>
</tr>
</tbody>
</table>
</div>
<div class="plf:datatable-empty" data-plf-datatable-empty hidden>No invoices match this search.</div>
<div class="plf:datatable-footer" data-plf-datatable-controls hidden>
<p class="plf:datatable-status" role="status"><span data-plf-datatable-count="from">0</span>–<span data-plf-datatable-count="to">0</span> of <span data-plf-datatable-count="matched">0</span> invoices<span class="plf:sr-only"> · Page <span data-plf-datatable-count="page">0</span> of <span data-plf-datatable-count="pages">0</span></span></p>
<div class="plf:datatable-pagination">
<label class="plf:datatable-page-size" for="invoices-size">
Rows per page
<select class="plf:select plf:select-sm" id="invoices-size" data-plf-datatable-page-size-control data-plf-select>
<option>10</option>
<option>25</option>
<option>50</option>
<option>100</option>
</select>
</label>
<nav class="plf:datatable-pages" aria-label="Invoice result pages">
<button class="plf:btn plf:btn-outline plf:btn-sm plf:btn-square" aria-label="First page" data-plf-datatable-first></button>
<button class="plf:btn plf:btn-outline plf:btn-sm plf:btn-square" aria-label="Previous page" data-plf-datatable-previous></button>
<span class="plf:datatable-page-jump">
<label class="plf:sr-only" for="invoices-page">Page</label>
<input class="plf:input plf:input-sm plf:datatable-page-input" id="invoices-page" type="number" inputmode="numeric">
<span>of <span data-plf-datatable-count="pages">0</span></span>
</span>
<button class="plf:btn plf:btn-outline plf:btn-sm plf:btn-square" aria-label="Next page" data-plf-datatable-next></button>
<button class="plf:btn plf:btn-outline plf:btn-sm plf:btn-square" aria-label="Last page" data-plf-datatable-last></button>
</nav>
</div>
</div>
</div>
<script>
document.getElementById('invoices').addEventListener('plf:datatable:action', (event) => {
const { action, rows } = event.detail;
if (action === 'review') {
// Send the selected <tr> rows to your application.
}
});
</script>| Project | Description |
|---|---|
| Customer guide | A long project description stays readable and wraps inside its cell while the table scrolls. |
| Customer onboarding | https://example.test/projects/customer-onboarding-with-a-very-long-unbroken-identifier-and-shared-review-resources |
| Partner portal | The first column stays in view when you scroll sideways. |
<div class="datatable" data-plf-datatable data-plf-sticky="both" data-plf-page-size="2" style="--plf-datatable-max-height: 18rem">
<div class="datatable-toolbar" data-plf-datatable-controls hidden>
<div class="field datatable-filter">
<label class="field-label" for="projects-filter">Search projects</label>
<div class="input-group input-group-sm">
<span class="icon icon-search"></span>
<input class="input-group-control" id="projects-filter" type="search">
</div>
</div>
</div>
<div class="table-wrapper" tabindex="0" role="region" aria-labelledby="projects-caption">
<table class="table table-zebra" style="--plf-table-min-width: 42rem">
<caption id="projects-caption">Projects</caption>
<thead>
<tr>
<th scope="col" data-plf-column="project" data-plf-width="210" data-plf-searchable data-plf-datatable-sortable>Project</th>
<th scope="col" data-plf-column="description" data-plf-width="380" data-plf-searchable data-plf-datatable-sortable>Description</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Customer guide</th>
<td>A long project description stays readable and wraps inside its cell while the table scrolls.</td>
</tr>
<tr>
<th scope="row">Customer onboarding</th>
<td>https://example.test/projects/customer-onboarding-with-a-very-long-unbroken-identifier-and-shared-review-resources</td>
</tr>
<tr>
<th scope="row">Partner portal</th>
<td>The first column stays in view when you scroll sideways.</td>
</tr>
</tbody>
</table>
</div>
<div class="datatable-empty" data-plf-datatable-empty hidden>No projects match.</div>
<div class="datatable-footer" data-plf-datatable-controls hidden>
<p class="datatable-status" role="status"><span data-plf-datatable-count="from">0</span>–<span data-plf-datatable-count="to">0</span> of <span data-plf-datatable-count="matched">0</span> projects<span class="sr-only"> · Page <span data-plf-datatable-count="page">0</span> of <span data-plf-datatable-count="pages">0</span></span></p>
<div class="datatable-pagination">
<nav class="datatable-pages" aria-label="Project result pages">
<button class="btn btn-outline btn-sm btn-square" aria-label="First page" data-plf-datatable-first></button>
<button class="btn btn-outline btn-sm btn-square" aria-label="Previous page" data-plf-datatable-previous></button>
<span class="datatable-page-jump">
<label class="sr-only" for="projects-page">Page</label>
<input class="input input-sm datatable-page-input" id="projects-page" type="number" inputmode="numeric">
<span>of <span data-plf-datatable-count="pages">0</span></span>
</span>
<button class="btn btn-outline btn-sm btn-square" aria-label="Next page" data-plf-datatable-next></button>
<button class="btn btn-outline btn-sm btn-square" aria-label="Last page" data-plf-datatable-last></button>
</nav>
</div>
</div>
</div><div class="plf:datatable" data-plf-datatable data-plf-sticky="both" data-plf-page-size="2" style="--plf-datatable-max-height: 18rem">
<div class="plf:datatable-toolbar" data-plf-datatable-controls hidden>
<div class="plf:field plf:datatable-filter">
<label class="plf:field-label" for="projects-filter">Search projects</label>
<div class="plf:input-group plf:input-group-sm">
<span class="plf:icon plf:icon-search"></span>
<input class="plf:input-group-control" id="projects-filter" type="search">
</div>
</div>
</div>
<div class="plf:table-wrapper" tabindex="0" role="region" aria-labelledby="projects-caption">
<table class="plf:table plf:table-zebra" style="--plf-table-min-width: 42rem">
<caption id="projects-caption">Projects</caption>
<thead>
<tr>
<th scope="col" data-plf-column="project" data-plf-width="210" data-plf-searchable data-plf-datatable-sortable>Project</th>
<th scope="col" data-plf-column="description" data-plf-width="380" data-plf-searchable data-plf-datatable-sortable>Description</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Customer guide</th>
<td>A long project description stays readable and wraps inside its cell while the table scrolls.</td>
</tr>
<tr>
<th scope="row">Customer onboarding</th>
<td>https://example.test/projects/customer-onboarding-with-a-very-long-unbroken-identifier-and-shared-review-resources</td>
</tr>
<tr>
<th scope="row">Partner portal</th>
<td>The first column stays in view when you scroll sideways.</td>
</tr>
</tbody>
</table>
</div>
<div class="plf:datatable-empty" data-plf-datatable-empty hidden>No projects match.</div>
<div class="plf:datatable-footer" data-plf-datatable-controls hidden>
<p class="plf:datatable-status" role="status"><span data-plf-datatable-count="from">0</span>–<span data-plf-datatable-count="to">0</span> of <span data-plf-datatable-count="matched">0</span> projects<span class="plf:sr-only"> · Page <span data-plf-datatable-count="page">0</span> of <span data-plf-datatable-count="pages">0</span></span></p>
<div class="plf:datatable-pagination">
<nav class="plf:datatable-pages" aria-label="Project result pages">
<button class="plf:btn plf:btn-outline plf:btn-sm plf:btn-square" aria-label="First page" data-plf-datatable-first></button>
<button class="plf:btn plf:btn-outline plf:btn-sm plf:btn-square" aria-label="Previous page" data-plf-datatable-previous></button>
<span class="plf:datatable-page-jump">
<label class="plf:sr-only" for="projects-page">Page</label>
<input class="plf:input plf:input-sm plf:datatable-page-input" id="projects-page" type="number" inputmode="numeric">
<span>of <span data-plf-datatable-count="pages">0</span></span>
</span>
<button class="plf:btn plf:btn-outline plf:btn-sm plf:btn-square" aria-label="Next page" data-plf-datatable-next></button>
<button class="plf:btn plf:btn-outline plf:btn-sm plf:btn-square" aria-label="Last page" data-plf-datatable-last></button>
</nav>
</div>
</div>
</div>Empty
With an empty body, author the data-plf-datatable-empty message without hidden, so it shows before and after the script runs.
| Name |
|---|
<div class="datatable" data-plf-datatable>
<div class="datatable-toolbar" data-plf-datatable-controls hidden>
<div class="field datatable-filter">
<label class="field-label" for="empty-filter">Search contacts</label>
<div class="input-group input-group-sm">
<span class="icon icon-search"></span>
<input class="input-group-control" id="empty-filter" type="search" placeholder="Name or email">
</div>
</div>
</div>
<div class="table-wrapper" tabindex="0" role="region" aria-labelledby="empty-caption">
<table class="table table-bordered">
<caption id="empty-caption">No contacts yet</caption>
<thead>
<tr>
<th scope="col" data-plf-column="name" data-plf-searchable>Name</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="datatable-empty" data-plf-datatable-empty>Your contact list is empty.</div>
<div class="datatable-footer" data-plf-datatable-controls hidden>
<p class="datatable-status" role="status"><span data-plf-datatable-count="from">0</span>–<span data-plf-datatable-count="to">0</span> of <span data-plf-datatable-count="matched">0</span> contacts<span class="sr-only"> · Page <span data-plf-datatable-count="page">0</span> of <span data-plf-datatable-count="pages">0</span></span></p>
<div class="datatable-pagination">
<nav class="datatable-pages" aria-label="Empty result pages">
<button class="btn btn-outline btn-sm btn-square" aria-label="First page" data-plf-datatable-first></button>
<button class="btn btn-outline btn-sm btn-square" aria-label="Previous page" data-plf-datatable-previous></button>
<span class="datatable-page-jump">
<label class="sr-only" for="empty-page">Page</label>
<input class="input input-sm datatable-page-input" id="empty-page" type="number" inputmode="numeric">
<span>of <span data-plf-datatable-count="pages">0</span></span>
</span>
<button class="btn btn-outline btn-sm btn-square" aria-label="Next page" data-plf-datatable-next></button>
<button class="btn btn-outline btn-sm btn-square" aria-label="Last page" data-plf-datatable-last></button>
</nav>
</div>
</div>
</div><div class="plf:datatable" data-plf-datatable>
<div class="plf:datatable-toolbar" data-plf-datatable-controls hidden>
<div class="plf:field plf:datatable-filter">
<label class="plf:field-label" for="empty-filter">Search contacts</label>
<div class="plf:input-group plf:input-group-sm">
<span class="plf:icon plf:icon-search"></span>
<input class="plf:input-group-control" id="empty-filter" type="search" placeholder="Name or email">
</div>
</div>
</div>
<div class="plf:table-wrapper" tabindex="0" role="region" aria-labelledby="empty-caption">
<table class="plf:table plf:table-bordered">
<caption id="empty-caption">No contacts yet</caption>
<thead>
<tr>
<th scope="col" data-plf-column="name" data-plf-searchable>Name</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="plf:datatable-empty" data-plf-datatable-empty>Your contact list is empty.</div>
<div class="plf:datatable-footer" data-plf-datatable-controls hidden>
<p class="plf:datatable-status" role="status"><span data-plf-datatable-count="from">0</span>–<span data-plf-datatable-count="to">0</span> of <span data-plf-datatable-count="matched">0</span> contacts<span class="plf:sr-only"> · Page <span data-plf-datatable-count="page">0</span> of <span data-plf-datatable-count="pages">0</span></span></p>
<div class="plf:datatable-pagination">
<nav class="plf:datatable-pages" aria-label="Empty result pages">
<button class="plf:btn plf:btn-outline plf:btn-sm plf:btn-square" aria-label="First page" data-plf-datatable-first></button>
<button class="plf:btn plf:btn-outline plf:btn-sm plf:btn-square" aria-label="Previous page" data-plf-datatable-previous></button>
<span class="plf:datatable-page-jump">
<label class="plf:sr-only" for="empty-page">Page</label>
<input class="plf:input plf:input-sm plf:datatable-page-input" id="empty-page" type="number" inputmode="numeric">
<span>of <span data-plf-datatable-count="pages">0</span></span>
</span>
<button class="plf:btn plf:btn-outline plf:btn-sm plf:btn-square" aria-label="Next page" data-plf-datatable-next></button>
<button class="plf:btn plf:btn-outline plf:btn-sm plf:btn-square" aria-label="Last page" data-plf-datatable-last></button>
</nav>
</div>
</div>
</div>| Customer | Status | Revenue | Joined | |
|---|---|---|---|---|
| Avery Stone | avery.stone@example.test | Customer | $0.50 | 2026-01-01 |
| Blair Chen | blair.chen@example.test | Trial | $137.50 | 2026-02-02 |
| Casey Rivera | casey.rivera@example.test | Paused | $274.50 | 2026-03-03 |
| Devon Patel | devon.patel@example.test | Customer | $411.50 | 2026-04-04 |
| Elliot Brooks | elliot.brooks@example.test | Trial | $548.50 | 2026-05-05 |
| Finley Hart | finley.hart@example.test | Paused | $685.50 | 2026-06-06 |
<div class="datatable" data-plf-datatable>
<div class="datatable-toolbar" data-plf-datatable-controls hidden>
<div class="field datatable-filter">
<label class="field-label" for="density-filter">Search customers</label>
<div class="input-group input-group-sm">
<span class="icon icon-search"></span>
<input class="input-group-control" id="density-filter" type="search" placeholder="Customer, email or status">
</div>
</div>
<div class="datatable-settings">
<label class="datatable-density" for="density-control">
Density
<select class="select select-sm" id="density-control" data-plf-select>
<option value="comfortable">Comfortable</option>
<option value="compact">Compact</option>
</select>
</label>
</div>
</div>
<div class="table-wrapper" tabindex="0" role="region" aria-labelledby="density-caption">
<table class="table table-zebra" style="--plf-table-min-width: 48rem">
<caption id="density-caption">Customer list</caption>
<thead>
<tr>
<th scope="col" data-plf-column="name" data-plf-searchable>Customer</th>
<th scope="col" data-plf-column="email" data-plf-searchable>Email</th>
<th scope="col" data-plf-column="status" data-plf-searchable>Status</th>
<th scope="col" data-plf-column="revenue">Revenue</th>
<th scope="col" data-plf-column="joined">Joined</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Avery Stone</th>
<td>avery.stone@example.test</td>
<td><span class="badge badge-success badge-soft">Customer</span></td>
<td>$0.50</td>
<td>2026-01-01</td>
</tr>
<tr>
<th scope="row">Blair Chen</th>
<td>blair.chen@example.test</td>
<td><span class="badge badge-outline">Trial</span></td>
<td>$137.50</td>
<td>2026-02-02</td>
</tr>
<tr>
<th scope="row">Casey Rivera</th>
<td>casey.rivera@example.test</td>
<td><span class="badge badge-neutral badge-soft">Paused</span></td>
<td>$274.50</td>
<td>2026-03-03</td>
</tr>
<tr>
<th scope="row">Devon Patel</th>
<td>devon.patel@example.test</td>
<td><span class="badge badge-success badge-soft">Customer</span></td>
<td>$411.50</td>
<td>2026-04-04</td>
</tr>
<tr>
<th scope="row">Elliot Brooks</th>
<td>elliot.brooks@example.test</td>
<td><span class="badge badge-outline">Trial</span></td>
<td>$548.50</td>
<td>2026-05-05</td>
</tr>
<tr>
<th scope="row">Finley Hart</th>
<td>finley.hart@example.test</td>
<td><span class="badge badge-neutral badge-soft">Paused</span></td>
<td>$685.50</td>
<td>2026-06-06</td>
</tr>
</tbody>
</table>
</div>
<div class="datatable-empty" data-plf-datatable-empty hidden>No customers match your search.</div>
<div class="datatable-footer" data-plf-datatable-controls hidden>
<p class="datatable-status" role="status"><span data-plf-datatable-count="from">0</span>–<span data-plf-datatable-count="to">0</span> of <span data-plf-datatable-count="matched">0</span> customers<span class="sr-only"> · Page <span data-plf-datatable-count="page">0</span> of <span data-plf-datatable-count="pages">0</span></span></p>
<div class="datatable-pagination">
<nav class="datatable-pages" aria-label="Customer list pages">
<button class="btn btn-outline btn-sm btn-square" aria-label="Previous page" data-plf-datatable-previous></button>
<button class="btn btn-outline btn-sm btn-square" aria-label="Next page" data-plf-datatable-next></button>
</nav>
</div>
</div>
</div><div class="plf:datatable" data-plf-datatable>
<div class="plf:datatable-toolbar" data-plf-datatable-controls hidden>
<div class="plf:field plf:datatable-filter">
<label class="plf:field-label" for="density-filter">Search customers</label>
<div class="plf:input-group plf:input-group-sm">
<span class="plf:icon plf:icon-search"></span>
<input class="plf:input-group-control" id="density-filter" type="search" placeholder="Customer, email or status">
</div>
</div>
<div class="plf:datatable-settings">
<label class="plf:datatable-density" for="density-control">
Density
<select class="plf:select plf:select-sm" id="density-control" data-plf-select>
<option value="comfortable">Comfortable</option>
<option value="compact">Compact</option>
</select>
</label>
</div>
</div>
<div class="plf:table-wrapper" tabindex="0" role="region" aria-labelledby="density-caption">
<table class="plf:table plf:table-zebra" style="--plf-table-min-width: 48rem">
<caption id="density-caption">Customer list</caption>
<thead>
<tr>
<th scope="col" data-plf-column="name" data-plf-searchable>Customer</th>
<th scope="col" data-plf-column="email" data-plf-searchable>Email</th>
<th scope="col" data-plf-column="status" data-plf-searchable>Status</th>
<th scope="col" data-plf-column="revenue">Revenue</th>
<th scope="col" data-plf-column="joined">Joined</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Avery Stone</th>
<td>avery.stone@example.test</td>
<td><span class="plf:badge plf:badge-success plf:badge-soft">Customer</span></td>
<td>$0.50</td>
<td>2026-01-01</td>
</tr>
<tr>
<th scope="row">Blair Chen</th>
<td>blair.chen@example.test</td>
<td><span class="plf:badge plf:badge-outline">Trial</span></td>
<td>$137.50</td>
<td>2026-02-02</td>
</tr>
<tr>
<th scope="row">Casey Rivera</th>
<td>casey.rivera@example.test</td>
<td><span class="plf:badge plf:badge-neutral plf:badge-soft">Paused</span></td>
<td>$274.50</td>
<td>2026-03-03</td>
</tr>
<tr>
<th scope="row">Devon Patel</th>
<td>devon.patel@example.test</td>
<td><span class="plf:badge plf:badge-success plf:badge-soft">Customer</span></td>
<td>$411.50</td>
<td>2026-04-04</td>
</tr>
<tr>
<th scope="row">Elliot Brooks</th>
<td>elliot.brooks@example.test</td>
<td><span class="plf:badge plf:badge-outline">Trial</span></td>
<td>$548.50</td>
<td>2026-05-05</td>
</tr>
<tr>
<th scope="row">Finley Hart</th>
<td>finley.hart@example.test</td>
<td><span class="plf:badge plf:badge-neutral plf:badge-soft">Paused</span></td>
<td>$685.50</td>
<td>2026-06-06</td>
</tr>
</tbody>
</table>
</div>
<div class="plf:datatable-empty" data-plf-datatable-empty hidden>No customers match your search.</div>
<div class="plf:datatable-footer" data-plf-datatable-controls hidden>
<p class="plf:datatable-status" role="status"><span data-plf-datatable-count="from">0</span>–<span data-plf-datatable-count="to">0</span> of <span data-plf-datatable-count="matched">0</span> customers<span class="plf:sr-only"> · Page <span data-plf-datatable-count="page">0</span> of <span data-plf-datatable-count="pages">0</span></span></p>
<div class="plf:datatable-pagination">
<nav class="plf:datatable-pages" aria-label="Customer list pages">
<button class="plf:btn plf:btn-outline plf:btn-sm plf:btn-square" aria-label="Previous page" data-plf-datatable-previous></button>
<button class="plf:btn plf:btn-outline plf:btn-sm plf:btn-square" aria-label="Next page" data-plf-datatable-next></button>
</nav>
</div>
</div>
</div>Multi-column sort
Shift+click or Shift+Enter on a sort button adds its column to the sort, and datatable-sort-rank shows its place.
| Customer | Status | Revenue | Joined | |
|---|---|---|---|---|
| Avery Stone | avery.stone@example.test | Customer | $0.50 | 2026-01-01 |
| Blair Chen | blair.chen@example.test | Trial | $137.50 | 2026-02-02 |
| Casey Rivera | casey.rivera@example.test | Paused | $274.50 | 2026-03-03 |
| Devon Patel | devon.patel@example.test | Customer | $411.50 | 2026-04-04 |
| Elliot Brooks | elliot.brooks@example.test | Trial | $548.50 | 2026-05-05 |
| Finley Hart | finley.hart@example.test | Paused | $685.50 | 2026-06-06 |
<div class="datatable" data-plf-datatable data-plf-multi-sort>
<div class="datatable-toolbar" data-plf-datatable-controls hidden>
<div class="field datatable-filter">
<label class="field-label" for="sort-filter">Search customers</label>
<div class="input-group input-group-sm">
<span class="icon icon-search"></span>
<input class="input-group-control" id="sort-filter" type="search" placeholder="Customer, email or status">
</div>
</div>
</div>
<div class="table-wrapper" tabindex="0" role="region" aria-labelledby="sort-caption">
<table class="table table-zebra" style="--plf-table-min-width: 48rem">
<caption id="sort-caption">Customer revenue</caption>
<thead>
<tr>
<th scope="col" data-plf-column="name" data-plf-searchable data-plf-datatable-sortable>Customer</th>
<th scope="col" data-plf-column="email" data-plf-searchable>Email</th>
<th scope="col" data-plf-column="status" data-plf-searchable data-plf-datatable-sortable>Status</th>
<th scope="col" data-plf-column="revenue" data-plf-type="number" data-plf-datatable-sortable>Revenue</th>
<th scope="col" data-plf-column="joined" data-plf-type="date" data-plf-datatable-sortable>Joined</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Avery Stone</th>
<td>avery.stone@example.test</td>
<td><span class="badge badge-success badge-soft">Customer</span></td>
<td data-plf-value="0.5">$0.50</td>
<td data-plf-value="2026-01-01">2026-01-01</td>
</tr>
<tr>
<th scope="row">Blair Chen</th>
<td>blair.chen@example.test</td>
<td><span class="badge badge-outline">Trial</span></td>
<td data-plf-value="137.5">$137.50</td>
<td data-plf-value="2026-02-02">2026-02-02</td>
</tr>
<tr>
<th scope="row">Casey Rivera</th>
<td>casey.rivera@example.test</td>
<td><span class="badge badge-neutral badge-soft">Paused</span></td>
<td data-plf-value="274.5">$274.50</td>
<td data-plf-value="2026-03-03">2026-03-03</td>
</tr>
<tr>
<th scope="row">Devon Patel</th>
<td>devon.patel@example.test</td>
<td><span class="badge badge-success badge-soft">Customer</span></td>
<td data-plf-value="411.5">$411.50</td>
<td data-plf-value="2026-04-04">2026-04-04</td>
</tr>
<tr>
<th scope="row">Elliot Brooks</th>
<td>elliot.brooks@example.test</td>
<td><span class="badge badge-outline">Trial</span></td>
<td data-plf-value="548.5">$548.50</td>
<td data-plf-value="2026-05-05">2026-05-05</td>
</tr>
<tr>
<th scope="row">Finley Hart</th>
<td>finley.hart@example.test</td>
<td><span class="badge badge-neutral badge-soft">Paused</span></td>
<td data-plf-value="685.5">$685.50</td>
<td data-plf-value="2026-06-06">2026-06-06</td>
</tr>
</tbody>
</table>
</div>
<div class="datatable-empty" data-plf-datatable-empty hidden>No customers match your search.</div>
<div class="datatable-footer" data-plf-datatable-controls hidden>
<p class="datatable-status" role="status"><span data-plf-datatable-count="from">0</span>–<span data-plf-datatable-count="to">0</span> of <span data-plf-datatable-count="matched">0</span> customers<span class="sr-only"> · Page <span data-plf-datatable-count="page">0</span> of <span data-plf-datatable-count="pages">0</span></span></p>
<div class="datatable-pagination">
<nav class="datatable-pages" aria-label="Customer revenue pages">
<button class="btn btn-outline btn-sm btn-square" aria-label="Previous page" data-plf-datatable-previous></button>
<button class="btn btn-outline btn-sm btn-square" aria-label="Next page" data-plf-datatable-next></button>
</nav>
</div>
</div>
</div><div class="plf:datatable" data-plf-datatable data-plf-multi-sort>
<div class="plf:datatable-toolbar" data-plf-datatable-controls hidden>
<div class="plf:field plf:datatable-filter">
<label class="plf:field-label" for="sort-filter">Search customers</label>
<div class="plf:input-group plf:input-group-sm">
<span class="plf:icon plf:icon-search"></span>
<input class="plf:input-group-control" id="sort-filter" type="search" placeholder="Customer, email or status">
</div>
</div>
</div>
<div class="plf:table-wrapper" tabindex="0" role="region" aria-labelledby="sort-caption">
<table class="plf:table plf:table-zebra" style="--plf-table-min-width: 48rem">
<caption id="sort-caption">Customer revenue</caption>
<thead>
<tr>
<th scope="col" data-plf-column="name" data-plf-searchable data-plf-datatable-sortable>Customer</th>
<th scope="col" data-plf-column="email" data-plf-searchable>Email</th>
<th scope="col" data-plf-column="status" data-plf-searchable data-plf-datatable-sortable>Status</th>
<th scope="col" data-plf-column="revenue" data-plf-type="number" data-plf-datatable-sortable>Revenue</th>
<th scope="col" data-plf-column="joined" data-plf-type="date" data-plf-datatable-sortable>Joined</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Avery Stone</th>
<td>avery.stone@example.test</td>
<td><span class="plf:badge plf:badge-success plf:badge-soft">Customer</span></td>
<td data-plf-value="0.5">$0.50</td>
<td data-plf-value="2026-01-01">2026-01-01</td>
</tr>
<tr>
<th scope="row">Blair Chen</th>
<td>blair.chen@example.test</td>
<td><span class="plf:badge plf:badge-outline">Trial</span></td>
<td data-plf-value="137.5">$137.50</td>
<td data-plf-value="2026-02-02">2026-02-02</td>
</tr>
<tr>
<th scope="row">Casey Rivera</th>
<td>casey.rivera@example.test</td>
<td><span class="plf:badge plf:badge-neutral plf:badge-soft">Paused</span></td>
<td data-plf-value="274.5">$274.50</td>
<td data-plf-value="2026-03-03">2026-03-03</td>
</tr>
<tr>
<th scope="row">Devon Patel</th>
<td>devon.patel@example.test</td>
<td><span class="plf:badge plf:badge-success plf:badge-soft">Customer</span></td>
<td data-plf-value="411.5">$411.50</td>
<td data-plf-value="2026-04-04">2026-04-04</td>
</tr>
<tr>
<th scope="row">Elliot Brooks</th>
<td>elliot.brooks@example.test</td>
<td><span class="plf:badge plf:badge-outline">Trial</span></td>
<td data-plf-value="548.5">$548.50</td>
<td data-plf-value="2026-05-05">2026-05-05</td>
</tr>
<tr>
<th scope="row">Finley Hart</th>
<td>finley.hart@example.test</td>
<td><span class="plf:badge plf:badge-neutral plf:badge-soft">Paused</span></td>
<td data-plf-value="685.5">$685.50</td>
<td data-plf-value="2026-06-06">2026-06-06</td>
</tr>
</tbody>
</table>
</div>
<div class="plf:datatable-empty" data-plf-datatable-empty hidden>No customers match your search.</div>
<div class="plf:datatable-footer" data-plf-datatable-controls hidden>
<p class="plf:datatable-status" role="status"><span data-plf-datatable-count="from">0</span>–<span data-plf-datatable-count="to">0</span> of <span data-plf-datatable-count="matched">0</span> customers<span class="plf:sr-only"> · Page <span data-plf-datatable-count="page">0</span> of <span data-plf-datatable-count="pages">0</span></span></p>
<div class="plf:datatable-pagination">
<nav class="plf:datatable-pages" aria-label="Customer revenue pages">
<button class="plf:btn plf:btn-outline plf:btn-sm plf:btn-square" aria-label="Previous page" data-plf-datatable-previous></button>
<button class="plf:btn plf:btn-outline plf:btn-sm plf:btn-square" aria-label="Next page" data-plf-datatable-next></button>
</nav>
</div>
</div>
</div>Column settings
Each data-plf-width must fall within its range input’s min and max.
| Customer | Status | Revenue | Joined | |
|---|---|---|---|---|
| Avery Stone | avery.stone@example.test | Customer | $0.50 | 2026-01-01 |
| Blair Chen | blair.chen@example.test | Trial | $137.50 | 2026-02-02 |
| Casey Rivera | casey.rivera@example.test | Paused | $274.50 | 2026-03-03 |
| Devon Patel | devon.patel@example.test | Customer | $411.50 | 2026-04-04 |
| Elliot Brooks | elliot.brooks@example.test | Trial | $548.50 | 2026-05-05 |
| Finley Hart | finley.hart@example.test | Paused | $685.50 | 2026-06-06 |
<div class="datatable" data-plf-datatable data-plf-storage-key="customer-columns">
<div class="datatable-toolbar" data-plf-datatable-controls hidden>
<div class="field datatable-filter">
<label class="field-label" for="columns-filter">Search customers</label>
<div class="input-group input-group-sm">
<span class="icon icon-search"></span>
<input class="input-group-control" id="columns-filter" type="search" placeholder="Customer, email or status">
</div>
</div>
<div class="datatable-settings">
<div class="popover" data-plf-popover data-plf-align="end">
<button class="btn btn-outline btn-sm">
<span class="icon icon-columns-3"></span>
<span>Columns</span>
</button>
<div class="popover-content datatable-columns-panel" hidden style="--plf-popover-width: 24rem">
<div class="datatable-columns-heading">
<h4 class="popover-title">Visible columns</h4>
<button class="btn btn-ghost btn-sm btn-square" aria-label="Close columns" data-plf-popover-close>
<span class="icon icon-x"></span>
</button>
</div>
<div class="datatable-column-settings">
<div class="datatable-column-toggles">
<label class="choice datatable-column-choice">
<input class="checkbox" type="checkbox" data-plf-datatable-column-toggle="name">
<span class="choice-label">Customer (required)</span>
</label>
<label class="choice datatable-column-choice">
<input class="checkbox" type="checkbox" data-plf-datatable-column-toggle="email">
<span class="choice-label">Email</span>
</label>
<label class="choice datatable-column-choice">
<input class="checkbox" type="checkbox" data-plf-datatable-column-toggle="status">
<span class="choice-label">Status</span>
</label>
<label class="choice datatable-column-choice">
<input class="checkbox" type="checkbox" data-plf-datatable-column-toggle="revenue">
<span class="choice-label">Revenue</span>
</label>
<label class="choice datatable-column-choice">
<input class="checkbox" type="checkbox" data-plf-datatable-column-toggle="joined">
<span class="choice-label">Joined</span>
</label>
</div>
<details class="collapse collapse-arrow datatable-disclosure">
<summary class="collapse-trigger">
<span class="collapse-title">Layout</span>
</summary>
<div class="collapse-content datatable-column-settings">
<div class="datatable-column-setting">
<label class="field-label" for="columns-width-name">Customer width</label>
<div class="datatable-column-actions">
<div data-plf-slider>
<div class="range-field">
<input class="range" id="columns-width-name" type="range" min="80" max="480" step="10" data-plf-datatable-column-width="name">
</div>
</div>
</div>
</div>
<div class="datatable-column-setting">
<label class="field-label" for="columns-width-email">Email width</label>
<div class="datatable-column-actions">
<div data-plf-slider>
<div class="range-field">
<input class="range" id="columns-width-email" type="range" min="80" max="480" step="10" data-plf-datatable-column-width="email">
</div>
</div>
<button class="btn btn-outline btn-sm" data-plf-datatable-move-column="email" data-plf-datatable-move="-1" aria-label="Move email earlier">←</button>
<button class="btn btn-outline btn-sm" data-plf-datatable-move-column="email" data-plf-datatable-move="1" aria-label="Move email later">→</button>
</div>
</div>
<div class="datatable-column-setting">
<label class="field-label" for="columns-width-status">Status width</label>
<div class="datatable-column-actions">
<div data-plf-slider>
<div class="range-field">
<input class="range" id="columns-width-status" type="range" min="80" max="480" step="10" data-plf-datatable-column-width="status">
</div>
</div>
<button class="btn btn-outline btn-sm" data-plf-datatable-move-column="status" data-plf-datatable-move="-1" aria-label="Move status earlier">←</button>
<button class="btn btn-outline btn-sm" data-plf-datatable-move-column="status" data-plf-datatable-move="1" aria-label="Move status later">→</button>
</div>
</div>
<div class="datatable-column-setting">
<label class="field-label" for="columns-width-revenue">Revenue width</label>
<div class="datatable-column-actions">
<div data-plf-slider>
<div class="range-field">
<input class="range" id="columns-width-revenue" type="range" min="80" max="480" step="10" data-plf-datatable-column-width="revenue">
</div>
</div>
<button class="btn btn-outline btn-sm" data-plf-datatable-move-column="revenue" data-plf-datatable-move="-1" aria-label="Move revenue earlier">←</button>
<button class="btn btn-outline btn-sm" data-plf-datatable-move-column="revenue" data-plf-datatable-move="1" aria-label="Move revenue later">→</button>
</div>
</div>
<div class="datatable-column-setting">
<label class="field-label" for="columns-width-joined">Joined width</label>
<div class="datatable-column-actions">
<div data-plf-slider>
<div class="range-field">
<input class="range" id="columns-width-joined" type="range" min="80" max="480" step="10" data-plf-datatable-column-width="joined">
</div>
</div>
<button class="btn btn-outline btn-sm" data-plf-datatable-move-column="joined" data-plf-datatable-move="-1" aria-label="Move joined earlier">←</button>
<button class="btn btn-outline btn-sm" data-plf-datatable-move-column="joined" data-plf-datatable-move="1" aria-label="Move joined later">→</button>
</div>
</div>
</div>
</details>
</div>
</div>
</div>
<button class="btn btn-outline btn-sm" data-plf-datatable-reset>
<span class="icon icon-rotate"></span>
<span>Reset view</span>
</button>
</div>
</div>
<div class="table-wrapper" tabindex="0" role="region" aria-labelledby="columns-caption">
<table class="table table-zebra" style="--plf-table-min-width: 48rem">
<caption id="columns-caption">Customer directory</caption>
<thead>
<tr>
<th scope="col" data-plf-column="name" data-plf-width="180" data-plf-searchable data-plf-required data-plf-fixed>Customer</th>
<th scope="col" data-plf-column="email" data-plf-width="240" data-plf-searchable>Email</th>
<th scope="col" data-plf-column="status" data-plf-width="120" data-plf-searchable>Status</th>
<th scope="col" data-plf-column="revenue" data-plf-width="130">Revenue</th>
<th scope="col" data-plf-column="joined" data-plf-width="140">Joined</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Avery Stone</th>
<td>avery.stone@example.test</td>
<td><span class="badge badge-success badge-soft">Customer</span></td>
<td>$0.50</td>
<td>2026-01-01</td>
</tr>
<tr>
<th scope="row">Blair Chen</th>
<td>blair.chen@example.test</td>
<td><span class="badge badge-outline">Trial</span></td>
<td>$137.50</td>
<td>2026-02-02</td>
</tr>
<tr>
<th scope="row">Casey Rivera</th>
<td>casey.rivera@example.test</td>
<td><span class="badge badge-neutral badge-soft">Paused</span></td>
<td>$274.50</td>
<td>2026-03-03</td>
</tr>
<tr>
<th scope="row">Devon Patel</th>
<td>devon.patel@example.test</td>
<td><span class="badge badge-success badge-soft">Customer</span></td>
<td>$411.50</td>
<td>2026-04-04</td>
</tr>
<tr>
<th scope="row">Elliot Brooks</th>
<td>elliot.brooks@example.test</td>
<td><span class="badge badge-outline">Trial</span></td>
<td>$548.50</td>
<td>2026-05-05</td>
</tr>
<tr>
<th scope="row">Finley Hart</th>
<td>finley.hart@example.test</td>
<td><span class="badge badge-neutral badge-soft">Paused</span></td>
<td>$685.50</td>
<td>2026-06-06</td>
</tr>
</tbody>
</table>
</div>
<div class="datatable-empty" data-plf-datatable-empty hidden>No customers match your search.</div>
<div class="datatable-footer" data-plf-datatable-controls hidden>
<p class="datatable-status" role="status"><span data-plf-datatable-count="from">0</span>–<span data-plf-datatable-count="to">0</span> of <span data-plf-datatable-count="matched">0</span> customers<span class="sr-only"> · Page <span data-plf-datatable-count="page">0</span> of <span data-plf-datatable-count="pages">0</span></span></p>
<div class="datatable-pagination">
<nav class="datatable-pages" aria-label="Customer directory pages">
<button class="btn btn-outline btn-sm btn-square" aria-label="Previous page" data-plf-datatable-previous></button>
<button class="btn btn-outline btn-sm btn-square" aria-label="Next page" data-plf-datatable-next></button>
</nav>
</div>
</div>
</div><div class="plf:datatable" data-plf-datatable data-plf-storage-key="customer-columns">
<div class="plf:datatable-toolbar" data-plf-datatable-controls hidden>
<div class="plf:field plf:datatable-filter">
<label class="plf:field-label" for="columns-filter">Search customers</label>
<div class="plf:input-group plf:input-group-sm">
<span class="plf:icon plf:icon-search"></span>
<input class="plf:input-group-control" id="columns-filter" type="search" placeholder="Customer, email or status">
</div>
</div>
<div class="plf:datatable-settings">
<div class="plf:popover" data-plf-popover data-plf-align="end">
<button class="plf:btn plf:btn-outline plf:btn-sm">
<span class="plf:icon plf:icon-columns-3"></span>
<span>Columns</span>
</button>
<div class="plf:popover-content plf:datatable-columns-panel" hidden style="--plf-popover-width: 24rem">
<div class="plf:datatable-columns-heading">
<h4 class="plf:popover-title">Visible columns</h4>
<button class="plf:btn plf:btn-ghost plf:btn-sm plf:btn-square" aria-label="Close columns" data-plf-popover-close>
<span class="plf:icon plf:icon-x"></span>
</button>
</div>
<div class="plf:datatable-column-settings">
<div class="plf:datatable-column-toggles">
<label class="plf:choice plf:datatable-column-choice">
<input class="plf:checkbox" type="checkbox" data-plf-datatable-column-toggle="name">
<span class="plf:choice-label">Customer (required)</span>
</label>
<label class="plf:choice plf:datatable-column-choice">
<input class="plf:checkbox" type="checkbox" data-plf-datatable-column-toggle="email">
<span class="plf:choice-label">Email</span>
</label>
<label class="plf:choice plf:datatable-column-choice">
<input class="plf:checkbox" type="checkbox" data-plf-datatable-column-toggle="status">
<span class="plf:choice-label">Status</span>
</label>
<label class="plf:choice plf:datatable-column-choice">
<input class="plf:checkbox" type="checkbox" data-plf-datatable-column-toggle="revenue">
<span class="plf:choice-label">Revenue</span>
</label>
<label class="plf:choice plf:datatable-column-choice">
<input class="plf:checkbox" type="checkbox" data-plf-datatable-column-toggle="joined">
<span class="plf:choice-label">Joined</span>
</label>
</div>
<details class="plf:collapse plf:collapse-arrow plf:datatable-disclosure">
<summary class="plf:collapse-trigger">
<span class="plf:collapse-title">Layout</span>
</summary>
<div class="plf:collapse-content plf:datatable-column-settings">
<div class="plf:datatable-column-setting">
<label class="plf:field-label" for="columns-width-name">Customer width</label>
<div class="plf:datatable-column-actions">
<div data-plf-slider>
<div class="plf:range-field">
<input class="plf:range" id="columns-width-name" type="range" min="80" max="480" step="10" data-plf-datatable-column-width="name">
</div>
</div>
</div>
</div>
<div class="plf:datatable-column-setting">
<label class="plf:field-label" for="columns-width-email">Email width</label>
<div class="plf:datatable-column-actions">
<div data-plf-slider>
<div class="plf:range-field">
<input class="plf:range" id="columns-width-email" type="range" min="80" max="480" step="10" data-plf-datatable-column-width="email">
</div>
</div>
<button class="plf:btn plf:btn-outline plf:btn-sm" data-plf-datatable-move-column="email" data-plf-datatable-move="-1" aria-label="Move email earlier">←</button>
<button class="plf:btn plf:btn-outline plf:btn-sm" data-plf-datatable-move-column="email" data-plf-datatable-move="1" aria-label="Move email later">→</button>
</div>
</div>
<div class="plf:datatable-column-setting">
<label class="plf:field-label" for="columns-width-status">Status width</label>
<div class="plf:datatable-column-actions">
<div data-plf-slider>
<div class="plf:range-field">
<input class="plf:range" id="columns-width-status" type="range" min="80" max="480" step="10" data-plf-datatable-column-width="status">
</div>
</div>
<button class="plf:btn plf:btn-outline plf:btn-sm" data-plf-datatable-move-column="status" data-plf-datatable-move="-1" aria-label="Move status earlier">←</button>
<button class="plf:btn plf:btn-outline plf:btn-sm" data-plf-datatable-move-column="status" data-plf-datatable-move="1" aria-label="Move status later">→</button>
</div>
</div>
<div class="plf:datatable-column-setting">
<label class="plf:field-label" for="columns-width-revenue">Revenue width</label>
<div class="plf:datatable-column-actions">
<div data-plf-slider>
<div class="plf:range-field">
<input class="plf:range" id="columns-width-revenue" type="range" min="80" max="480" step="10" data-plf-datatable-column-width="revenue">
</div>
</div>
<button class="plf:btn plf:btn-outline plf:btn-sm" data-plf-datatable-move-column="revenue" data-plf-datatable-move="-1" aria-label="Move revenue earlier">←</button>
<button class="plf:btn plf:btn-outline plf:btn-sm" data-plf-datatable-move-column="revenue" data-plf-datatable-move="1" aria-label="Move revenue later">→</button>
</div>
</div>
<div class="plf:datatable-column-setting">
<label class="plf:field-label" for="columns-width-joined">Joined width</label>
<div class="plf:datatable-column-actions">
<div data-plf-slider>
<div class="plf:range-field">
<input class="plf:range" id="columns-width-joined" type="range" min="80" max="480" step="10" data-plf-datatable-column-width="joined">
</div>
</div>
<button class="plf:btn plf:btn-outline plf:btn-sm" data-plf-datatable-move-column="joined" data-plf-datatable-move="-1" aria-label="Move joined earlier">←</button>
<button class="plf:btn plf:btn-outline plf:btn-sm" data-plf-datatable-move-column="joined" data-plf-datatable-move="1" aria-label="Move joined later">→</button>
</div>
</div>
</div>
</details>
</div>
</div>
</div>
<button class="plf:btn plf:btn-outline plf:btn-sm" data-plf-datatable-reset>
<span class="plf:icon plf:icon-rotate"></span>
<span>Reset view</span>
</button>
</div>
</div>
<div class="plf:table-wrapper" tabindex="0" role="region" aria-labelledby="columns-caption">
<table class="plf:table plf:table-zebra" style="--plf-table-min-width: 48rem">
<caption id="columns-caption">Customer directory</caption>
<thead>
<tr>
<th scope="col" data-plf-column="name" data-plf-width="180" data-plf-searchable data-plf-required data-plf-fixed>Customer</th>
<th scope="col" data-plf-column="email" data-plf-width="240" data-plf-searchable>Email</th>
<th scope="col" data-plf-column="status" data-plf-width="120" data-plf-searchable>Status</th>
<th scope="col" data-plf-column="revenue" data-plf-width="130">Revenue</th>
<th scope="col" data-plf-column="joined" data-plf-width="140">Joined</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Avery Stone</th>
<td>avery.stone@example.test</td>
<td><span class="plf:badge plf:badge-success plf:badge-soft">Customer</span></td>
<td>$0.50</td>
<td>2026-01-01</td>
</tr>
<tr>
<th scope="row">Blair Chen</th>
<td>blair.chen@example.test</td>
<td><span class="plf:badge plf:badge-outline">Trial</span></td>
<td>$137.50</td>
<td>2026-02-02</td>
</tr>
<tr>
<th scope="row">Casey Rivera</th>
<td>casey.rivera@example.test</td>
<td><span class="plf:badge plf:badge-neutral plf:badge-soft">Paused</span></td>
<td>$274.50</td>
<td>2026-03-03</td>
</tr>
<tr>
<th scope="row">Devon Patel</th>
<td>devon.patel@example.test</td>
<td><span class="plf:badge plf:badge-success plf:badge-soft">Customer</span></td>
<td>$411.50</td>
<td>2026-04-04</td>
</tr>
<tr>
<th scope="row">Elliot Brooks</th>
<td>elliot.brooks@example.test</td>
<td><span class="plf:badge plf:badge-outline">Trial</span></td>
<td>$548.50</td>
<td>2026-05-05</td>
</tr>
<tr>
<th scope="row">Finley Hart</th>
<td>finley.hart@example.test</td>
<td><span class="plf:badge plf:badge-neutral plf:badge-soft">Paused</span></td>
<td>$685.50</td>
<td>2026-06-06</td>
</tr>
</tbody>
</table>
</div>
<div class="plf:datatable-empty" data-plf-datatable-empty hidden>No customers match your search.</div>
<div class="plf:datatable-footer" data-plf-datatable-controls hidden>
<p class="plf:datatable-status" role="status"><span data-plf-datatable-count="from">0</span>–<span data-plf-datatable-count="to">0</span> of <span data-plf-datatable-count="matched">0</span> customers<span class="plf:sr-only"> · Page <span data-plf-datatable-count="page">0</span> of <span data-plf-datatable-count="pages">0</span></span></p>
<div class="plf:datatable-pagination">
<nav class="plf:datatable-pages" aria-label="Customer directory pages">
<button class="plf:btn plf:btn-outline plf:btn-sm plf:btn-square" aria-label="Previous page" data-plf-datatable-previous></button>
<button class="plf:btn plf:btn-outline plf:btn-sm plf:btn-square" aria-label="Next page" data-plf-datatable-next></button>
</nav>
</div>
</div>
</div>Filters
Range inputs need their column’s type: number inputs for a number column, date inputs for a date column.
Filters
| Customer | Status | Revenue | Joined | |
|---|---|---|---|---|
| Avery Stone | avery.stone@example.test | Customer | $0.50 | 2026-01-01 |
| Blair Chen | blair.chen@example.test | Trial | $137.50 | 2026-02-02 |
| Casey Rivera | casey.rivera@example.test | Paused | $274.50 | 2026-03-03 |
| Devon Patel | devon.patel@example.test | Customer | $411.50 | 2026-04-04 |
| Elliot Brooks | elliot.brooks@example.test | Trial | $548.50 | 2026-05-05 |
| Finley Hart | finley.hart@example.test | Paused | $685.50 | 2026-06-06 |
<div class="datatable" id="filters-datatable" data-plf-datatable>
<div class="datatable-toolbar" data-plf-datatable-controls hidden>
<div class="field datatable-filter">
<label class="field-label" for="filters-filter">Search customers</label>
<div class="input-group input-group-sm">
<span class="icon icon-search"></span>
<input class="input-group-control" id="filters-filter" type="search" placeholder="Customer, email or status">
</div>
</div>
</div>
<details class="collapse collapse-arrow datatable-disclosure" data-plf-datatable-controls hidden>
<summary class="collapse-trigger">
<span class="collapse-title">Filters</span>
<span class="datatable-filter-count" id="filters-count" hidden></span>
</summary>
<div class="collapse-content datatable-filters">
<div class="field">
<label class="field-label" for="filters-status">Status</label>
<select class="select select-sm" id="filters-status" data-plf-datatable-column-filter="status" data-plf-select>
<option value="">Any status</option>
<option>Customer</option>
<option>Trial</option>
<option>Paused</option>
</select>
</div>
<div class="field">
<label class="field-label" for="filters-min">Minimum revenue</label>
<input class="input input-sm" id="filters-min" type="number" step="any" data-plf-datatable-filter-min="revenue" placeholder="No minimum">
</div>
<div class="field">
<label class="field-label" for="filters-max">Maximum revenue</label>
<input class="input input-sm" id="filters-max" type="number" step="any" data-plf-datatable-filter-max="revenue" placeholder="No maximum">
</div>
<div class="field">
<label class="field-label" for="filters-since">Joined from</label>
<input class="input input-sm" id="filters-since" type="date" data-plf-datatable-filter-min="joined">
</div>
<div class="field">
<label class="field-label" for="filters-until">Joined through</label>
<input class="input input-sm" id="filters-until" type="date" data-plf-datatable-filter-max="joined">
</div>
<button class="btn btn-outline btn-sm" data-plf-datatable-clear-filters>Clear filters</button>
</div>
</details>
<div class="datatable-filter-chips" id="filters-chips" role="group" aria-label="Active filters" hidden></div>
<template id="filters-chip">
<button class="btn btn-outline btn-sm datatable-filter-chip">
<span dir="auto"></span>
<span aria-hidden="true">×</span>
</button>
</template>
<div class="table-wrapper" tabindex="0" role="region" aria-labelledby="filters-caption">
<table class="table table-zebra" style="--plf-table-min-width: 48rem">
<caption id="filters-caption">Customer accounts</caption>
<thead>
<tr>
<th scope="col" data-plf-column="name" data-plf-searchable>Customer</th>
<th scope="col" data-plf-column="email" data-plf-searchable>Email</th>
<th scope="col" data-plf-column="status" data-plf-searchable>Status</th>
<th scope="col" data-plf-column="revenue" data-plf-type="number">Revenue</th>
<th scope="col" data-plf-column="joined" data-plf-type="date">Joined</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Avery Stone</th>
<td>avery.stone@example.test</td>
<td><span class="badge badge-success badge-soft">Customer</span></td>
<td data-plf-value="0.5">$0.50</td>
<td data-plf-value="2026-01-01">2026-01-01</td>
</tr>
<tr>
<th scope="row">Blair Chen</th>
<td>blair.chen@example.test</td>
<td><span class="badge badge-outline">Trial</span></td>
<td data-plf-value="137.5">$137.50</td>
<td data-plf-value="2026-02-02">2026-02-02</td>
</tr>
<tr>
<th scope="row">Casey Rivera</th>
<td>casey.rivera@example.test</td>
<td><span class="badge badge-neutral badge-soft">Paused</span></td>
<td data-plf-value="274.5">$274.50</td>
<td data-plf-value="2026-03-03">2026-03-03</td>
</tr>
<tr>
<th scope="row">Devon Patel</th>
<td>devon.patel@example.test</td>
<td><span class="badge badge-success badge-soft">Customer</span></td>
<td data-plf-value="411.5">$411.50</td>
<td data-plf-value="2026-04-04">2026-04-04</td>
</tr>
<tr>
<th scope="row">Elliot Brooks</th>
<td>elliot.brooks@example.test</td>
<td><span class="badge badge-outline">Trial</span></td>
<td data-plf-value="548.5">$548.50</td>
<td data-plf-value="2026-05-05">2026-05-05</td>
</tr>
<tr>
<th scope="row">Finley Hart</th>
<td>finley.hart@example.test</td>
<td><span class="badge badge-neutral badge-soft">Paused</span></td>
<td data-plf-value="685.5">$685.50</td>
<td data-plf-value="2026-06-06">2026-06-06</td>
</tr>
</tbody>
</table>
</div>
<div class="datatable-empty" data-plf-datatable-empty hidden>
<span>No customers match. Clear filters or try another search.</span>
<button class="btn btn-outline btn-sm" data-plf-datatable-clear-filters>Clear filters</button>
</div>
<div class="datatable-footer" data-plf-datatable-controls hidden>
<p class="datatable-status" role="status"><span data-plf-datatable-count="from">0</span>–<span data-plf-datatable-count="to">0</span> of <span data-plf-datatable-count="matched">0</span> <span id="filters-result-label">customers</span><span id="filters-result-total" hidden> · <span data-plf-datatable-count="total">0</span> total</span><span class="sr-only"> · Page <span data-plf-datatable-count="page">0</span> of <span data-plf-datatable-count="pages">0</span></span></p>
<div class="datatable-pagination">
<nav class="datatable-pages" aria-label="Customer account pages">
<button class="btn btn-outline btn-sm btn-square" aria-label="Previous page" data-plf-datatable-previous></button>
<button class="btn btn-outline btn-sm btn-square" aria-label="Next page" data-plf-datatable-next></button>
</nav>
</div>
</div>
</div>
<script>
const table = document.getElementById('filters-datatable');
const chips = document.getElementById('filters-chips');
const chip = document.getElementById('filters-chip');
const count = document.getElementById('filters-count');
// Each active filter becomes a chip that removes it; the status says "matches" and shows the total while filtered.
function showFilters() {
const instance = PLF.DataTable.getInstance(table);
const { query } = instance.getState();
const filters = instance.getActiveFilters();
const active = query ? [['query', `Search: ${query}`]] : [];
for (const filter of filters) {
const values = [filter.text, filter.min !== null && `≥ ${filter.min}`, filter.max !== null && `≤ ${filter.max}`];
active.push([filter.key, `${filter.label}: ${values.filter(Boolean).join(' · ')}`]);
}
const hadFocus = chips.contains(document.activeElement);
chips.replaceChildren(...active.map(([key, text]) => {
const button = chip.content.firstElementChild.cloneNode(true);
button.value = key;
button.setAttribute('aria-label', `Remove ${text} filter`);
button.querySelector('span').textContent = text;
return button;
}));
chips.hidden = !active.length;
// A removed chip passes its focus to the first chip left, or to the search field.
if (hadFocus) (chips.querySelector('button') ?? document.getElementById('filters-filter')).focus();
count.textContent = filters.length;
count.hidden = !filters.length;
document.getElementById('filters-result-label').textContent = active.length ? 'matches' : 'customers';
document.getElementById('filters-result-total').hidden = !active.length;
}
table.addEventListener('plf:datatable:change', showFilters);
chips.addEventListener('click', (event) => {
const button = event.target.closest('button');
const instance = PLF.DataTable.getInstance(table);
if (button?.value === 'query') instance.setFilter('');
else if (button) instance.setColumnFilter(button.value, {});
});
document.addEventListener('DOMContentLoaded', () => {
PLF.DataTable.getOrCreateInstance(table);
showFilters();
});
</script><div class="plf:datatable" id="filters-datatable" data-plf-datatable>
<div class="plf:datatable-toolbar" data-plf-datatable-controls hidden>
<div class="plf:field plf:datatable-filter">
<label class="plf:field-label" for="filters-filter">Search customers</label>
<div class="plf:input-group plf:input-group-sm">
<span class="plf:icon plf:icon-search"></span>
<input class="plf:input-group-control" id="filters-filter" type="search" placeholder="Customer, email or status">
</div>
</div>
</div>
<details class="plf:collapse plf:collapse-arrow plf:datatable-disclosure" data-plf-datatable-controls hidden>
<summary class="plf:collapse-trigger">
<span class="plf:collapse-title">Filters</span>
<span class="plf:datatable-filter-count" id="filters-count" hidden></span>
</summary>
<div class="plf:collapse-content plf:datatable-filters">
<div class="plf:field">
<label class="plf:field-label" for="filters-status">Status</label>
<select class="plf:select plf:select-sm" id="filters-status" data-plf-datatable-column-filter="status" data-plf-select>
<option value="">Any status</option>
<option>Customer</option>
<option>Trial</option>
<option>Paused</option>
</select>
</div>
<div class="plf:field">
<label class="plf:field-label" for="filters-min">Minimum revenue</label>
<input class="plf:input plf:input-sm" id="filters-min" type="number" step="any" data-plf-datatable-filter-min="revenue" placeholder="No minimum">
</div>
<div class="plf:field">
<label class="plf:field-label" for="filters-max">Maximum revenue</label>
<input class="plf:input plf:input-sm" id="filters-max" type="number" step="any" data-plf-datatable-filter-max="revenue" placeholder="No maximum">
</div>
<div class="plf:field">
<label class="plf:field-label" for="filters-since">Joined from</label>
<input class="plf:input plf:input-sm" id="filters-since" type="date" data-plf-datatable-filter-min="joined">
</div>
<div class="plf:field">
<label class="plf:field-label" for="filters-until">Joined through</label>
<input class="plf:input plf:input-sm" id="filters-until" type="date" data-plf-datatable-filter-max="joined">
</div>
<button class="plf:btn plf:btn-outline plf:btn-sm" data-plf-datatable-clear-filters>Clear filters</button>
</div>
</details>
<div class="plf:datatable-filter-chips" id="filters-chips" role="group" aria-label="Active filters" hidden></div>
<template id="filters-chip">
<button class="plf:btn plf:btn-outline plf:btn-sm plf:datatable-filter-chip">
<span dir="auto"></span>
<span aria-hidden="true">×</span>
</button>
</template>
<div class="plf:table-wrapper" tabindex="0" role="region" aria-labelledby="filters-caption">
<table class="plf:table plf:table-zebra" style="--plf-table-min-width: 48rem">
<caption id="filters-caption">Customer accounts</caption>
<thead>
<tr>
<th scope="col" data-plf-column="name" data-plf-searchable>Customer</th>
<th scope="col" data-plf-column="email" data-plf-searchable>Email</th>
<th scope="col" data-plf-column="status" data-plf-searchable>Status</th>
<th scope="col" data-plf-column="revenue" data-plf-type="number">Revenue</th>
<th scope="col" data-plf-column="joined" data-plf-type="date">Joined</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Avery Stone</th>
<td>avery.stone@example.test</td>
<td><span class="plf:badge plf:badge-success plf:badge-soft">Customer</span></td>
<td data-plf-value="0.5">$0.50</td>
<td data-plf-value="2026-01-01">2026-01-01</td>
</tr>
<tr>
<th scope="row">Blair Chen</th>
<td>blair.chen@example.test</td>
<td><span class="plf:badge plf:badge-outline">Trial</span></td>
<td data-plf-value="137.5">$137.50</td>
<td data-plf-value="2026-02-02">2026-02-02</td>
</tr>
<tr>
<th scope="row">Casey Rivera</th>
<td>casey.rivera@example.test</td>
<td><span class="plf:badge plf:badge-neutral plf:badge-soft">Paused</span></td>
<td data-plf-value="274.5">$274.50</td>
<td data-plf-value="2026-03-03">2026-03-03</td>
</tr>
<tr>
<th scope="row">Devon Patel</th>
<td>devon.patel@example.test</td>
<td><span class="plf:badge plf:badge-success plf:badge-soft">Customer</span></td>
<td data-plf-value="411.5">$411.50</td>
<td data-plf-value="2026-04-04">2026-04-04</td>
</tr>
<tr>
<th scope="row">Elliot Brooks</th>
<td>elliot.brooks@example.test</td>
<td><span class="plf:badge plf:badge-outline">Trial</span></td>
<td data-plf-value="548.5">$548.50</td>
<td data-plf-value="2026-05-05">2026-05-05</td>
</tr>
<tr>
<th scope="row">Finley Hart</th>
<td>finley.hart@example.test</td>
<td><span class="plf:badge plf:badge-neutral plf:badge-soft">Paused</span></td>
<td data-plf-value="685.5">$685.50</td>
<td data-plf-value="2026-06-06">2026-06-06</td>
</tr>
</tbody>
</table>
</div>
<div class="plf:datatable-empty" data-plf-datatable-empty hidden>
<span>No customers match. Clear filters or try another search.</span>
<button class="plf:btn plf:btn-outline plf:btn-sm" data-plf-datatable-clear-filters>Clear filters</button>
</div>
<div class="plf:datatable-footer" data-plf-datatable-controls hidden>
<p class="plf:datatable-status" role="status"><span data-plf-datatable-count="from">0</span>–<span data-plf-datatable-count="to">0</span> of <span data-plf-datatable-count="matched">0</span> <span id="filters-result-label">customers</span><span id="filters-result-total" hidden> · <span data-plf-datatable-count="total">0</span> total</span><span class="plf:sr-only"> · Page <span data-plf-datatable-count="page">0</span> of <span data-plf-datatable-count="pages">0</span></span></p>
<div class="plf:datatable-pagination">
<nav class="plf:datatable-pages" aria-label="Customer account pages">
<button class="plf:btn plf:btn-outline plf:btn-sm plf:btn-square" aria-label="Previous page" data-plf-datatable-previous></button>
<button class="plf:btn plf:btn-outline plf:btn-sm plf:btn-square" aria-label="Next page" data-plf-datatable-next></button>
</nav>
</div>
</div>
</div>
<script>
const table = document.getElementById('filters-datatable');
const chips = document.getElementById('filters-chips');
const chip = document.getElementById('filters-chip');
const count = document.getElementById('filters-count');
// Each active filter becomes a chip that removes it; the status says "matches" and shows the total while filtered.
function showFilters() {
const instance = PLF.DataTable.getInstance(table);
const { query } = instance.getState();
const filters = instance.getActiveFilters();
const active = query ? [['query', `Search: ${query}`]] : [];
for (const filter of filters) {
const values = [filter.text, filter.min !== null && `≥ ${filter.min}`, filter.max !== null && `≤ ${filter.max}`];
active.push([filter.key, `${filter.label}: ${values.filter(Boolean).join(' · ')}`]);
}
const hadFocus = chips.contains(document.activeElement);
chips.replaceChildren(...active.map(([key, text]) => {
const button = chip.content.firstElementChild.cloneNode(true);
button.value = key;
button.setAttribute('aria-label', `Remove ${text} filter`);
button.querySelector('span').textContent = text;
return button;
}));
chips.hidden = !active.length;
// A removed chip passes its focus to the first chip left, or to the search field.
if (hadFocus) (chips.querySelector('button') ?? document.getElementById('filters-filter')).focus();
count.textContent = filters.length;
count.hidden = !filters.length;
document.getElementById('filters-result-label').textContent = active.length ? 'matches' : 'customers';
document.getElementById('filters-result-total').hidden = !active.length;
}
table.addEventListener('plf:datatable:change', showFilters);
chips.addEventListener('click', (event) => {
const button = event.target.closest('button');
const instance = PLF.DataTable.getInstance(table);
if (button?.value === 'query') instance.setFilter('');
else if (button) instance.setColumnFilter(button.value, {});
});
document.addEventListener('DOMContentLoaded', () => {
PLF.DataTable.getOrCreateInstance(table);
showFilters();
});
</script>Saved views
Mark the views controls and the table runs them: choosing a view applies it, Save or Enter saves the search, filters, sorting, page size, columns and density under the name, and Update and Delete act on the chosen view, which shows (modified) once the table changes. data-plf-storage-key keeps the views on this device.
| Customer | Status | Revenue | Joined | |
|---|---|---|---|---|
| Avery Stone | avery.stone@example.test | Customer | $0.50 | 2026-01-01 |
| Blair Chen | blair.chen@example.test | Trial | $137.50 | 2026-02-02 |
| Casey Rivera | casey.rivera@example.test | Paused | $274.50 | 2026-03-03 |
| Devon Patel | devon.patel@example.test | Customer | $411.50 | 2026-04-04 |
| Elliot Brooks | elliot.brooks@example.test | Trial | $548.50 | 2026-05-05 |
| Finley Hart | finley.hart@example.test | Paused | $685.50 | 2026-06-06 |
<div class="datatable" id="views-datatable" data-plf-datatable data-plf-storage-key="customer-views">
<div class="datatable-toolbar" data-plf-datatable-controls hidden>
<div class="field datatable-filter">
<label class="field-label" for="views-filter">Search customers</label>
<div class="input-group input-group-sm">
<span class="icon icon-search"></span>
<input class="input-group-control" id="views-filter" type="search" placeholder="Customer, email or status">
</div>
</div>
<div class="datatable-settings">
<div class="popover" data-plf-popover data-plf-align="end">
<button class="btn btn-outline btn-sm">Views</button>
<div class="popover-content datatable-views-panel" hidden>
<div class="datatable-views-heading">
<h4 class="popover-title">Saved views</h4>
<button class="btn btn-ghost btn-sm btn-square" aria-label="Close views" data-plf-popover-close>
<span class="icon icon-x"></span>
</button>
</div>
<p class="popover-description">Save the search and sorting on this device.</p>
<div class="field">
<label class="field-label" for="views-choice">Saved view</label>
<select class="select select-sm" id="views-choice" data-plf-select data-plf-datatable-views>
<option value="">Current view (unsaved)</option>
</select>
</div>
<div class="field">
<label class="field-label" for="views-name">View name</label>
<input class="input input-sm" id="views-name" type="text" maxlength="80" placeholder="Active customers" data-plf-datatable-view-name>
</div>
<div class="datatable-view-actions">
<button class="btn btn-primary btn-sm datatable-view-save" data-plf-datatable-view-save>Save view</button>
<button class="btn btn-outline btn-sm" data-plf-datatable-view-update>Update view</button>
<button class="btn btn-ghost btn-sm" data-plf-datatable-view-delete>Delete view</button>
</div>
<output class="datatable-status" data-plf-datatable-views-status></output>
</div>
</div>
</div>
</div>
<div class="table-wrapper" tabindex="0" role="region" aria-labelledby="views-caption">
<table class="table table-zebra" style="--plf-table-min-width: 48rem">
<caption id="views-caption">Customer segments</caption>
<thead>
<tr>
<th scope="col" data-plf-column="name" data-plf-searchable data-plf-datatable-sortable>Customer</th>
<th scope="col" data-plf-column="email" data-plf-searchable>Email</th>
<th scope="col" data-plf-column="status" data-plf-searchable data-plf-datatable-sortable>Status</th>
<th scope="col" data-plf-column="revenue" data-plf-type="number" data-plf-datatable-sortable>Revenue</th>
<th scope="col" data-plf-column="joined">Joined</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Avery Stone</th>
<td>avery.stone@example.test</td>
<td><span class="badge badge-success badge-soft">Customer</span></td>
<td data-plf-value="0.5">$0.50</td>
<td>2026-01-01</td>
</tr>
<tr>
<th scope="row">Blair Chen</th>
<td>blair.chen@example.test</td>
<td><span class="badge badge-outline">Trial</span></td>
<td data-plf-value="137.5">$137.50</td>
<td>2026-02-02</td>
</tr>
<tr>
<th scope="row">Casey Rivera</th>
<td>casey.rivera@example.test</td>
<td><span class="badge badge-neutral badge-soft">Paused</span></td>
<td data-plf-value="274.5">$274.50</td>
<td>2026-03-03</td>
</tr>
<tr>
<th scope="row">Devon Patel</th>
<td>devon.patel@example.test</td>
<td><span class="badge badge-success badge-soft">Customer</span></td>
<td data-plf-value="411.5">$411.50</td>
<td>2026-04-04</td>
</tr>
<tr>
<th scope="row">Elliot Brooks</th>
<td>elliot.brooks@example.test</td>
<td><span class="badge badge-outline">Trial</span></td>
<td data-plf-value="548.5">$548.50</td>
<td>2026-05-05</td>
</tr>
<tr>
<th scope="row">Finley Hart</th>
<td>finley.hart@example.test</td>
<td><span class="badge badge-neutral badge-soft">Paused</span></td>
<td data-plf-value="685.5">$685.50</td>
<td>2026-06-06</td>
</tr>
</tbody>
</table>
</div>
<div class="datatable-empty" data-plf-datatable-empty hidden>No customers match your search.</div>
<div class="datatable-footer" data-plf-datatable-controls hidden>
<p class="datatable-status" role="status"><span data-plf-datatable-count="from">0</span>–<span data-plf-datatable-count="to">0</span> of <span data-plf-datatable-count="matched">0</span> customers<span class="sr-only"> · Page <span data-plf-datatable-count="page">0</span> of <span data-plf-datatable-count="pages">0</span></span></p>
<div class="datatable-pagination">
<nav class="datatable-pages" aria-label="Customer segment pages">
<button class="btn btn-outline btn-sm btn-square" aria-label="Previous page" data-plf-datatable-previous></button>
<button class="btn btn-outline btn-sm btn-square" aria-label="Next page" data-plf-datatable-next></button>
</nav>
</div>
</div>
</div><div class="plf:datatable" id="views-datatable" data-plf-datatable data-plf-storage-key="customer-views">
<div class="plf:datatable-toolbar" data-plf-datatable-controls hidden>
<div class="plf:field plf:datatable-filter">
<label class="plf:field-label" for="views-filter">Search customers</label>
<div class="plf:input-group plf:input-group-sm">
<span class="plf:icon plf:icon-search"></span>
<input class="plf:input-group-control" id="views-filter" type="search" placeholder="Customer, email or status">
</div>
</div>
<div class="plf:datatable-settings">
<div class="plf:popover" data-plf-popover data-plf-align="end">
<button class="plf:btn plf:btn-outline plf:btn-sm">Views</button>
<div class="plf:popover-content plf:datatable-views-panel" hidden>
<div class="plf:datatable-views-heading">
<h4 class="plf:popover-title">Saved views</h4>
<button class="plf:btn plf:btn-ghost plf:btn-sm plf:btn-square" aria-label="Close views" data-plf-popover-close>
<span class="plf:icon plf:icon-x"></span>
</button>
</div>
<p class="plf:popover-description">Save the search and sorting on this device.</p>
<div class="plf:field">
<label class="plf:field-label" for="views-choice">Saved view</label>
<select class="plf:select plf:select-sm" id="views-choice" data-plf-select data-plf-datatable-views>
<option value="">Current view (unsaved)</option>
</select>
</div>
<div class="plf:field">
<label class="plf:field-label" for="views-name">View name</label>
<input class="plf:input plf:input-sm" id="views-name" type="text" maxlength="80" placeholder="Active customers" data-plf-datatable-view-name>
</div>
<div class="plf:datatable-view-actions">
<button class="plf:btn plf:btn-primary plf:btn-sm plf:datatable-view-save" data-plf-datatable-view-save>Save view</button>
<button class="plf:btn plf:btn-outline plf:btn-sm" data-plf-datatable-view-update>Update view</button>
<button class="plf:btn plf:btn-ghost plf:btn-sm" data-plf-datatable-view-delete>Delete view</button>
</div>
<output class="plf:datatable-status" data-plf-datatable-views-status></output>
</div>
</div>
</div>
</div>
<div class="plf:table-wrapper" tabindex="0" role="region" aria-labelledby="views-caption">
<table class="plf:table plf:table-zebra" style="--plf-table-min-width: 48rem">
<caption id="views-caption">Customer segments</caption>
<thead>
<tr>
<th scope="col" data-plf-column="name" data-plf-searchable data-plf-datatable-sortable>Customer</th>
<th scope="col" data-plf-column="email" data-plf-searchable>Email</th>
<th scope="col" data-plf-column="status" data-plf-searchable data-plf-datatable-sortable>Status</th>
<th scope="col" data-plf-column="revenue" data-plf-type="number" data-plf-datatable-sortable>Revenue</th>
<th scope="col" data-plf-column="joined">Joined</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Avery Stone</th>
<td>avery.stone@example.test</td>
<td><span class="plf:badge plf:badge-success plf:badge-soft">Customer</span></td>
<td data-plf-value="0.5">$0.50</td>
<td>2026-01-01</td>
</tr>
<tr>
<th scope="row">Blair Chen</th>
<td>blair.chen@example.test</td>
<td><span class="plf:badge plf:badge-outline">Trial</span></td>
<td data-plf-value="137.5">$137.50</td>
<td>2026-02-02</td>
</tr>
<tr>
<th scope="row">Casey Rivera</th>
<td>casey.rivera@example.test</td>
<td><span class="plf:badge plf:badge-neutral plf:badge-soft">Paused</span></td>
<td data-plf-value="274.5">$274.50</td>
<td>2026-03-03</td>
</tr>
<tr>
<th scope="row">Devon Patel</th>
<td>devon.patel@example.test</td>
<td><span class="plf:badge plf:badge-success plf:badge-soft">Customer</span></td>
<td data-plf-value="411.5">$411.50</td>
<td>2026-04-04</td>
</tr>
<tr>
<th scope="row">Elliot Brooks</th>
<td>elliot.brooks@example.test</td>
<td><span class="plf:badge plf:badge-outline">Trial</span></td>
<td data-plf-value="548.5">$548.50</td>
<td>2026-05-05</td>
</tr>
<tr>
<th scope="row">Finley Hart</th>
<td>finley.hart@example.test</td>
<td><span class="plf:badge plf:badge-neutral plf:badge-soft">Paused</span></td>
<td data-plf-value="685.5">$685.50</td>
<td>2026-06-06</td>
</tr>
</tbody>
</table>
</div>
<div class="plf:datatable-empty" data-plf-datatable-empty hidden>No customers match your search.</div>
<div class="plf:datatable-footer" data-plf-datatable-controls hidden>
<p class="plf:datatable-status" role="status"><span data-plf-datatable-count="from">0</span>–<span data-plf-datatable-count="to">0</span> of <span data-plf-datatable-count="matched">0</span> customers<span class="plf:sr-only"> · Page <span data-plf-datatable-count="page">0</span> of <span data-plf-datatable-count="pages">0</span></span></p>
<div class="plf:datatable-pagination">
<nav class="plf:datatable-pages" aria-label="Customer segment pages">
<button class="plf:btn plf:btn-outline plf:btn-sm plf:btn-square" aria-label="Previous page" data-plf-datatable-previous></button>
<button class="plf:btn plf:btn-outline plf:btn-sm plf:btn-square" aria-label="Next page" data-plf-datatable-next></button>
</nav>
</div>
</div>
</div>Row actions
Your action code reads the row’s texts by column key with getRowData(row).
| Customer | Status | Revenue | Joined | Actions | |
|---|---|---|---|---|---|
| Avery Stone | avery.stone@example.test | Customer | $0.50 | 2026-01-01 |
|
| Blair Chen | blair.chen@example.test | Trial | $137.50 | 2026-02-02 |
|
| Casey Rivera | casey.rivera@example.test | Paused | $274.50 | 2026-03-03 |
|
| Devon Patel | devon.patel@example.test | Customer | $411.50 | 2026-04-04 |
|
| Elliot Brooks | elliot.brooks@example.test | Trial | $548.50 | 2026-05-05 |
|
<div class="datatable" id="actions-datatable" data-plf-datatable>
<div class="datatable-toolbar" data-plf-datatable-controls hidden>
<div class="field datatable-filter">
<label class="field-label" for="actions-filter">Search customers</label>
<div class="input-group input-group-sm">
<span class="icon icon-search"></span>
<input class="input-group-control" id="actions-filter" type="search" placeholder="Customer, email or status">
</div>
</div>
</div>
<div class="table-wrapper" tabindex="0" role="region" aria-labelledby="actions-caption">
<table class="table table-zebra" style="--plf-table-min-width: 56rem">
<caption id="actions-caption">Customer contacts</caption>
<thead>
<tr>
<th scope="col" data-plf-column="name" data-plf-searchable>Customer</th>
<th scope="col" data-plf-column="email" data-plf-searchable>Email</th>
<th scope="col" data-plf-column="status" data-plf-searchable>Status</th>
<th scope="col" data-plf-column="revenue">Revenue</th>
<th scope="col" data-plf-column="joined">Joined</th>
<th scope="col" data-plf-column="actions" data-plf-no-export>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row"><a class="btn btn-link btn-sm" href="#actions-details">Avery Stone</a></th>
<td>avery.stone@example.test</td>
<td><span class="badge badge-success badge-soft">Customer</span></td>
<td>$0.50</td>
<td>2026-01-01</td>
<td>
<div class="dropdown" data-plf-dropdown-menu data-plf-align="end" data-plf-datatable-row-controls hidden>
<button class="btn btn-ghost btn-sm btn-square" aria-label="Actions for Avery Stone">
<span class="icon icon-dots"></span>
</button>
<div class="dropdown-content menu" aria-label="Customer actions" hidden>
<button class="menu-item" data-plf-datatable-action="view">
<span class="icon icon-eye"></span>
<span class="menu-text">View details</span>
</button>
<button class="menu-item" data-plf-datatable-action="copy-email">
<span class="icon icon-copy"></span>
<span class="menu-text">Copy email</span>
</button>
</div>
</div>
</td>
</tr>
<tr>
<th scope="row"><a class="btn btn-link btn-sm" href="#actions-details">Blair Chen</a></th>
<td>blair.chen@example.test</td>
<td><span class="badge badge-outline">Trial</span></td>
<td>$137.50</td>
<td>2026-02-02</td>
<td>
<div class="dropdown" data-plf-dropdown-menu data-plf-align="end" data-plf-datatable-row-controls hidden>
<button class="btn btn-ghost btn-sm btn-square" aria-label="Actions for Blair Chen">
<span class="icon icon-dots"></span>
</button>
<div class="dropdown-content menu" aria-label="Customer actions" hidden>
<button class="menu-item" data-plf-datatable-action="view">
<span class="icon icon-eye"></span>
<span class="menu-text">View details</span>
</button>
<button class="menu-item" data-plf-datatable-action="copy-email">
<span class="icon icon-copy"></span>
<span class="menu-text">Copy email</span>
</button>
</div>
</div>
</td>
</tr>
<tr>
<th scope="row"><a class="btn btn-link btn-sm" href="#actions-details">Casey Rivera</a></th>
<td>casey.rivera@example.test</td>
<td><span class="badge badge-neutral badge-soft">Paused</span></td>
<td>$274.50</td>
<td>2026-03-03</td>
<td>
<div class="dropdown" data-plf-dropdown-menu data-plf-align="end" data-plf-datatable-row-controls hidden>
<button class="btn btn-ghost btn-sm btn-square" aria-label="Actions for Casey Rivera">
<span class="icon icon-dots"></span>
</button>
<div class="dropdown-content menu" aria-label="Customer actions" hidden>
<button class="menu-item" data-plf-datatable-action="view">
<span class="icon icon-eye"></span>
<span class="menu-text">View details</span>
</button>
<button class="menu-item" data-plf-datatable-action="copy-email">
<span class="icon icon-copy"></span>
<span class="menu-text">Copy email</span>
</button>
</div>
</div>
</td>
</tr>
<tr>
<th scope="row"><a class="btn btn-link btn-sm" href="#actions-details">Devon Patel</a></th>
<td>devon.patel@example.test</td>
<td><span class="badge badge-success badge-soft">Customer</span></td>
<td>$411.50</td>
<td>2026-04-04</td>
<td>
<div class="dropdown" data-plf-dropdown-menu data-plf-align="end" data-plf-datatable-row-controls hidden>
<button class="btn btn-ghost btn-sm btn-square" aria-label="Actions for Devon Patel">
<span class="icon icon-dots"></span>
</button>
<div class="dropdown-content menu" aria-label="Customer actions" hidden>
<button class="menu-item" data-plf-datatable-action="view">
<span class="icon icon-eye"></span>
<span class="menu-text">View details</span>
</button>
<button class="menu-item" data-plf-datatable-action="copy-email">
<span class="icon icon-copy"></span>
<span class="menu-text">Copy email</span>
</button>
</div>
</div>
</td>
</tr>
<tr>
<th scope="row"><a class="btn btn-link btn-sm" href="#actions-details">Elliot Brooks</a></th>
<td>elliot.brooks@example.test</td>
<td><span class="badge badge-outline">Trial</span></td>
<td>$548.50</td>
<td>2026-05-05</td>
<td>
<div class="dropdown" data-plf-dropdown-menu data-plf-align="end" data-plf-datatable-row-controls hidden>
<button class="btn btn-ghost btn-sm btn-square" aria-label="Actions for Elliot Brooks">
<span class="icon icon-dots"></span>
</button>
<div class="dropdown-content menu" aria-label="Customer actions" hidden>
<button class="menu-item" data-plf-datatable-action="view">
<span class="icon icon-eye"></span>
<span class="menu-text">View details</span>
</button>
<button class="menu-item" data-plf-datatable-action="copy-email">
<span class="icon icon-copy"></span>
<span class="menu-text">Copy email</span>
</button>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="datatable-empty" data-plf-datatable-empty hidden>No customers match your search.</div>
<div class="datatable-footer" data-plf-datatable-controls hidden>
<p class="datatable-status" role="status"><span data-plf-datatable-count="from">0</span>–<span data-plf-datatable-count="to">0</span> of <span data-plf-datatable-count="matched">0</span> customers<span class="sr-only"> · Page <span data-plf-datatable-count="page">0</span> of <span data-plf-datatable-count="pages">0</span></span></p>
<div class="datatable-pagination">
<nav class="datatable-pages" aria-label="Customer contact pages">
<button class="btn btn-outline btn-sm btn-square" aria-label="Previous page" data-plf-datatable-previous></button>
<button class="btn btn-outline btn-sm btn-square" aria-label="Next page" data-plf-datatable-next></button>
</nav>
</div>
</div>
</div>
<dialog class="modal" id="actions-details" data-plf-modal>
<div class="modal-header">
<h3 class="modal-title">Customer details</h3>
<button class="btn btn-outline btn-sm" data-plf-trigger="modal:close">Close</button>
</div>
<p id="actions-details-content"></p>
</dialog>
<script>
const table = document.getElementById('actions-datatable');
const dialog = document.getElementById('actions-details');
// getRowData() gives the row's texts by column key.
function showCustomer(row, trigger) {
const customer = PLF.DataTable.getInstance(table).getRowData(row);
document.getElementById('actions-details-content').textContent = [
`Customer: ${customer.name}`, `Email: ${customer.email}`, `Status: ${customer.status}`,
`Revenue: ${customer.revenue}`, `Joined: ${customer.joined}`,
].join(' · ');
PLF.Modal.getInstance(dialog).open(trigger);
}
table.addEventListener('plf:datatable:action', (event) => {
const {action, row} = event.detail;
if (action === 'view') showCustomer(row);
if (action === 'copy-email') navigator.clipboard.writeText(PLF.DataTable.getInstance(table).getRowData(row).email);
});
// Each name links to the same details.
table.addEventListener('click', (event) => {
const link = event.target.closest('a[href="#actions-details"]');
if (!link) return;
event.preventDefault();
showCustomer(link.closest('tr'), link);
});
</script><div class="plf:datatable" id="actions-datatable" data-plf-datatable>
<div class="plf:datatable-toolbar" data-plf-datatable-controls hidden>
<div class="plf:field plf:datatable-filter">
<label class="plf:field-label" for="actions-filter">Search customers</label>
<div class="plf:input-group plf:input-group-sm">
<span class="plf:icon plf:icon-search"></span>
<input class="plf:input-group-control" id="actions-filter" type="search" placeholder="Customer, email or status">
</div>
</div>
</div>
<div class="plf:table-wrapper" tabindex="0" role="region" aria-labelledby="actions-caption">
<table class="plf:table plf:table-zebra" style="--plf-table-min-width: 56rem">
<caption id="actions-caption">Customer contacts</caption>
<thead>
<tr>
<th scope="col" data-plf-column="name" data-plf-searchable>Customer</th>
<th scope="col" data-plf-column="email" data-plf-searchable>Email</th>
<th scope="col" data-plf-column="status" data-plf-searchable>Status</th>
<th scope="col" data-plf-column="revenue">Revenue</th>
<th scope="col" data-plf-column="joined">Joined</th>
<th scope="col" data-plf-column="actions" data-plf-no-export>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row"><a class="plf:btn plf:btn-link plf:btn-sm" href="#actions-details">Avery Stone</a></th>
<td>avery.stone@example.test</td>
<td><span class="plf:badge plf:badge-success plf:badge-soft">Customer</span></td>
<td>$0.50</td>
<td>2026-01-01</td>
<td>
<div class="plf:dropdown" data-plf-dropdown-menu data-plf-align="end" data-plf-datatable-row-controls hidden>
<button class="plf:btn plf:btn-ghost plf:btn-sm plf:btn-square" aria-label="Actions for Avery Stone">
<span class="plf:icon plf:icon-dots"></span>
</button>
<div class="plf:dropdown-content plf:menu" aria-label="Customer actions" hidden>
<button class="plf:menu-item" data-plf-datatable-action="view">
<span class="plf:icon plf:icon-eye"></span>
<span class="plf:menu-text">View details</span>
</button>
<button class="plf:menu-item" data-plf-datatable-action="copy-email">
<span class="plf:icon plf:icon-copy"></span>
<span class="plf:menu-text">Copy email</span>
</button>
</div>
</div>
</td>
</tr>
<tr>
<th scope="row"><a class="plf:btn plf:btn-link plf:btn-sm" href="#actions-details">Blair Chen</a></th>
<td>blair.chen@example.test</td>
<td><span class="plf:badge plf:badge-outline">Trial</span></td>
<td>$137.50</td>
<td>2026-02-02</td>
<td>
<div class="plf:dropdown" data-plf-dropdown-menu data-plf-align="end" data-plf-datatable-row-controls hidden>
<button class="plf:btn plf:btn-ghost plf:btn-sm plf:btn-square" aria-label="Actions for Blair Chen">
<span class="plf:icon plf:icon-dots"></span>
</button>
<div class="plf:dropdown-content plf:menu" aria-label="Customer actions" hidden>
<button class="plf:menu-item" data-plf-datatable-action="view">
<span class="plf:icon plf:icon-eye"></span>
<span class="plf:menu-text">View details</span>
</button>
<button class="plf:menu-item" data-plf-datatable-action="copy-email">
<span class="plf:icon plf:icon-copy"></span>
<span class="plf:menu-text">Copy email</span>
</button>
</div>
</div>
</td>
</tr>
<tr>
<th scope="row"><a class="plf:btn plf:btn-link plf:btn-sm" href="#actions-details">Casey Rivera</a></th>
<td>casey.rivera@example.test</td>
<td><span class="plf:badge plf:badge-neutral plf:badge-soft">Paused</span></td>
<td>$274.50</td>
<td>2026-03-03</td>
<td>
<div class="plf:dropdown" data-plf-dropdown-menu data-plf-align="end" data-plf-datatable-row-controls hidden>
<button class="plf:btn plf:btn-ghost plf:btn-sm plf:btn-square" aria-label="Actions for Casey Rivera">
<span class="plf:icon plf:icon-dots"></span>
</button>
<div class="plf:dropdown-content plf:menu" aria-label="Customer actions" hidden>
<button class="plf:menu-item" data-plf-datatable-action="view">
<span class="plf:icon plf:icon-eye"></span>
<span class="plf:menu-text">View details</span>
</button>
<button class="plf:menu-item" data-plf-datatable-action="copy-email">
<span class="plf:icon plf:icon-copy"></span>
<span class="plf:menu-text">Copy email</span>
</button>
</div>
</div>
</td>
</tr>
<tr>
<th scope="row"><a class="plf:btn plf:btn-link plf:btn-sm" href="#actions-details">Devon Patel</a></th>
<td>devon.patel@example.test</td>
<td><span class="plf:badge plf:badge-success plf:badge-soft">Customer</span></td>
<td>$411.50</td>
<td>2026-04-04</td>
<td>
<div class="plf:dropdown" data-plf-dropdown-menu data-plf-align="end" data-plf-datatable-row-controls hidden>
<button class="plf:btn plf:btn-ghost plf:btn-sm plf:btn-square" aria-label="Actions for Devon Patel">
<span class="plf:icon plf:icon-dots"></span>
</button>
<div class="plf:dropdown-content plf:menu" aria-label="Customer actions" hidden>
<button class="plf:menu-item" data-plf-datatable-action="view">
<span class="plf:icon plf:icon-eye"></span>
<span class="plf:menu-text">View details</span>
</button>
<button class="plf:menu-item" data-plf-datatable-action="copy-email">
<span class="plf:icon plf:icon-copy"></span>
<span class="plf:menu-text">Copy email</span>
</button>
</div>
</div>
</td>
</tr>
<tr>
<th scope="row"><a class="plf:btn plf:btn-link plf:btn-sm" href="#actions-details">Elliot Brooks</a></th>
<td>elliot.brooks@example.test</td>
<td><span class="plf:badge plf:badge-outline">Trial</span></td>
<td>$548.50</td>
<td>2026-05-05</td>
<td>
<div class="plf:dropdown" data-plf-dropdown-menu data-plf-align="end" data-plf-datatable-row-controls hidden>
<button class="plf:btn plf:btn-ghost plf:btn-sm plf:btn-square" aria-label="Actions for Elliot Brooks">
<span class="plf:icon plf:icon-dots"></span>
</button>
<div class="plf:dropdown-content plf:menu" aria-label="Customer actions" hidden>
<button class="plf:menu-item" data-plf-datatable-action="view">
<span class="plf:icon plf:icon-eye"></span>
<span class="plf:menu-text">View details</span>
</button>
<button class="plf:menu-item" data-plf-datatable-action="copy-email">
<span class="plf:icon plf:icon-copy"></span>
<span class="plf:menu-text">Copy email</span>
</button>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="plf:datatable-empty" data-plf-datatable-empty hidden>No customers match your search.</div>
<div class="plf:datatable-footer" data-plf-datatable-controls hidden>
<p class="plf:datatable-status" role="status"><span data-plf-datatable-count="from">0</span>–<span data-plf-datatable-count="to">0</span> of <span data-plf-datatable-count="matched">0</span> customers<span class="plf:sr-only"> · Page <span data-plf-datatable-count="page">0</span> of <span data-plf-datatable-count="pages">0</span></span></p>
<div class="plf:datatable-pagination">
<nav class="plf:datatable-pages" aria-label="Customer contact pages">
<button class="plf:btn plf:btn-outline plf:btn-sm plf:btn-square" aria-label="Previous page" data-plf-datatable-previous></button>
<button class="plf:btn plf:btn-outline plf:btn-sm plf:btn-square" aria-label="Next page" data-plf-datatable-next></button>
</nav>
</div>
</div>
</div>
<dialog class="plf:modal" id="actions-details" data-plf-modal>
<div class="plf:modal-header">
<h3 class="plf:modal-title">Customer details</h3>
<button class="plf:btn plf:btn-outline plf:btn-sm" data-plf-trigger="modal:close">Close</button>
</div>
<p id="actions-details-content"></p>
</dialog>
<script>
const table = document.getElementById('actions-datatable');
const dialog = document.getElementById('actions-details');
// getRowData() gives the row's texts by column key.
function showCustomer(row, trigger) {
const customer = PLF.DataTable.getInstance(table).getRowData(row);
document.getElementById('actions-details-content').textContent = [
`Customer: ${customer.name}`, `Email: ${customer.email}`, `Status: ${customer.status}`,
`Revenue: ${customer.revenue}`, `Joined: ${customer.joined}`,
].join(' · ');
PLF.Modal.getInstance(dialog).open(trigger);
}
table.addEventListener('plf:datatable:action', (event) => {
const {action, row} = event.detail;
if (action === 'view') showCustomer(row);
if (action === 'copy-email') navigator.clipboard.writeText(PLF.DataTable.getInstance(table).getRowData(row).email);
});
// Each name links to the same details.
table.addEventListener('click', (event) => {
const link = event.target.closest('a[href="#actions-details"]');
if (!link) return;
event.preventDefault();
showCustomer(link.closest('tr'), link);
});
</script>JavaScript
DataTable needs JavaScript and is an extension: the Full script and Full CSS leave it out. The Full script (pelatform-ui.min.js) and the extension scripts after it start every component on page load, with no code of your own; with Core and single files, the last line starts them. For markup you add later, see Dynamic content.
<link rel="stylesheet" href="pelatform-ui.min.css">
<link rel="stylesheet" href="extensions/datatable.min.css">
<link rel="stylesheet" href="icons/icons.min.css">
<script src="pelatform-ui.min.js" defer></script>
<script src="extensions/datatable.min.js" defer></script>
<script src="extensions/datatable-excel.min.js" defer></script>
<script src="extensions/datatable-pdf.min.js" defer></script><link rel="stylesheet" href="core/pelatform-ui-core.min.css">
<link rel="stylesheet" href="components/button.min.css">
<link rel="stylesheet" href="components/form.min.css">
<link rel="stylesheet" href="components/badge.min.css">
<link rel="stylesheet" href="components/modal.min.css">
<link rel="stylesheet" href="components/table.min.css">
<link rel="stylesheet" href="components/popover.min.css">
<link rel="stylesheet" href="components/dropdown-menu.min.css">
<link rel="stylesheet" href="components/collapsible.min.css">
<link rel="stylesheet" href="components/slider.min.css">
<link rel="stylesheet" href="components/select.min.css">
<link rel="stylesheet" href="extensions/datatable.min.css">
<link rel="stylesheet" href="icons/icons.min.css">
<script src="core/pelatform-ui-core.min.js" defer></script>
<script src="components/modal.min.js" defer></script>
<script src="components/popover.min.js" defer></script>
<script src="components/dropdown-menu.min.js" defer></script>
<script src="components/slider.min.js" defer></script>
<script src="components/select.min.js" defer></script>
<script src="extensions/datatable.min.js" defer></script>
<script src="extensions/datatable-excel.min.js" defer></script>
<script src="extensions/datatable-pdf.min.js" defer></script>
<script>document.addEventListener('DOMContentLoaded', () => PLF.init());</script><link rel="stylesheet" href="pelatform-ui.prefixed.min.css">
<link rel="stylesheet" href="extensions/datatable.prefixed.min.css">
<link rel="stylesheet" href="icons/icons.prefixed.min.css">
<script src="pelatform-ui.min.js" defer></script>
<script src="extensions/datatable.min.js" defer></script>
<script src="extensions/datatable-excel.min.js" defer></script>
<script src="extensions/datatable-pdf.min.js" defer></script><link rel="stylesheet" href="core/pelatform-ui-core.prefixed.min.css">
<link rel="stylesheet" href="components/button.prefixed.min.css">
<link rel="stylesheet" href="components/form.prefixed.min.css">
<link rel="stylesheet" href="components/badge.prefixed.min.css">
<link rel="stylesheet" href="components/modal.prefixed.min.css">
<link rel="stylesheet" href="components/table.prefixed.min.css">
<link rel="stylesheet" href="components/popover.prefixed.min.css">
<link rel="stylesheet" href="components/dropdown-menu.prefixed.min.css">
<link rel="stylesheet" href="components/collapsible.prefixed.min.css">
<link rel="stylesheet" href="components/slider.prefixed.min.css">
<link rel="stylesheet" href="components/select.prefixed.min.css">
<link rel="stylesheet" href="extensions/datatable.prefixed.min.css">
<link rel="stylesheet" href="icons/icons.prefixed.min.css">
<script src="core/pelatform-ui-core.min.js" defer></script>
<script src="components/modal.min.js" defer></script>
<script src="components/popover.min.js" defer></script>
<script src="components/dropdown-menu.min.js" defer></script>
<script src="components/slider.min.js" defer></script>
<script src="components/select.min.js" defer></script>
<script src="extensions/datatable.min.js" defer></script>
<script src="extensions/datatable-excel.min.js" defer></script>
<script src="extensions/datatable-pdf.min.js" defer></script>
<script>document.addEventListener('DOMContentLoaded', () => PLF.init());</script>Rows are read once, so after changing them call PLF.destroy(container), then PLF.init(container).
With ESM imports:
- Import
DataTablefrom@pelatform/ui/extensions/datatable. - Register
excelAdapterorpdfAdapterwithDataTable.registerExportAdapter(); Extensions lists the adapters’ vendors. - Call
use(DataTable)beforeinit().
Hooks
| Attribute | Purpose |
|---|---|
data-plf-datatable |
The datatable container |
data-plf-datatable-controls |
Toolbars and footers, authored hidden; the search field and the paging buttons go inside |
data-plf-datatable-filter |
Optional; marks the search field among several search inputs. Without it, the one labeled <input type="search"> is the search field, which is required |
data-plf-datatable-empty |
The message shown when no rows match, outside the table |
data-plf-datatable-status |
Optional; without it, the role="status" element that holds the counters is the status |
data-plf-datatable-count |
A text slot: matched in the status is required; total, page, pages, from, to and selected are optional |
data-plf-datatable-previous, data-plf-datatable-next |
Required paging buttons; data-plf-datatable-first and data-plf-datatable-last are optional. Each draws its chevron |
data-plf-datatable-page-input |
Optional; without it, the number input among the paging buttons types a page. The script adds its instructions unless it has aria-describedby |
data-plf-page-help |
On the container, the page input’s instructions; Type a page number, then press Enter or move focus away. by default |
data-plf-datatable-pages |
An optional container for numbered page buttons, cloned from one button in a <template data-plf-datatable-page-template>; data-plf-page-label names them, Page {page} by default |
data-plf-page-size |
On the container, rows per page; 10 by default |
data-plf-datatable-page-size-control |
An optional <select> of page sizes that includes the starting one |
data-plf-column |
Each header’s unique key |
data-plf-type |
text (default), number or date |
data-plf-value |
On a number or date cell, the value to sort and filter by, such as 1200. or 2026-01-31 |
data-plf-searchable |
Includes the column in search |
data-plf-datatable-sortable |
On a header, the script adds a sort button around the header’s content, which names the column |
data-plf-sort-label |
On the container, the sort buttons’ name; Sort by {label} by default, {label} being the header’s text |
data-plf-multi-sort |
On the container, Shift adds a column to the sort, and each sort button shows its place |
aria-sort |
ascending or descending on one sortable header for the starting sort |
data-plf-datatable-column-filter |
A text input or <select> that filters the column with that key |
data-plf-datatable-filter-min, data-plf-datatable-filter-max |
Number or date inputs for a range on that column, inclusive |
data-plf-datatable-clear-filters |
A button that clears search and every filter |
data-plf-datatable-select-row, data-plf-datatable-select-all |
Optional; without them, the checkbox in a header selects the page and the checkbox in its column selects each row. Author them hidden: the script shows them |
data-plf-selection-scope |
On the container, page (default) or filtered for the header checkbox |
data-plf-datatable-row-controls |
Menus and other controls in rows, authored hidden |
data-plf-row |
An optional unique row key for state and exports; the row id otherwise |
data-plf-datatable-bulk |
A bar shown while rows are selected |
data-plf-datatable-select-filtered, data-plf-datatable-clear-selection |
Buttons that select every matching row or clear the selection |
data-plf-datatable-action |
A button that fires plf: for its row, or for the selected rows outside the body |
data-plf-datatable-export-format |
A button: csv, copy, print, and xlsx or pdf with their adapter |
data-plf-datatable-export-scope |
Optional; without it, the <select> whose options are filtered (default), all and selected |
data-plf-no-export |
On a header, leaves the column out of exports |
data-plf-export-text |
On a cell, the text to export instead of its content |
data-plf-datatable-column-toggle |
A checkbox that shows or hides the column with that key |
data-plf-required, data-plf-fixed |
On a header, the column cannot be hidden, or cannot move |
data-plf-width |
A header’s starting width in pixels, from 24 to 2000 and within its width control’s min and max |
data-plf-datatable-column-width |
A range input that sets the column width |
data-plf-datatable-move-column, data-plf-datatable-move |
A button that moves the column by -1 or 1 |
data-plf-density |
On the container, comfortable (default) or compact |
data-plf-datatable-density-control |
Optional; without it, the <select> with the comfortable and compact options |
data-plf-sticky |
On the container, header, column or both: keeps the header row, the first visible column or both in view while the table scrolls |
data-plf-storage-key |
Keeps the view and saved views in localStorage under this key |
data-plf-datatable-views |
A <select> holding one option, with an empty value, for the current view: the script lists the saved views after it, and choosing one applies it |
data-plf-datatable-view-name, data-plf-datatable-view-save |
A name field and its button: Save, or Enter in the field, saves the current view under the name |
data-plf-datatable-view-update, data-plf-datatable-view-delete |
Buttons that save over or delete the view chosen in the select; disabled while none is chosen |
data-plf-datatable-views-status |
An <output> or role="status" element for the views’ messages |
data-plf-view-saved, data-plf-view-deleted, data-plf-view-applied |
On the container, the messages, {name} being the view’s name; Saved '{name}'., Deleted '{name}'. and Applied '{name}'. by default |
data-plf-view-taken, data-plf-view-invalid |
On the container, the messages for a name already saved and an empty or long one; That name is already saved. and Enter a view name of 1–80 characters. by default |
data-plf-view-modified |
On the container, the chosen view’s option once the table changes; {name} by default |
data-plf-datatable-reset |
A button that restores the starting view and selection |
The script adds data-plf-datatable-selected to selected rows and data-plf-datatable-sort-direction to sorted headers.
Methods
| Method | Behavior |
|---|---|
PLF. |
Returns the instance, creating it; options takes pageSize, multiSort, density, storageKey and export (title, filename, fontBytes) |
PLF. |
Returns the instance or null |
PLF. |
Adds a download format; adapter. returns {bytes, mimeType, extension} |
instance. |
Returns a copy of the sorts, query, filters, page, counts, selection, columns and density |
instance., setSorts |
Sorts one column asc, desc or null, adds it to a multi-sort, or replaces every sort with [{column, direction}] |
instance., setColumnFilter, clearFilters |
Searches, filters one column (match is contains or exact), or clears every filter |
instance., setPageSize |
Goes to a page, counted from 1, or changes the rows per page |
instance., selectRows, selectPage, selectFiltered, clearSelection |
Selects rows; pass false to the first four to deselect |
instance. |
Returns the selected <tr> rows in sorted order |
instance. |
Returns a row’s texts by column key, as exports read them, such as {name: |
instance. |
Returns the active column filters as {key, label, text, min, max}, in column order |
instance., setColumnOrder, setColumnWidth |
Changes the columns; a null width clears it |
instance. |
Sets comfortable or compact |
instance., applyView, deleteView, getViews |
Keeps named views of the sorts, filters, page size, columns and density |
instance. |
Restores the starting view and selection |
instance. |
Fires plf: for a row, or for the selected rows without one |
instance., exportData |
Returns the rows to export, or downloads, copies or prints them; settings takes scope, visibleColumns, title and filename |
instance. |
Removes listeners and restores the original row order |
Methods return false when a change is invalid, canceled or changes nothing.
Events
| Event | When |
|---|---|
plf: |
Before a sort, filter, page, selection, column, density or view change; cancelable |
plf: |
After the change |
plf:, plf: |
Before and after an action request; the first is cancelable |
plf: |
Before an export; cancelable |
plf:, plf:, plf: |
An export starts, succeeds or fails |
plf: |
Every started export ends, with result success, error or aborted |
plf:, plf: |
Before and after a saved view is saved or deleted; the first is cancelable |
Each event’s detail:
| Property | Events | Value |
|---|---|---|
instance |
All | The DataTable instance |
trigger |
All | The control used, or null from a method or a saved-view control |
reason |
All | trigger when trigger is set, otherwise api |
previousState |
before-change, change |
The state before the change, shaped like getState |
state |
before-change, change |
The state after the change |
action |
before-action, action |
The action’s name |
row |
before-action, action |
The action’s row, or null for the selected rows |
rows |
before-action, action |
The rows it covers |
state |
before-action, action |
The current state |
format |
before-export, export-start, export, export-error, export-end |
csv, copy, print, or an adapter’s format, such as xlsx or pdf |
data |
before-export, export-start, export, export-error, export-end |
The rows and columns it takes, as getExportData returns them |
state |
before-export, export-start, export, export-error, export-end |
The state when the export started |
error |
export-error |
The error that made it fail |
action |
before-views-change, views-change |
save or delete |
name |
before-views-change, views-change |
The view’s name |
previousViews |
before-views-change, views-change |
The saved views before, as getViews returns them |
views |
before-views-change, views-change |
The saved views after |
persisted |
views-change |
Whether localStorage kept the views |
| Key | Action |
|---|---|
| Enter, Space | On a sort button, sorts ascending, then descending, then unsorted |
| Shift+Enter, Shift+Space | With data-plf-multi-sort, adds the column to the sort, as Shift+click does |
| Enter | In the page input, goes to the typed page |
| Escape | In the page input, restores the current page |
| Variable | Default |
|---|---|
--plf-datatable-gap |
--plf-space-4, 1rem (16px), between the toolbar, table and footer |
--plf-datatable-filter-max-width |
24rem (384px), for the search field |
--plf-datatable-page-input-width |
At least 2. (40px), growing with the page count |
--plf-datatable-icon-size |
--plf-space-4, 1rem (16px), the sort icon |
--plf-datatable-status-color |
--plf-color-text-muted, for the status and empty message |
--plf-datatable-max-height |
32rem (512px), the scroll height with a sticky header |
--plf-datatable-selected-bg |
--plf-color-primary-subtle, on selected rows and the bulk bar |
--plf-datatable-pinned-shadow |
A 1px --plf-color-border-subtle line beside a sticky column |