Passer au contenu principal

TableToolbar

Implemented

Composing molecule: search input (44px-hit clear button) + filter chip group + optional trailing slot + conditional bulk actions, wrapped in role=toolbar. Every announced string has an EN default plus an i18n port (filtersLabel, clearSearchLabel, bulkActionsLabel).

Categorymolecules
Sourcepatterns/molecules/tabletoolbar.html
AI HintSearch + filter-chip + bulk-actions row for table-like surfaces. Composes Input (search + clear) and FilterChipGroup (filters). Point searchAriaControls at the table region id. On locale-aware surfaces pass ariaLabel, filtersLabel, clearSearchLabel, bulkActionsLabel and searchPlaceholder from i18n keys — the EN defaults leak on /fr otherwise. Use hideSearch when filtering is server-side.

Search + filter chips + bulk-actions toolbar row for data tables. Composes the Input atom (search slot) and FilterChipGroup (facet strip) inside a role="toolbar" widget; the bulk cluster only renders while rows are selected. The molecule is pure composition — no new atom, no new token.

TableToolbar

Table toolbar molecule — Content & Layout Molecules, Family 6

Single toolbar row for DataTable-like surfaces: search input (leading search icon, conditional clear button), optional filter chip strip, optional trailing content, and a bulk-actions cluster gated on selectedCount > 0. The toolbar root is transparent — it inherits the host surface. Announcements (result counts, selection changes) belong to the consuming table, not this molecule.

Search + bulk actions (selectedCount gating)

Table region the search controls (aria-controls wiring).

selectedCount: 3 — the bulk cluster only renders while > 0. Type in the search to reveal the clear button.

Filters + filtersLabel (localized group name)

Active filter: confirmed — only the filter whose membership flipped fires its onToggle.

i18n ports — localized clearSearchLabel + bulkActionsLabel (fr host)

With search text present the clear button renders — its accessible name is clearSearchLabel; the bulk group (selectedCount 2) is named by bulkActionsLabel. EN defaults keep EN-only hosts unchanged.

hideSearch — filters-only toolbar with a trailing count

Server-side search surfaces omit the search slot entirely instead of wiring a no-op handler; result counts live in the trailing slot.

This component supports light and dark mode.

Usage

All props are optional — a bare <TableToolbar /> renders an empty toolbar row, so pass the slots you need. The bulk cluster appears only while selectedCount > 0. Wire searchAriaControls to the table wrapper id.

import { TableToolbar } from '@epasslive/design-system'

// Search + bulk actions:
<TableToolbar
  searchPlaceholder="Search orders…"
  onSearchChange={setQ}
  selectedCount={3}
  bulkActions={bulkButtons}
/>

// Filter-only toolbar on a locale-aware host:
<TableToolbar
  hideSearch
  searchAriaControls="bookings-table-region"
  filters={filters.map((f) => ({
    id: f.id,
    label: t(`filters.${f.id}`),
    active: status === f.id,
    onToggle: () => setStatus(f.id),
  }))}
  filtersLabel={t('filterAriaLabel')}
  ariaLabel={t('toolbarAriaLabel')}
/>

// All four i18n ports on a locale-aware host (mol46 step 6 added the last two):
<TableToolbar
  searchPlaceholder={t('searchPlaceholder')}
  clearSearchLabel={t('clearSearchLabel')}
  bulkActionsLabel={t('bulkActionsLabel')}
  filtersLabel={t('filtersLabel')}
  ariaLabel={t('toolbarAriaLabel')}
/>

TableToolbar vs. TextFormatToolbar vs. FilterChipGroup

Three toolbar-shaped surfaces overlap by name; pick by what sits above. Rule of thumb: a data table row → TableToolbar; a rich-text editor → TextFormatToolbar; a full admin filter stack → compose FilterChipGroup (or TableToolbar) into the ListPanel toolbar slot; just chips → FilterChipGroup.

Pattern
Best for
Keyboard
Relationship
TableToolbar
The row above a data table: search box + filter chip strip + selection-gated bulk actions. Composes Input and FilterChipGroup.
Plain DOM tab order (no roving tabindex) — Tab reaches search, then each chip, then bulk buttons.
This molecule. Embedded chips render at size="sm" inside a flex-nowrap overflow-x-auto strip.
TextFormatToolbar
Rich-text formatting bar (bold/italic/underline) for text editors — name-adjacent but a completely different molecule.
Own surface — see its catalog entry.
Sibling molecule; do not reach for TableToolbar when editing text, or vice versa.
ListPanel toolbar slot
Admin list pages: the full filter stack (search + FilterChipGroup + sort) composed into the header row of a list panel.
Depends on what you compose — standalone FilterChipGroup chips wrap by default.
The toolbar slot is an opaque ReactNode; the old filter-bar organism was folded into this composition (ADR-041). Recipes: molecules/filterchipgroup.recipes.md.
FilterChipGroup alone
Just a toggleable chip row (date ranges, A–Z indexes) with no search box and no bulk actions.
Native buttons, wraps by default (flex-wrap) instead of scrolling.
TableToolbar embeds it at size="sm"; standalone use gets the wrapping default — nicer for keyboard users than the dense scrolling strip.

i18n status

State as of 2026-08-28 (mol46 step 6) — all four label ports are landed, none are English-hardcoded:

  • Portable (4): ariaLabel (default 'Table tools'), filtersLabel (default 'Filters'), clearSearchLabel (default 'Clear search') and bulkActionsLabel (default 'Bulk actions') — the last two landed in mol46 step 6. Plus searchPlaceholder/searchLabel. Contract: EN defaults keep EN-only hosts unchanged with zero props; locale-aware consumers pass localized values so nothing announces in English on /fr routes.

Accessibility

  • The root is a role="toolbar" composite widget (not a landmark), named via aria-label. Keyboard model is plain DOM tab order — no APG roving tabindex is implemented.
  • The search input has no visible label element; its accessible name comes from the fallback chain aria-label={searchLabel ?? searchPlaceholder}.
  • The bulk slot renders only while selectedCount > 0 as a conditional role="group" named by bulkActionsLabel — no dead controls, but also no aria-live: selection announcements belong to the consuming table.
  • Filter chips are native aria-pressed buttons at ≥44px height (--ep-tap-min); the selected pair is theme-stable at 5.70:1.
  • Tap target (fixed, mol46 step 6): the clear-search button keeps its 20×20px glyph but carries a 44×44px hit rect via --ep-tap-min classes + inline-style fallback with -my-2 centering (CopyButton precedent); the Input atom no longer wraps interactive trailing content in aria-hidden — the clear button is focusable and announced.

Props

Prop
Type
Default
Description
searchPlaceholder
string
Search…

Placeholder text for the search input. Also the accessible-name fallback when searchLabel is not passed — localize it on locale-aware hosts.

searchValue
string
—

Controlled search value (defaults to the empty string). When non-empty, the clear button renders after the input.

onSearchChange
(value: string) => void
—

Change handler called with the new search string. Also fired with an empty string by the clear button. A no-op default keeps an uncontrolled-feeling render safe.

searchLabel
string
—

Accessible name for the search input. Falls back to searchPlaceholder (aria-label chain — there is no visible label element).

searchAriaControls
string
—

id of the region the search input controls (aria-controls). Wire it to the table wrapper so screen readers announce the relationship.

hideSearch
boolean
false

Omit the search input entirely — toolbars that only host filters + bulk actions (server-side search surfaces) render without the search slot instead of wiring a no-op handler.

filters
TableToolbarFilter[]
—

Filter chip group rendered between search and bulk actions. Each entry is { id, label, active?, onToggle? } — only filters whose membership flipped fire their onToggle.

filtersLabel
string
Filters

Accessible label for the embedded FilterChipGroup (i18n port — landed, molecules 31-45). Locale-aware consumers pass a localized label; the default keeps EN-only hosts unchanged.

clearSearchLabel
string
Clear search

Accessible label for the clear-search button (i18n port — mol46 step 6). Locale-aware consumers pass a localized label; the default keeps EN-only hosts unchanged.

bulkActionsLabel
string
Bulk actions

Accessible label for the bulk-actions group (i18n port — mol46 step 6). Locale-aware consumers pass a localized label; the default keeps EN-only hosts unchanged.

trailing
React.ReactNode
—

Right-aligned content rendered after the filter chips (before bulk actions) — live result counts etc. A bare flex child: wide clusters need their own flex/flex-wrap wrapper.

selectedCount
number
0

Number of currently selected rows. When 0 / undefined the bulk actions do not render at all. Mirrored to data-selected-count for styling and e2e.

bulkActions
React.ReactNode
—

Bulk action controls, rendered right-aligned (ml-auto) inside a conditional role="group" named by bulkActionsLabel (default "Bulk actions").

ariaLabel
string
Table tools

Accessible name for the toolbar container (role="toolbar"). Mandatory in practice on locale-aware surfaces — every storefront consumer passes a localized label.

className
string
—

Additional classes appended to the toolbar root (base: flex flex-wrap items-center gap-3 py-3 px-4).

Props

Preview

TableToolbar

Prop
Type
Required
Default
Description
searchPlaceholder
string
Optional
'Search…'

Placeholder text shown in the search input.

searchValue
string
Optional
—

Controlled search value.

onSearchChange
(value: string) => void
Optional
—

Change handler.

searchLabel
string
Optional
—

Accessible name for the search input.

searchAriaControls
string
Optional
—

aria-controls id of the searched region.

clearSearchLabel
string
Optional
'Clear search'

Accessible label for the clear-search button (i18n port — landed, molecules 46). Locale-aware consumers pass a localized label (e.g. 'Effacer la recherche'); the default keeps EN-only hosts unchanged.

filters
TableToolbarFilter[]
Optional
—

Optional filter chip group.

filtersLabel
string
Optional
'Filters'

Accessible label for the filter chip group (i18n port — landed, molecules 31-45). Locale-aware consumers pass a localized label (e.g. 'Filtres') so the group is not announced in English on /fr routes; the default keeps EN-only hosts unchanged.

trailing
React.ReactNode
Optional
—

Right-aligned trailing content.

selectedCount
number
Optional
—

When >0, bulk actions render.

bulkActions
React.ReactNode
Optional
—

Bulk action controls.

bulkActionsLabel
string
Optional
'Bulk actions'

Accessible label for the bulk actions group (i18n port — landed, molecules 46). Same contract as filtersLabel: locale-aware consumers pass a localized label (e.g. 'Actions groupées'); the default keeps EN-only hosts unchanged.

ariaLabel
string
Optional
—

Accessible label for the toolbar container.

className
string
Optional
—

Extra className passed to the toolbar root.

hideSearch
boolean
Optional
false

Omit the search input entirely (§126 Wave 3 #2) — toolbars that only host filters + bulk actions.

Quick use:<TableToolbar />

Accessibility

ARIA roletoolbar

ARIA

2 rules

WCAG 4.1.2

Toolbar root is role='toolbar' with aria-label; the bulk-actions cluster (role='group', aria-label from bulkActionsLabel) only renders when selectedCount > 0

WCAG 2.5.8 / §129 §4.2

Clear-search button hit rect is 44px via --ep-tap-min (class + inline-style fallback, CopyButton precedent); the visible box stays compact

Type

1 rule

NASA Rule 5

filters onToggle fires only for chips whose active membership actually flipped (set-diff semantics, never a blanket refire)

Usage

Import

tsxSnippet
import { TableToolbar } from "@epasslive/design-system"

Example 1

tsxSnippet
<TableToolbar searchValue={q} onSearchChange={setQ} searchLabel={t("searchLabel")} searchAriaControls="table-region" filters={chips} filtersLabel={t("filtersLabel")} ariaLabel={t("toolbarLabel")} />

Example 2

tsxSnippet
<TableToolbar hideSearch filters={chips} filtersLabel={t("filtersLabel")} selectedCount={selected.length} bulkActions={<ExportButton />} />

Anti-patterns

— Do not hand-roll a search input + chip row above a table — adopt this molecule so role=toolbar grouping, the clear affordance and bulk gating stay consistent

— Do not render decorative no-op filter chips (active:false with an empty onToggle) — wire real state or omit the filters prop

Design System v4.1.0 | ePassLive