Design.dev design.dev
UI Component Prompt

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

01

Search Input

Debounced text filter with a magnifier icon and inline clear button. Matches across every column.

02

Result Count

Live "Showing X–Y of Z" region announced to assistive tech via aria-live.

03

Sortable Header

Keyboard-focusable column header with a tri-state click cycle: none, ascending, descending.

04

Sort Indicator

Animated caret in the active column that rotates to flip direction without re-rendering.

05

Comparator

Type-aware comparison for strings, numbers, and dates. Stable so equal rows keep insertion order.

06

Status Pill

Colored badge with a leading dot for at-a-glance scanning of categorical values.

07

Sticky Header

Headers stay pinned while the body scrolls. A subtle shadow appears once the user has scrolled.

08

Pagination Nav

First, last, and a window around the current page with ellipses. Disabled at boundaries.

09

Page Size

Selector for rows per page that re-clamps the current page so the visible window stays valid.

10

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
Copied to clipboard