SuperAPI AI Directory

frontend-component-data-tables-command-palette

Data & analytics

View repository

Updated 10-15 22:18

About this skill

Frontend Component Data Tables + Command Palette

This skill packages two related component patterns : the accessible data table and the Cmd+K command palette. Both rely on the APG Combobox pattern (the palette directly; the sortable-table header sometimes indirectly), and both must respect focus management rules. This skill builds on [[frontend-a11y-aria-patterns]] (ARIA roles), [[frontend-a11y-focus-keyboard-inert]] (focus rules and the inert attribute), and [[frontend-impl-popover-dialog-anchor]] (<dialog>showModal()).

Sources : MDN: HTML table element (verified 2026-05-19), APG: Combobox (verified 2026-05-19), APG: Table (verified 2026-05-19), APG: Grid (verified 2026-05-19), MDN: position (verified 2026-05-19).

Quick Reference

Native <table> skeleton

<table>
  <caption>Q1 sales by region</caption>
  <thead>
    <tr>
      <th scope="col"><button type="button" aria-sort="ascending">Region</button></th>
      <th scope="col"><button type="button" aria-sort="none">Revenue</button></th>
      <th scope="col"><button type="button" aria-sort="none">Growth</button></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">EU</th>
      <td>EUR 1,234,567</td>
      <td>+5.4%</td>
    </tr>
  </tbody>
</table>

Rules :

  • <caption> (first child of <table>) provides the accessible name. Use aria-label ONLY when no visible caption fits the design.
  • <th scope="col"> and <th scope="row"> associate headers with cells so screen readers can announce "Revenue, EU, EUR 1,234,567".
  • The deprecated summary attribute is dropped per MDN: HTML table element (verified 2026-05-19).
  • For tables with colspan / rowspan the simple scope mechanism is insufficient ; use id on headers and headers="<id-list>" on cells.

aria-sort values

Value Meaning
ascending Column is sorted ascending.
descending Column is sorted descending.
none (default) Column is not the active sort.
other Sorted by a non-standard order (e.g. by a derived score).

Exactly ONE <th> should be ascending or descending at a time ; the rest are none.

Sticky <thead>

.table-wrap {
  max-block-size: 60vh;
  overflow: auto;          /* required : sticky needs a scrolling ancestor */
}
.table-wrap thead th {
  position: sticky;
  inset-block-start: 0;
  background: white;       /* required : header would otherwise scroll under */
}

Per MDN: position (verified 2026-05-19), position: sticky requires a non-auto inset value AND a scrolling ancestor. NEVER use border-collapse: collapse together with sticky headers ; the collapsed border lets the body show through. Use border-collapse: separate; border-spacing: 0; instead.

aria-rowcount / aria-colcount for virtualised tables

When the DOM contains only the visible rows (virtualisation), declare the total counts so screen readers can announce position :

<table aria-rowcount="10000" aria-colcount="5">
  <tbody>
    <tr aria-rowindex="42">
      <td aria-colindex="1">...</td>
    </tr>
  </tbody>
</table>

aria-rowindex is 1-based and reflects the row's position in the FULL dataset, not just the current DOM.

Mobile table strategy matrix

Strategy When
Horizontal scroll (display: block; overflow-x: auto; on <table>) Few columns, all important; scrolling is acceptable.
Container-query card reflow Many columns; cards on narrow viewports stack key-value pairs vertically.
Hide-some-columns Each row has a "primary" identity column; secondary columns hide below a threshold.

NEVER force a many-column table to fit a phone viewport without one of these strategies ; the result is unreadable squeezed columns.

Command palette skeleton (Cmd+K)

<button id="cmd-trigger" type="button" aria-haspopup="dialog" aria-controls="cmd-dialog">
  Search commands <kbd>Ctrl K</kbd>
</button>

<dialog id="cmd-dialog" aria-labelledby="cmd-title">
  <h2 id="cmd-title" class="visually-hidden">Command palette</h2>
  <input
    id="cmd-input"
    role="combobox"
    aria-controls="cmd-listbox"
    aria-expanded="true"
    aria-autocomplete="list"
    aria-activedescendant=""
    autocomplete="off"
    placeholder="Type a command..."
  />
  <ul id="cmd-listbox" role="listbox" aria-label="Commands">
    <li role="option" id="cmd-opt-1">Open file</li>
    <li role="option" id="cmd-opt-2">Save file</li>
  </ul>
</dialog>
const trigger = document.getElementById('cmd-trigger');
const dialog = document.getElementById('cmd-dialog');
const input = document.getElementById('cmd-input');
let lastFocus = null;

trigger.addEventListener('click', open);
window.addEventListener('keydown', (e) => {
  if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); open(); }
});

function open() {
  lastFocus = document.activeElement;
  dialog.showModal();
  input.focus();
}

dialog.addEventListener('close', () => {
  if (lastFocus && document.contains(lastFocus)) lastFocus.focus();
});

Per APG: Combobox (verified 2026-05-19) :

  • role="combobox" on the input, aria-controls pointing to the listbox, aria-expanded reflecting popup state, aria-autocomplete="list".
  • DOM focus stays on the input ; aria-activedescendant references the highlighted option's id.
  • NEVER move DOM focus into the listbox.

WCAG 2.1.1 : keyboard shortcut MUST have a visible alternative

A Cmd+K shortcut without a visible trigger button violates WCAG 2.1.1 Keyboard. Provide BOTH :

  • The visible trigger button (<button>Search commands ⌘K</button>).
  • The keyboard shortcut listener.

The button MUST do exactly the same thing as the shortcut.

Decision Trees

Decision : native <table> or <div role="table">?

What is the content?

  Tabular data with rows and columns that semantically belong together
  (sales by region, users by status, transactions).
    -> Native <table>. Free keyboard semantics, free screen-reader
       announcement of "row N of M, column X", free header-cell
       association via scope. No ARIA needed.

  Tabular layout for VISUAL alignment, not semantic tabular data
  (a CSS layout that happens to look like a table).
    -> Use CSS Grid. NEVER use <table> for layout. NEVER use
       <div role="table"> for layout.

  Tabular data that requires composite-widget keyboard model
  (arrow keys move between cells, F2 enters edit mode, single
  tabstop in the whole table).
    -> ARIA grid pattern with role="grid" + role="row" + role="cell".
       Heavier to implement; only when the interaction model
       genuinely matches grid (spreadsheet, large data editor).

  Tabular data with virtualisation (DOM holds only visible rows).
    -> Native <table> with aria-rowcount / aria-colcount AND
       aria-rowindex / aria-colindex on the visible rows.

Decision : mobile table strategy?

How many columns and how critical is each?

  3-4 columns, all critical.
    -> Horizontal scroll. <table> set to display:block; overflow-x:auto.
       Optionally make the first column sticky:
         th:first-child, td:first-child { position: sticky; inset-inline-start: 0; }

  5-8 columns, primary identity + secondary data.
    -> Hide secondary columns below a threshold with a container query:
         @container (max-width: 30rem) {
           th:nth-child(n+4), td:nth-child(n+4) { display: none; }
         }
       Provide a "show all columns" button as an accessible alternative.

  Many columns OR a card-feel preferred on mobile.
    -> Container-query reflow to card layout. Below the threshold,
       each row becomes a labelled grid of key-value pairs. Heavier
       to implement; best mobile reading experience.

  Free-form data that does not have natural keys.
    -> Reconsider whether a table is the right pattern at all.
       A card list with sort and filter may be a better fit.

Decision : command palette focus model?

Always : aria-activedescendant on the combobox input.

NEVER move DOM focus into the listbox while the user types. The APG
Combobox pattern explicitly states this for listbox / grid / tree
popups. Moving DOM focus into the popup breaks the typing flow and
screen-reader announcement.

For a dialog popup (e.g. multi-step command that opens its own form),
DO move DOM focus into the dialog after the user selects the option,
and restore to the input or original trigger on close.

Patterns

Pattern 1 : sortable + sticky + responsive table (full demo)

See examples.md for the complete renderable HTML page.

Pattern 2 : command palette dialog

See examples.md for the complete renderable HTML page.

Pattern 3 : indeterminate select-all checkbox

<th scope="col">
  <input type="checkbox" id="select-all" aria-label="Select all rows">
</th>
const selectAll = document.getElementById('select-all');
const rowChecks = document.querySelectorAll('tbody input[type=checkbox]');

function updateHeader() {
  const checked = [...rowChecks].filter(c => c.checked).length;
  selectAll.checked = checked === rowChecks.length;
  selectAll.indeterminate = checked > 0 && checked < rowChecks.length;
}

selectAll.addEventListener('change', () => {
  rowChecks.forEach(c => c.checked = selectAll.checked);
});
rowChecks.forEach(c => c.addEventListener('change', updateHeader));

indeterminate is a JS-only property (not an HTML attribute) ; screen readers announce "mixed" or "partially checked".

Pattern 4 : sticky first column in horizontal-scroll table

.scroll-table th:first-child, .scroll-table td:first-child {
  position: sticky;
  inset-inline-start: 0;
  background: var(--bg);
  z-index: 1;
}
.scroll-table thead th {
  position: sticky;
  inset-block-start: 0;
  background: var(--bg);
  z-index: 2;   /* higher than first-column to cover the intersection */
}

The top-left intersection cell needs the highest z-index so it covers both axes.

Pattern 5 : registering Cmd+K only when no input is focused

window.addEventListener('keydown', (e) => {
  if (!(e.metaKey || e.ctrlKey) || e.key !== 'k') return;
  // Allow Cmd+K to be intercepted by inputs that need it (rare).
  // For most cases, open the palette regardless.
  e.preventDefault();
  open();
});

Some sites also intercept Cmd+K only when the active element is not an editable field, to leave the shortcut available for browser address bar.

Anti-Patterns Index

See anti-patterns.md. Eight cataloged : <div role="table"> instead of native <table>; missing scope on <th>; sortable button as <div>; sticky <thead> without scrolling parent; command palette using DOM focus on options; Cmd+K without visible UI trigger (WCAG 2.1.1); missing <caption> or aria-label on table; aria-live="assertive" on filter result count (noisy interruption every keystroke).

Reference Links

  • Methods and signatures : full <table> surface, ARIA attributes, APG combobox keyboard model, sticky-header recipe, command-palette state machine.
  • Examples : two renderable HTML demos (data table + command palette) plus six additional patterns.
  • Anti-patterns : eight cataloged anti-patterns with symptom, root cause, and fix.

Cross-references

  • [[frontend-a11y-aria-patterns]] : full APG Combobox surface and accessibility rules.
  • [[frontend-a11y-focus-keyboard-inert]] : focus trap, inert, focus restore.
  • [[frontend-impl-popover-dialog-anchor]] : <dialog>showModal() and popover mechanics.
  • [[frontend-syntax-css-grid-subgrid]] : Grid for the container-query card reflow.
  • [[frontend-impl-responsive-layout-fluid]] : container queries underpinning the mobile-table reflow.