mica
mica

One collection, composed from small pieces

Mica styles a native table, a filter/selection toolbar, a scroll region, and a pagination footer. Your app owns the rows, query, sort order, visible columns, selection, and current page. There is no table engine, renderer, or framework dependency.

Open the complete example. Try filtering, sorting, selecting a page, column visibility, row actions, and empty/loading/error states. Its application script is example code, not part of the Mica module.

Start with a native table

<section data-data-table aria-labelledby="projects-title">
  <header data-table-heading>
    <div><h2 id="projects-title">Projects</h2><p>Keep the work in motion.</p></div>
    <a href="/projects/new">New project</a>
  </header>
  <m-table-scroll style="--table-min-width: 36rem">
    <div data-table-region role="region" aria-label="Projects table" tabindex="0">
      <table data-table>
        <caption data-visually-hidden>Projects and their owners</caption>
        <thead><tr><th scope="col">Project</th><th scope="col">Owner</th></tr></thead>
        <tbody><tr><td><a href="/projects/website">Website refresh</a></td><td>Alex Morgan</td></tr></tbody>
      </table>
    </div>
  </m-table-scroll>
</section>
<!-- Optional visual overflow indicators; scrolling already works. -->
<script type="module" src="table.js"></script>

Use a caption and column headers with scope="col". This remains a table, not an ARIA grid: Tab visits controls and links, while assistive technology provides table navigation. Author real links for row destinations and explicit buttons for actions; do not make the whole row a fake button.

The scroll region is keyboard focusable and named. It scrolls horizontally; rows flow vertically with the page. The optional table.js import only measures overflow and paints edge indicators. It observes resizing and authored content changes, supports RTL, and cleans up when disconnected. It adds no rows, controls, data behavior, or events.

Filters and selection share one space

<div data-table-toolbar>
  <form data-table-filters action="/projects" method="get">
    <label data-table-search>Search projects <input type="search" name="q"></label>
    <button>Search</button>
  </form>
  <div data-table-selection role="group" aria-label="Selected project actions" hidden>
    <strong role="status">2 selected on this page</strong>
    <button type="button">Archive</button>
    <button type="button" data-table-clear>Clear selection</button>
  </div>
</div>

The two panels must be direct children of data-table-toolbar. When selection exists, your app removes hidden from the selection panel and sets inert on the filters. CSS hides the filters while retaining their layout space, so selecting a row does not move the table. Clear selection reverses both changes. Keep the actions short; reserve additional height with --table-toolbar-min-height if your action set needs more space than the filters.

// Application code, scoped to this collection:
selectionPanel.hidden = selectedIds.size === 0;
filters.inert = selectedIds.size > 0;
pageCheckbox.checked = rows.length > 0 && selectedIds.size === rows.length;
pageCheckbox.indeterminate = selectedIds.size > 0 && selectedIds.size < rows.length;
// Update each row checkbox and toggle data-selected on its tr.

Give each checkbox a record-specific accessible name. Keep focus on the checkbox when selection changes. After a bulk action or Clear selection, restore focus to a surviving control. Set a concise live selection count. Native checkbox indeterminate is a JavaScript property; CSS does not implement selection logic.

The example selects only the current page, and clears selection on page, filter, search, page-size, or sort changes. Say so in the select-all label and action count. An application that selects across pages must explicitly explain that larger scope and handle records not loaded in the browser.

Sort, find, and act

Put a native button inside each sortable header and set aria-sort="ascending" or "descending" on the currently sorted th. Remove the attribute from other headers. Keep any arrow decorative with aria-hidden="true". The application reorders records and maintains focus; Mica styles the header state.

Search and filters determine results; sorting orders them; pagination slices them. Reset to page one when the query or page size changes. Clamp the page after deletions. For server data, store these values in the URL, send them with the request, discard stale responses, and render the returned total and rows together.

Column visibility and row actions can use native popovers with labeled checkboxes and buttons. Hide both header and body cells for a column; update any state cell’s colspan. The example keeps selection, project names, and row actions available. If the sorted column is hidden, it returns to Project sorting.

Show the result range once, in the footer. Offer Clear filters only while filters are active. Use a separate result announcement after bulk actions. Mica imposes no icons, column schema, request library, or mutation API.

Narrow viewports and collection states

Within data-data-table, columns marked data-table-sticky stick to the inline start below 44rem. Set --table-sticky-offset on each pinned cell (including its header) to the combined width of earlier pinned columns. Author column widths so the pinned region leaves space to discover other columns. The example sets --table-min-width:0px and gives every column an explicit width, so hiding a column does not redistribute a larger minimum width into the pinned cells. The example freezes selection and project names; scroll indicators sit at the frozen-column boundary.

Use a full-width td data-table-state for first-use empty, no matches, or request failure. Include an appropriate next action: create, clear filters, or retry. Set data-state on the scroll region for a compact state table with hidden headers, and supply a colspan matching visible columns.

For loading, set aria-busy="true" on the table region and use decorative skeleton rows with aria-hidden="true". Keep “Loading…” in a live status outside that busy region. Disable paging while the result is unknown. Loading, empty, no matches, and failure should have distinct messages.

Compose the footer with pagination. It contains the only result range, an optional rows-per-page select, and page navigation. Narrow footers can replace numeric links with an authored “Page 2 of 6” status while preserving Previous and Next.

API and customization

HookContract
data-data-tableCollection container, usually a named section. Establishes responsive layout.
data-table-headingHeading/description and optional creation action.
data-table-toolbarDirect filter and selection panels; preserves filter height.
data-table-filters, data-table-searchWrapping controls and an expanding search label.
data-table-selection, data-table-clearHidden until app selection exists; clear action aligns to the end.
m-table-scrollOne direct data-table-region child containing a native table. Optional module measures edges.
data-tableNative table with authored caption, headers, and rows.
data-table-stickyOptional narrow-screen pinned header/body cells, with explicit offsets.
data-selectedApplication-owned presence attribute on a selected tr; quiet fill.
data-table-descriptionSecondary cell text; visual ellipsis preserves the full text in the DOM.
data-table-state, data-stateState cell and optional state-mode scroll region.

Set --table-min-width on m-table-scroll (default 54rem), and --table-sticky-offset on cells (default 0). Collection spacing uses --table-padding (space-lg), --table-cell-padding (space-sm), and --table-font-size (.8125rem). Set overrides directly on the collection, scroll wrapper, or footer that needs them. Native text fields remain at least 16px.

Other knobs: --table-heading-size (1.375rem), --table-toolbar-min-height (0px), --table-selected-background (surface-raised), --table-edge-width (space-md), --table-edge-color (14% text color), --table-description-size (.75rem), --table-sort-target (2rem), --table-state-padding (space-2xl), and --table-state-font-size (1rem). Internal data-m-* attributes and --m-* properties belong to the edge enhancement.

← sidebarstepper →