@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. Column search with datalist
Set datalist: true (globally on columnSearch, or per column, which takes priority) to attach a native <datalist> of that column's unique values to its search input as autocomplete suggestions. Try clicking the Artist search box below.
new Table({
data: tracks,
columns: [
{ id: "title", name: "Title" },
{ id: "artist", name: "Artist", datalist: true },
{ id: "duration", name: "Duration" },
],
components: { columnSearch: { placeholder: "Search…" } },
}).render(container);6. 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);7. 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);8. 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);9. 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);10. 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;
},
},
}11. 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);12. Everything together
Sortable + resizable + column search + pagination + column selector, all on one table.
See README.md for the full API reference.