Design system

Library toolbar

The row above a library grid: the scope switch on the left and its filter menus on the right.

import { FolderFilter, Toolbar } from "@/pages/library/LibraryPage";
import ComposerMenu from "@/pages/harness/ComposerMenu";
import "@/pages/library/library.css";

The toolbar sits above every grid on the Components and Templates tabs of the Library. It says what is shown on the left (the scope switch, "My workspace" or "Library", with a count) and offers filters as menus on the right. It is made of three pieces:

  • Toolbar, a row with a lead slot on the left and its children pushed to the right.
  • The scope switch, which is markup the Library page builds and passes as lead. It is not a component.
  • FolderFilter, a Folder menu plus a chip for the tag picked on a card. Other filters, such as Status and Type, are composer menus.

In use

The workspace scope has Folder and Status filters. Switch to Library and it has only Type, because library components have no folder or status yet.

const lead = (
  <div className="lib-scope" role="group" aria-label="Show">
    {scopes.map((s) => (
      <button
        key={s.id}
        type="button"
        aria-pressed={scope === s.id}
        className={`lib-scope__option${scope === s.id ? " lib-scope__option--active" : ""}`}
        onClick={() => setScope(s.id)}
      >
        {s.label}
        <span className="lib-scope__count">{s.count}</span>
      </button>
    ))}
  </div>
);

<Toolbar lead={lead}>
  <FolderFilter folders={folders} list={items} folder={folder} onFolder={setFolder} tag={tag} onTag={setTag} />
  <ComposerMenu
    label="Status"
    value={filter}
    options={STATUS_FILTERS.map((f) => ({ id: f.id, label: `${f.label} ${count(f.id)}` }))}
    onChange={setFilter}
  />
</Toolbar>

Each menu option carries its count in the label ("Demo campaigns 4", "Draft 1"), and the "All" option counts everything in list.

A tag picked on a card

Tags are picked by clicking one on a card (see Filing). While a tag is active, FolderFilter shows it as a blue chip before the Folder menu. Clicking the chip clears it.

<FolderFilter folders={folders} list={items} folder={folder} onFolder={setFolder} tag="demo" onTag={setTag} />

Scope only

When the workspace is empty there is nothing to filter. The tab still renders the toolbar with just the scope switch, so people can move to Library from the same place.

<Toolbar lead={lead} />
<Empty icon={Shapes} title="Your workspace has no components yet" … />

FolderFilter also renders nothing when folders is empty and no tag is picked.

Other uses of the scope switch

The same .lib-scope classes draw two more segmented controls:

  • .lib-scope--fill stretches the options to the full width. Page settings uses it for "Public" and "Anyone with the password".
  • .lib-scope--view puts the switch on a --color-grey-100 track. The page builder uses it for Preview and Code, with role="tablist" and role="tab" instead of aria-pressed.
<div className="lib-scope lib-scope--fill" role="group" aria-label="Restrict access">
  {[["public", "Public"], ["password", "Anyone with the password"]].map(([id, label]) => (
    <button key={id} type="button" aria-pressed={access === id} className={`lib-scope__option${access === id ? " lib-scope__option--active" : ""}`} onClick={() => setAccess(id)}>
      {label}
    </button>
  ))}
</div>

Anatomy

PartClassStyle
Row.lib-sortFlex row, items centred, space between, 12px gap.
Filters.lib-sort__filtersFlex row with an 8px gap, pushed right with margin-left: auto.
Scope track.lib-scope2px padding, 8px corners, --color-grey-50 fill.
Scope option.lib-scope__option12px text in --color-grey-500, 5px by 12px padding, 6px corners. Hover turns the text to the primary text colour.
Active option.lib-scope__option--activeWhite fill, 1px --color-grey-100 border, a 1px shadow, medium weight.
Count.lib-scope__countTabular figures in the muted text colour (#8E93AF).
Tag chip.lib-tag.lib-tag--on11px text on --color-primary-50, --color-primary-300 border, --color-primary-700 text, fully rounded.
Folder menuComposerMenuA secondaryGhost Button at md (32px), label then value in medium weight, then a caret.

Props

Toolbar

PropTypeDefaultNotes
leadReactNodeLeft side. The scope switch on the Library page.
childrenReactNodeRight side. Filter menus and the tag chip.

FolderFilter

PropTypeDefaultNotes
foldersstring[]Folder names in use. The Library page gets them from foldersOf(items) in useLibraryStore.js.
list{ folder }[]Everything that can be filtered, used for the counts in the menu.
folderstring | nullThe picked folder. null shows "All".
onFolder(folder: string | null) => voidCalled with null when "All" is picked.
tagstring | nullThe tag picked on a card. Shown as a chip when set.
onTag(tag: null) => voidCalled with null when the chip is clicked.

All six are needed. There are no defaults.

Accessibility

  • The scope options are <button> elements with aria-pressed, grouped by role="group" and aria-label="Show". Screen readers announce each as a toggle button.
  • The focus ring on a scope option is a 2px --color-primary-300 outline inset by 2px.
  • The menus are product dropdown menus: arrow keys move through the options, Enter picks one, Escape closes.
  • The tag chip reads "Tagged demo ×". The "×" is text, so some screen readers say "times". It has no aria-label that says it clears the tag.

Do and don't

  • Do lead with the scope, then filters on the right, as in menus and filters.
  • Do put counts in the scope and in the menu options, so people can see what a filter will leave before they pick it.
  • Do pair a filtered-out grid with the "No components match these filters" empty state and a "Clear filters" action.
  • Don't add a third scope option. If more than two lists are needed, they belong in tabs.
  • Don't use the scope switch for settings that change data. It only changes what is shown. Page settings is the one exception, through .lib-scope--fill.

When to use it

Use Toolbar and FolderFilter for grids of library items. For a two-option switch elsewhere in the product, use TabToggle from @/ui; the .lib-scope classes are local to the library.

Where it is used

FileWhat
src/pages/library/LibraryPage.jsx, LibraryPageBuilds the scope switch with counts and passes it as lead.
src/pages/library/LibraryPage.jsx, WorkspaceComponentsToolbar with FolderFilter and a Status menu.
src/pages/library/LibraryPage.jsx, LibraryComponentsToolbar with a Type menu.
src/pages/library/LibraryPage.jsx, TemplatesToolbar with FolderFilter in the workspace scope, and nothing on the right in the library scope.
src/pages/library/PageSettings.jsx.lib-scope--fill for access control.
src/pages/library/PageBuilderPage.jsx.lib-scope--view for Preview and Code.

On this page