TableToolbar
ImplementedComposing 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).
patterns/molecules/tabletoolbar.htmlSearch + 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)
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.
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') andbulkActionsLabel(default'Bulk actions') — the last two landed in mol46 step 6. PlussearchPlaceholder/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 viaaria-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 > 0as a conditionalrole="group"named bybulkActionsLabel— no dead controls, but also noaria-live: selection announcements belong to the consuming table. - Filter chips are native
aria-pressedbuttons 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-minclasses + inline-style fallback with-my-2centering (CopyButton precedent); the Input atom no longer wraps interactive trailing content inaria-hidden— the clear button is focusable and announced.
Props
searchPlaceholderstringSearch…Placeholder text for the search input. Also the accessible-name fallback when searchLabel is not passed — localize it on locale-aware hosts.
searchValuestringControlled search value (defaults to the empty string). When non-empty, the clear button renders after the input.
onSearchChange(value: string) => voidChange 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.
searchLabelstringAccessible name for the search input. Falls back to searchPlaceholder (aria-label chain — there is no visible label element).
searchAriaControlsstringid of the region the search input controls (aria-controls). Wire it to the table wrapper so screen readers announce the relationship.
hideSearchbooleanfalseOmit 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.
filtersTableToolbarFilter[]Filter chip group rendered between search and bulk actions. Each entry is { id, label, active?, onToggle? } — only filters whose membership flipped fire their onToggle.
filtersLabelstringFiltersAccessible 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.
clearSearchLabelstringClear searchAccessible 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.
bulkActionsLabelstringBulk actionsAccessible 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.
trailingReact.ReactNodeRight-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.
selectedCountnumber0Number of currently selected rows. When 0 / undefined the bulk actions do not render at all. Mirrored to data-selected-count for styling and e2e.
bulkActionsReact.ReactNodeBulk action controls, rendered right-aligned (ml-auto) inside a conditional role="group" named by bulkActionsLabel (default "Bulk actions").
ariaLabelstringTable toolsAccessible name for the toolbar container (role="toolbar"). Mandatory in practice on locale-aware surfaces — every storefront consumer passes a localized label.
classNamestringAdditional classes appended to the toolbar root (base: flex flex-wrap items-center gap-3 py-3 px-4).
Props
TableToolbar
searchPlaceholderstring'Search…'Placeholder text shown in the search input.
searchValuestring—Controlled search value.
onSearchChange(value: string) => void—Change handler.
searchLabelstring—Accessible name for the search input.
searchAriaControlsstring—aria-controls id of the searched region.
clearSearchLabelstring'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.
filtersTableToolbarFilter[]—Optional filter chip group.
filtersLabelstring'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.
trailingReact.ReactNode—Right-aligned trailing content.
selectedCountnumber—When >0, bulk actions render.
bulkActionsReact.ReactNode—Bulk action controls.
bulkActionsLabelstring'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.
ariaLabelstring—Accessible label for the toolbar container.
classNamestring—Extra className passed to the toolbar root.
hideSearchbooleanfalseOmit the search input entirely (§126 Wave 3 #2) — toolbars that only host filters + bulk actions.
<TableToolbar />Accessibility
ARIA
2 rulesType
1 ruleUsage
Import
import { TableToolbar } from "@epasslive/design-system"Example 1
<TableToolbar searchValue={q} onSearchChange={setQ} searchLabel={t("searchLabel")} searchAriaControls="table-region" filters={chips} filtersLabel={t("filtersLabel")} ariaLabel={t("toolbarLabel")} />Example 2
<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