Data Table
An accessible data table with column sorting, full-text search, and pagination. Vanilla HTML, CSS, and JavaScript — no libraries.
Copy → Paste → Ship
Live Preview
Sort · Search · Paginate| Name | Role | Status | Last Active | Score |
|---|
A working example of what the prompt generates — click any column header to sort (none → ascending → descending), use the search box to filter across all columns, and page through the results below. The header is sticky as you scroll.
The Prompt
Build an accessible data table component with column sorting, full-text search, and pagination. Vanilla HTML, CSS, and JavaScript only — no libraries, no frameworks, no CDN scripts.
DATA MODEL
- Accept an array of row objects and a column schema as input
- Each column has: { key, label, type ("string" | "number" | "date"), sortable (default true), align ("left" | "right") }
- Render <table> with semantic <thead> and <tbody>, scope="col" on every header, and a visible aria-label on the table
VISUAL DESIGN
- Dark theme: table background #0d0d0f, headers #131316, body text rgba(255,255,255,0.85)
- 13px body text, 11px uppercase header labels with 0.06em letter-spacing
- Subtle zebra striping on even rows (rgba(255,255,255,0.015))
- Row hover tint in cyan (rgba(0,196,255,0.04))
- Status column rendered as colored pills with a leading dot:
- Active: emerald (#00ff88)
- Pending: amber (#fbbf24)
- Inactive: slate (#94a3b8)
- Blocked: red (#ff6b6b)
- Numeric columns use tabular-nums and right-align
- Round avatars (26px) for the Name column built from initials
SORTING
- Tri-state click cycle on every sortable header: none → ascending → descending → none
- Type-aware comparators:
- string: localeCompare with { sensitivity: "base", numeric: true }
- number: subtract
- date: parse to timestamp, subtract
- Sort must be STABLE — preserve original row order on ties (decorate-sort-undecorate or stable Array.prototype.sort)
- Update aria-sort="ascending" | "descending" | "none" on the active header, set "none" on all others
- Animate a small caret SVG inside the active header (rotate 180° for descending)
SEARCH
- Single text input above the table with a magnifier icon, placeholder, and a clear (×) button that appears when there is text
- Debounce input events at 150ms
- Case-insensitive substring match across ALL column values (coerce non-strings to string for matching)
- Highlight matched substrings inside cell text with <mark> (cyan tint, rounded)
- When the search filter shrinks the result set, clamp the current page so it stays in range
PAGINATION
- Page size selector (10 / 25 / 50) below the table on the left
- Page number nav on the right with prev/next chevron buttons
- Show first, last, current, and ±1 around current; insert "…" placeholders for the gaps
- Disable prev at page 1 and next at the last page; never render fewer than 1 page
- Live "Showing X–Y of Z" count above the table that updates on every filter/sort/page change
ACCESSIBILITY
- All sortable headers are tabindex="0" and respond to Enter and Space
- aria-sort reflects current state on every header
- Pagination <nav> with aria-label, current page button has aria-current="page"
- Live result count region uses aria-live="polite" and aria-atomic="true"
- Empty state ("No results match …") replaces the <tbody> rows when filter zeroes out
- Visible focus rings on every interactive element (2px cyan outline, offset 2px)
- Search input is type="search" with an aria-label
LAYOUT
- Wrap the table in a scroll container (max-height 420px) so the body scrolls but headers stay visible
- thead th uses position: sticky; top: 0 with a higher background and a box-shadow that appears once the container scrolls
- Add a horizontal scrollbar at narrow widths (table min-width 640px)
- Stack toolbar items vertically below 600px
OUTPUT
- Single self-contained HTML file with embedded <style> and <script>
- Render against a sample dataset of ~40 rows (Name, Role, Status, Last Active ISO date, Score 0–100)
- Initial state: no sort applied, search empty, page 1 of (rowCount / pageSize)
- Production-grade aesthetic — should feel like a polished admin dashboard table
Anatomy
Search Input
Debounced text filter with a magnifier icon and inline clear button. Matches across every column.
Result Count
Live "Showing X–Y of Z" region announced to assistive tech via aria-live.
Sortable Header
Keyboard-focusable column header with a tri-state click cycle: none, ascending, descending.
Sort Indicator
Animated caret in the active column that rotates to flip direction without re-rendering.
Comparator
Type-aware comparison for strings, numbers, and dates. Stable so equal rows keep insertion order.
Status Pill
Colored badge with a leading dot for at-a-glance scanning of categorical values.
Sticky Header
Headers stay pinned while the body scrolls. A subtle shadow appears once the user has scrolled.
Pagination Nav
First, last, and a window around the current page with ellipses. Disabled at boundaries.
Page Size
Selector for rows per page that re-clamps the current page so the visible window stays valid.
Empty State
Friendly fallback when the search filter yields no rows, telling the user what they searched for.
Usage Guidelines
Use This When
- Building admin dashboards, settings screens, or internal tools that list structured records
- Working with a few hundred to a few thousand client-side rows where sort and filter are needed
- You want zero bundle weight — no TanStack Table, AG Grid, or DataTables.js
Not Ideal When
- You have millions of rows — you'll need server-side pagination plus row virtualization
- Cells require inline editing, drag reordering, or column resizing — out of scope here
- Your data is hierarchical (tree rows, expandable detail panels) — use a tree-table pattern instead