# Filter bar

Search, filters, sort and Clear all together above a list or a table, with the filters applied as chips and the number of results they leave.

Block: `smbk-filter-bar`. Status: experimental. Figma: the Figma file, https://www.figma.com/community/file/1688324308358870630. Desktop and mobile: differ by presentation only.

## Options

| Axis | Values | Default | Meaning |
|---|---|---|---|

Parts: `smbk-filter-bar__controls`, `smbk-filter-bar__search`, `smbk-filter-bar__filters`, `smbk-filter-bar__narrow-filters`, `smbk-filter-bar__sort`, `smbk-filter-bar__view`, `smbk-filter-bar__opener`, `smbk-filter-bar__opener-count`, `smbk-filter-bar__applied`, `smbk-filter-bar__clear`, `smbk-filter-bar__count`

## Markup

```html
<div role="search" aria-label="Filter projects, preview" class="smbk-filter-bar">
  <div class="smbk-filter-bar__controls">
    <div class="smbk-filter-bar__search">
      <span class="smbk-input smbk-input--size-md smbk-search smbk-search--size-md">
        <i
          class="ph ph-magnifying-glass smbk-icon smbk-icon--size-md smbk-input__icon"
          aria-hidden="true"
        ></i>
        <input
          type="search"
          class="smbk-input__control smbk-search__control"
          aria-label="Search projects"
          placeholder="Search…"
        >
        <kbd class="smbk-kbd smbk-kbd--size-sm smbk-search__kbd" aria-hidden="true">⌘ K</kbd>
      </span>
    </div>
    <div class="smbk-filter-bar__filters">
      <div class="smbk-dropdown__anchor">
        <span class="smbk-filter-bar__opener">
          <button
            type="button"
            class="smbk-btn smbk-btn--tone-outline smbk-btn--size-md"
            aria-label="Filters, 1 applied"
            id="filter-bar-1-opener"
            aria-haspopup="menu"
            aria-expanded="false"
            aria-controls="filter-bar-1-panel"
          >
            <i
              class="ph ph-funnel smbk-icon smbk-icon--size-md smbk-btn__icon"
              aria-hidden="true"
            ></i>
            <span class="smbk-btn__label">Filters</span>
          </button>
          <span
            class="smbk-badge smbk-badge--look-soft smbk-badge--tone-default smbk-badge--size-sm smbk-filter-bar__opener-count"
            aria-hidden="true"
          >
            <span class="smbk-badge__label">1</span>
          </span>
        </span>
        <div class="smbk-dropdown__layer">
          <div
            id="filter-bar-1-panel"
            role="menu"
            aria-label="Filters"
            class="smbk-dropdown smbk-dropdown--size-md smbk-dropdown--align-start"
            tabindex="-1"
            data-scrollbar=""
            hidden=""
          >
            <div role="group" aria-labelledby="filter-bar-1-group-0" class="smbk-dropdown__group">
              <div
                role="presentation"
                class="smbk-dropdown__label"
                id="filter-bar-1-group-0"
              >Status</div>
              <button
                type="button"
                role="menuitemcheckbox"
                aria-checked="true"
                tabindex="-1"
                class="smbk-dropdown__item"
              >
                <span
                  class="smbk-checkbox smbk-checkbox--size-sm smbk-checkbox--tone-default smbk-dropdown__checkbox"
                  aria-hidden="true"
                >
                  <span class="smbk-checkbox__box smbk-checkbox__box--checked">
                    <i
                      class="ph ph-check smbk-icon smbk-icon--size-md smbk-checkbox__icon"
                      aria-hidden="true"
                    ></i>
                  </span>
                </span>
                <span class="smbk-dropdown__text">Status: Open</span>
              </button>
              <button
                type="button"
                role="menuitemcheckbox"
                aria-checked="false"
                tabindex="-1"
                class="smbk-dropdown__item"
              >
                <span
                  class="smbk-checkbox smbk-checkbox--size-sm smbk-checkbox--tone-default smbk-dropdown__checkbox"
                  aria-hidden="true"
                >
                  <span class="smbk-checkbox__box">
                    <i
                      class="ph ph-check smbk-icon smbk-icon--size-md smbk-checkbox__icon"
                      aria-hidden="true"
                    ></i>
                  </span>
                </span>
                <span class="smbk-dropdown__text">Status: Closed</span>
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
    <div class="smbk-filter-bar__sort">
      <span class="smbk-select smbk-select--size-md">
        <button
          type="button"
          id="selectfilter-bar-2"
          class="smbk-input smbk-input--size-md smbk-select__well"
          role="combobox"
          aria-haspopup="listbox"
          aria-expanded="false"
          aria-label="Sort by"
        >
          <span class="smbk-select__value">Newest first</span>
          <i
            class="ph ph-caret-down smbk-icon smbk-icon--size-md smbk-input__icon smbk-select__icon"
            aria-hidden="true"
          ></i>
        </button>
      </span>
    </div>
  </div>
  <div role="group" aria-label="Applied filters" class="smbk-filter-bar__applied">
    <span class="smbk-chip smbk-chip--tone-default smbk-chip--size-sm">
      <span class="smbk-chip__label">Status: Open</span>
      <button type="button" class="smbk-chip__remove" aria-label="Remove Status: Open">
        <i class="ph ph-x-circle smbk-icon smbk-icon--size-md" aria-hidden="true"></i>
      </button>
    </span>
    <button
      type="button"
      class="smbk-link smbk-link--size-sm smbk-filter-bar__clear"
    ><span class="smbk-link__label">Clear all</span></button>
  </div>
  <div role="status" class="smbk-filter-bar__count">33 results</div>
</div>
```

## React

```tsx
import { FilterBar, FilterBarOpener } from './design-system/components/filter-bar/filter-bar.js';
import { SearchField } from './design-system/components/search-field/search-field.js';
import { Dropdown, DropdownLabel, DropdownItem } from './design-system/components/dropdown/dropdown.js';
import { Select } from './design-system/components/select/select.js';

<FilterBar label="Filter projects, preview" search={<SearchField aria-label="Search projects" placeholder="Search…" shortcut="⌘ K" />} filters={<Dropdown label="Filters" trigger={<FilterBarOpener count={1} />}>
  <DropdownLabel>Status</DropdownLabel>
  <DropdownItem checked>Status: Open</DropdownItem>
  <DropdownItem>Status: Closed</DropdownItem>
</Dropdown>} sort={<Select aria-label="Sort by" defaultValue="newest" options={[{ value: "newest", label: "Newest first" }, { value: "oldest", label: "Oldest first" }, { value: "name", label: "Name, A to Z" }]} />} applied={[{ id: "open", label: "Status: Open" }]} onRemove={() => {}} onClearAll={() => {}} count="33 results" />
```

## Stylesheet

```css
/* smbk-filter-bar — Filter bar. Figma "Filter bar" 7117:565: Layout (Wide, Narrow) × Condition (Pristine, Query
 * entered, Filters applied, Loading), 8 variants.
 * A column of up to three rows, the large stack space apart: the controls (the search field, the filters opener, the
 * sort list, the view switch), the filters applied as chips with Clear all, shown only while any are applied, and the
 * result count. The bar has no fill or edge of its own; its controls keep theirs. Every control is the system's own
 * component, at the medium size as Figma draws them; this sheet only lays them out.
 * Narrow (the layout switch): the search field and the filters opener stay; the sort list and the view switch go,
 * since a narrow page reaches them where its filters open, such as a drawer. A page that gives a narrow opener of its
 * own (narrow-filters) shows it in place of the filters part. */

.smbk-filter-bar {
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  gap: var(--smbk-space-stack-lg);
  min-inline-size: 0;
}

/* ---- the controls: the search field takes the room left over ---- */

.smbk-filter-bar__controls {
  display: flex;
  align-items: center;
  gap: var(--smbk-space-inline-md);
  min-inline-size: 0;
}

.smbk-filter-bar__search {
  display: flex;
  flex: 1 1 0;
  min-inline-size: 0;
}

.smbk-filter-bar__filters,
.smbk-filter-bar__view {
  display: inline-flex;
  flex: none;
  align-items: center;
}

.smbk-filter-bar__narrow-filters {
  display: none;
  flex: none;
  align-items: center;
}

.smbk-filter-bar__sort {
  display: flex;
  flex: none;
  inline-size: var(--smbk-size-filter-bar-sort-width);
}

/* The filters opener: the outline button, and beside it the number of filters applied in the small badge. The
   number is the badge's to show and the button's to say: the button's name carries it in words. */
.smbk-filter-bar__opener {
  display: inline-flex;
  flex: none;
  align-items: center;
  gap: var(--smbk-space-inline-xs);
}

/* ---- the filters applied: small removable chips that wrap onto more rows, Clear all after the last ---- */

.smbk-filter-bar__applied {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--smbk-space-inline-sm);
}

/* Clear all is a button drawn as the small link, since it acts on the page rather than going anywhere. */
.smbk-filter-bar__clear {
  flex: none;
}

/* ---- the result count: Body SM in the secondary text color; while results load, the spinner's ring and words
   that say so in its place ---- */

.smbk-filter-bar__count {
  display: flex;
  align-items: center;
  gap: var(--smbk-space-inline-xs);
  color: var(--smbk-color-text-subtle);
  font-family: var(--smbk-typography-body-sm-font-family);
  font-size: var(--smbk-typography-body-sm-font-size);
  font-weight: var(--smbk-typography-body-sm-font-weight);
  line-height: var(--smbk-typography-body-sm-line-height);
  letter-spacing: var(--smbk-typography-body-sm-letter-spacing);
  font-variant-numeric: tabular-nums;
}

/* ---- narrow ---- */

@container style(--smbk-layout: narrow) {
  .smbk-filter-bar__sort,
  .smbk-filter-bar__view,
  .smbk-filter-bar__controls:has(> .smbk-filter-bar__narrow-filters) > .smbk-filter-bar__filters {
    display: none;
  }

  .smbk-filter-bar__narrow-filters {
    display: inline-flex;
  }
}

/* ---- touch: the filters applied and Clear all stop their press areas halfway to each other ----
   On a touch screen each remove cross and Clear all take an invisible area, centred on them; in the row of filters
   applied, which wraps, each reaches only halfway into the row's gaps, so every filter stays one press. Nothing drawn
   moves. */

@media (pointer: coarse) {
  .smbk-filter-bar__clear {
    position: relative;
  }

  .smbk-filter-bar__clear::after {
    content: '';
    position: absolute;
  }

  .smbk-filter-bar__applied .smbk-chip__remove::after,
  .smbk-filter-bar__applied .smbk-filter-bar__clear::after {
    inset: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), var(--smbk-space-inline-sm) / -2);
  }
}
```

## Tokens it reads

```json
{
  "space": {
    "stack": {
      "lg": {
        "$type": "dimension",
        "$value": "{{space.16}}",
        "$description": "Gap between things stacked one above another, large."
      }
    },
    "inline": {
      "md": {
        "$type": "dimension",
        "$value": "{{space.8}}",
        "$description": "Gap between things side by side, medium."
      },
      "xs": {
        "$type": "dimension",
        "$value": "{{space.4}}",
        "$description": "Gap between things side by side, extra small."
      },
      "sm": {
        "$type": "dimension",
        "$value": "{{space.6}}",
        "$description": "Gap between things side by side, small."
      }
    }
  },
  "size": {
    "filter-bar": {
      "sort-width": {
        "$type": "dimension",
        "$value": "240px",
        "$description": "Width of the sort list in a wide filter bar, beside the search field and the filters button. A fixed frame width in Figma with no variable behind it; it should become one."
      }
    },
    "target": {
      "comfortable": {
        "$type": "dimension",
        "$value": "{{size.44}}",
        "$description": "Comfortable touch target (44)."
      }
    }
  },
  "color": {
    "text": {
      "subtle": {
        "$type": "color",
        "$value": "{{color.grey.700}}",
        "$description": "Secondary text: descriptions, metadata."
      }
    }
  },
  "typography": {
    "body-sm": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "14px",
        "fontWeight": 400,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      }
    }
  }
}
```

## Purpose

Search, filters, sort and Clear all together above a list or a table, with the filters applied shown as chips and the number of results they leave. It is a search landmark, so a screen reader can jump to it, and every control in it keeps its own Tab stop. It differs from a toolbar, which is one Tab stop moved through with the arrow keys and is for commands, and from a search field alone, which narrows a list by its words only.

## Anatomy

- The block: a search landmark with a name of its own. It has no fill or edge; its controls keep theirs. Up to three rows, the large stack space apart.
- `controls`: the first row. `search` holds the search field, which takes the room left over; `filters` holds the filters opener; `sort` holds the sort list at a fixed width; `view` holds an optional view switch at the end.
- `opener`: the filters opener, the outline button with a funnel and its words, and beside it, while filters are applied, `opener-count`, the small badge with how many.
- `narrow-filters`: an optional opener shown in place of the filters in a narrow space, such as one that opens a drawer holding the filters and the sort.
- `applied`: the second row, shown only while filters are applied: each filter as a small removable chip, then `clear`, a button drawn as the small link that removes them all. The chips wrap onto more rows.
- `count`: the last row, the number of results the filters leave, in Body SM; while results load, the spinner's ring and "Updating results" in its place.

The controls are the medium size, 40 tall.

## When to use

- Above a list, a table or a board that people narrow by words and by a few attributes, and sort.
- When the filters applied must stay in view, so nobody wonders why an item is missing.

## When not to use

- A single search box over a short list. Use the [search field](search-field.md) alone.
- Commands for a view, such as refresh or download. Use a [toolbar](toolbar.md); it can hold a search field and a filters opener too.
- Filters that take a page of their own, such as facets down the side of a large catalog. Lay those out on the page, with the chips of the filters applied above the results.
- Saying there are no results or that something failed. That belongs to the results, under the bar.

## Behavior

Ticking filters in the opener's menu applies them at once: each shows as a chip, the opener's badge counts them, and the count below says how many results are left. A chip's cross removes its filter; Clear all removes every one. The bar shows "Updating results" with the turning ring while the results load, and the count again when they arrive.

In a narrow space the search field and the filters opener stay and the sort list and the view switch go, since a narrow page reaches them where its filters open: a page can give a narrow opener of its own for a drawer that holds the filters and the sort. The applied chips stay in view and wrap.

The page holds the query, the filters applied and the count, and decides when the results change. Nothing moves.

## Accessibility

### What it does for you

- The bar is a search landmark with a name, given as its label.
- Tab goes through the search field, the filters opener, the sort list, the view switch, each chip's cross and Clear all, in that order.
- The opener says its count in words after its own words ("Filters, 2 applied"), so a voice command for "Filters" still finds it; the badge is hidden from screen readers.
- Each chip's cross is a real button named for its filter ("Remove Status: Open"). Removing one moves focus to the chip that takes its place, the next or else the one before; when the last goes, by its cross or by Clear all, focus goes back to the filters opener. A page that takes the filter away only once the results come back gets the same: focus waits on the cross until the chip goes.
- The count is a status message: a screen reader reads each new count, and "Updating results", without focus moving. It is in the page from the start, so the first count is not read out on arrival.
- The sort list is named for what it does ("Sort by"), so it is never taken for a filter.

### What you must do

- Name the bar with its label. A page with more than one names each differently.
- Name the search field and the sort list.

### How to test

- With the keyboard only, Tab through the bar: the order is the search field, the filters opener, the sort list, the view switch, each chip's cross, then Clear all.
- Remove a chip with Enter: focus moves to the chip that takes its place, and to the filters opener when the last goes.
- With a screen reader, the bar is found as a search landmark by its name, the opener is read with its count, and each cross is read with its filter.
- Change a filter: the new count is read out without focus moving.
- With text at 200% and the page 320 pixels wide, the search, the filters and the sort stay reachable.
- In a forced-color mode, the chips, crosses and focus rings are still visible.

### Keyboard

| Key | What it does |
|---|---|
| Tab or Shift + Tab | Moves focus along the bar: the search field, the filters opener, the sort list, the view switch, each chip's cross and Clear all. |
| Enter or Space | On the filters opener, a chip's cross or Clear all, presses it. |

## Usage rules

### Do

- Show every filter applied as a chip, and the number of results left.
- Name the bar, the search field and the sort list.
- Keep the filters and the sort where a narrow page can reach them.

### Don't

- Hide filters that are on behind the opener's count alone.
- Put the no-results message or an error inside the bar.
- Clear the filters when someone leaves the page and comes back, unless they asked for it.
