@marmooo/table is a dependency-free HTML <table> library.
Sorting, per-column search, pagination, column visibility, resizing, and cell editing β each an opt-in plugin or component. Every table on this page is a live, working instance.
1. Basic table
No plugins, no components β just data and columns. A column can supply render(row, td) to control how a cell is drawn.
import { Editable, Resizable, Sortable, Table } from "@marmooo/table";
// (the examples below reuse this same import)
new Table({
data: tracks,
columns: [
{ id: "title", name: "Title" },
{ id: "artist", name: "Artist" },
{ id: "duration", name: "Duration", render: (row, td) => {
td.textContent = formatDuration(row.duration);
} },
],
}).render(document.querySelector("#table-basic"));2. Sortable
Click a header to sort, click again to reverse. Headers are also keyboard-focusable β try Tab then Enter or Space. The current column/direction is exposed via aria-sort and the arrow icon.
const table = new Table({ data: tracks, columns });
table.options.plugins = [new Sortable(table)];
table.render(container);3. Non-sortable column + custom compare
sortable: false keeps a column (like an action button) out of sorting entirely. compare lets a column define its own ascending-order comparator β useful for dates, which sort incorrectly as plain strings.
{
id: "releasedAt",
name: "Released",
compare: (a, b) => new Date(a.releasedAt) - new Date(b.releasedAt),
},
{
id: "actions",
name: "",
sortable: false,
render: (row, td) => { td.appendChild(removeButton(row)); },
}4. Column search
Each column gets its own search input in the header (AND across columns, substring match within one). Typing something that matches nothing marks the offending input as invalid β try typing zzz in the Title box.
new Table({
data: tracks,
columns,
components: { columnSearch: { placeholder: "Searchβ¦" } },
}).render(container);5. Pagination
Set a pageSize and a container for the page controls. table.pagination.goToPage(n) and getTotalPages() are available for programmatic control.
new Table({
data: manyTracks,
columns,
components: {
pagination: { pageSize: 5, container: paginationEl, maxPageButtons: 5 },
},
}).render(container);6. Column selector
Lets people show/hide columns from a dropdown checklist. Works together with Resizable, which auto-resets widths when visibility changes.
new Table({
data: tracks,
columns,
components: { columnSelector: { container: selectorEl } },
}).render(container);7. Resizable columns
Drag a column border to resize it.
const table = new Table({ data: tracks, columns });
table.options.plugins = [new Resizable(table)];
table.render(container);8. Editable cells
Click any body cell to edit it in place. Editable doesn't write the value back into data β read it from the DOM (e.g. on blur) if you need to persist it.
const table = new Table({ data: tracks, columns });
table.options.plugins = [new Editable(table)];
table.render(container);9. Custom sort indicator
components.sortIndicator.render replaces the default arrow. It's called with the current direction and its return value replaces the old indicator β return null to hide it for a given state.
components: {
sortIndicator: {
render: (direction) => {
if (direction === "none") return null;
const span = document.createElement("span");
span.textContent = direction === "ascending" ? " β²" : " βΌ";
return span;
},
},
}10. setData()
Swap the underlying rows at any time β existing filters/sort are re-applied and pagination resets to page 1.
const table = new Table({ data: tracks, columns });
table.render(container);
// later, e.g. after an async fetch:
table.setData(freshRows);11. Everything together
Sortable + resizable + column search + pagination + column selector, all on one table.
See README.md for the full API reference.