# Search and filter bar

Places a search input and filter controls in one row above a list or table. Controls wrap to fit the available width, and narrow bars can move the filters into a drawer.

```tsx
// demo.tsx
import {Search, SearchAndFilterBar, Stack, Text} from '@earendil-works/elanor-react';
import {useState} from 'react';

import {MemberFilter} from './member-filter.tsx';

const members = [
  {
    name: 'Alex Morgan',
    status: 'Active',
    role: 'Admin',
    group: 'Engineering',
    twoFactor: 'Enabled',
  },
  {name: 'Sam Rivera', status: 'Active', role: 'Member', group: 'Design', twoFactor: 'Enabled'},
  {
    name: 'Jordan Lee',
    status: 'Invited',
    role: 'Member',
    group: 'Engineering',
    twoFactor: 'Disabled',
  },
  {
    name: 'Taylor Chen',
    status: 'Active',
    role: 'Viewer',
    group: 'Operations',
    twoFactor: 'Disabled',
  },
  {
    name: 'Casey Patel',
    status: 'Suspended',
    role: 'Member',
    group: 'Operations',
    twoFactor: 'Enabled',
  },
  {name: 'Robin Ellis', status: 'Active', role: 'Admin', group: 'Design', twoFactor: 'Enabled'},
];

export default function SearchAndFilterBarDemo() {
  const [query, setQuery] = useState('');
  const [status, setStatus] = useState<string | null>(null);
  const [role, setRole] = useState<string | null>(null);
  const [group, setGroup] = useState<string | null>(null);
  const [twoFactor, setTwoFactor] = useState<string | null>(null);
  const matchingMembers = members.filter(
    (member) =>
      member.name.toLowerCase().includes(query.trim().toLowerCase()) &&
      (status === null || member.status === status) &&
      (role === null || member.role === role) &&
      (group === null || member.group === group) &&
      (twoFactor === null || member.twoFactor === twoFactor),
  );

  return (
    <Stack align="stretch">
      <SearchAndFilterBar.Root role="search" aria-label="Member search and filters">
        <SearchAndFilterBar.Search>
          <Search
            density="compact"
            aria-label="Search members"
            placeholder="Search members"
            value={query}
            onValueChange={setQuery}
          />
        </SearchAndFilterBar.Search>
        <SearchAndFilterBar.Filters>
          <MemberFilter
            label="Status"
            value={status}
            options={['Active', 'Invited', 'Suspended']}
            onChange={setStatus}
          />
          <MemberFilter
            label="Role"
            value={role}
            options={['Admin', 'Member', 'Viewer']}
            onChange={setRole}
          />
          <MemberFilter
            label="Group"
            value={group}
            options={['Engineering', 'Design', 'Operations']}
            onChange={setGroup}
          />
          <MemberFilter
            label="2FA"
            value={twoFactor}
            options={['Enabled', 'Disabled']}
            onChange={setTwoFactor}
          />
        </SearchAndFilterBar.Filters>
      </SearchAndFilterBar.Root>
      <Text role="status">
        {matchingMembers.length} of {members.length} members
      </Text>
      <ul
        aria-label="Matching members"
        style={{margin: 0, paddingInlineStart: 'var(--semantic-dimension-space-padding-x-large)'}}
      >
        {matchingMembers.map((member) => (
          <li key={member.name}>
            <Text render={<span />}>
              {member.name} - {member.role}
            </Text>
          </li>
        ))}
      </ul>
    </Stack>
  );
}
```

```tsx
// member-filter.tsx
import {Dropdown} from '@earendil-works/elanor-react';

export interface MemberFilterProps {
  readonly label: string;
  /** The selected option, or `null` for no filter. */
  readonly value: string | null;
  readonly options: readonly string[];
  readonly onChange: (value: string | null) => void;
}

export function MemberFilter({label, value, options, onChange}: MemberFilterProps) {
  return (
    <Dropdown.Root value={value} onValueChange={onChange}>
      <Dropdown.Control>
        <Dropdown.Trigger density="compact">
          <Dropdown.Prefix>{label}</Dropdown.Prefix>
          <Dropdown.Value placeholder="All" />
          <Dropdown.Icon />
        </Dropdown.Trigger>
        <Dropdown.Clear aria-label={`Clear ${label} filter`} />
      </Dropdown.Control>
      <Dropdown.Portal>
        <Dropdown.Positioner alignItemWithTrigger={false}>
          <Dropdown.Popup>
            <Dropdown.List>
              {options.map((option) => (
                <Dropdown.Item key={option} value={option}>
                  <Dropdown.ItemIndicator />
                  <Dropdown.ItemText>{option}</Dropdown.ItemText>
                </Dropdown.Item>
              ))}
            </Dropdown.List>
          </Dropdown.Popup>
        </Dropdown.Positioner>
      </Dropdown.Portal>
    </Dropdown.Root>
  );
}
```

## Usage

```tsx
import {Search, SearchAndFilterBar} from '@earendil-works/elanor-react';

<SearchAndFilterBar.Root role="search" aria-label="Member search and filters">
  <SearchAndFilterBar.Search>
    <Search density="compact" />
  </SearchAndFilterBar.Search>
  <SearchAndFilterBar.Filters>
    {/* Filter controls, such as Dropdowns */}
  </SearchAndFilterBar.Filters>
</SearchAndFilterBar.Root>;
```

The bar only arranges its contents; the application filters the data. Give Root `role="search"` and an `aria-label`, and announce the number of results in a status region.

Set `density="compact"` on Search, Dropdown triggers, SegmentedControl, and FiltersTrigger, so everything in the bar is 24px high. Touch targets keep the default height.

## Filter widths

Each filter is as wide as its content, up to half the filter area. Filters that don't fit wrap to the next row. Below 48rem a filter can take the full row; below 24rem each one does.

Put controls directly in Filters. A wrapper element counts as one filter.

```tsx
// content-width.tsx
import {Search, SearchAndFilterBar, SegmentedControl} from '@earendil-works/elanor-react';
import {useState} from 'react';

import {MemberFilter} from './member-filter.tsx';

const owners = ['alexandra.morgan-whitfield@example.com', 'sam.rivera@example.com'];

export default function SearchAndFilterBarContentWidth() {
  const [query, setQuery] = useState('');
  const [owner, setOwner] = useState<string | null>(owners[0] ?? null);
  const [source, setSource] = useState(['all']);

  return (
    <SearchAndFilterBar.Root role="search" aria-label="Component search and filters">
      <SearchAndFilterBar.Search>
        <Search
          density="compact"
          aria-label="Search components"
          placeholder="Search components"
          value={query}
          onValueChange={setQuery}
        />
      </SearchAndFilterBar.Search>
      <SearchAndFilterBar.Filters>
        <MemberFilter label="Owner" value={owner} options={owners} onChange={setOwner} />
        <SegmentedControl.Root
          density="compact"
          aria-label="Source"
          value={source}
          onValueChange={(next) => {
            if (next.length > 0) setSource(next);
          }}
        >
          <SegmentedControl.Item value="all">All</SegmentedControl.Item>
          <SegmentedControl.Item value="built-in">Built-in</SegmentedControl.Item>
          <SegmentedControl.Item value="custom">Custom</SegmentedControl.Item>
        </SegmentedControl.Root>
      </SearchAndFilterBar.Filters>
    </SearchAndFilterBar.Root>
  );
}
```

```tsx
// member-filter.tsx
import {Dropdown} from '@earendil-works/elanor-react';

export interface MemberFilterProps {
  readonly label: string;
  /** The selected option, or `null` for no filter. */
  readonly value: string | null;
  readonly options: readonly string[];
  readonly onChange: (value: string | null) => void;
}

export function MemberFilter({label, value, options, onChange}: MemberFilterProps) {
  return (
    <Dropdown.Root value={value} onValueChange={onChange}>
      <Dropdown.Control>
        <Dropdown.Trigger density="compact">
          <Dropdown.Prefix>{label}</Dropdown.Prefix>
          <Dropdown.Value placeholder="All" />
          <Dropdown.Icon />
        </Dropdown.Trigger>
        <Dropdown.Clear aria-label={`Clear ${label} filter`} />
      </Dropdown.Control>
      <Dropdown.Portal>
        <Dropdown.Positioner alignItemWithTrigger={false}>
          <Dropdown.Popup>
            <Dropdown.List>
              {options.map((option) => (
                <Dropdown.Item key={option} value={option}>
                  <Dropdown.ItemIndicator />
                  <Dropdown.ItemText>{option}</Dropdown.ItemText>
                </Dropdown.Item>
              ))}
            </Dropdown.List>
          </Dropdown.Popup>
        </Dropdown.Positioner>
      </Dropdown.Portal>
    </Dropdown.Root>
  );
}
```

For filters of one width on the page grid, set `layout="tracks"` on Filters. The search starts the bar and the filters end it. From 75.5rem the bar follows a twelve-track grid: the search spans four tracks and each filter two. Narrower bars keep each filter at its 11.75rem minimum, which fits values such as “Status: Pending” or “Role: Member +1”. A filter's width doesn't depend on its value, so the row doesn't move when a value or Clear appears; long values truncate.

```tsx
// tracks.tsx
import {
  Button,
  Drawer,
  ScrollArea,
  Search,
  SearchAndFilterBar,
  Stack,
} from '@earendil-works/elanor-react';
import {useState} from 'react';

import {MemberFilter} from './member-filter.tsx';

import styles from './tracks.module.css';

const filters = [
  {label: 'Status', options: ['Active', 'Invited', 'Suspended']},
  {label: 'Role', options: ['Administrator', 'Member', 'Viewer']},
  {label: 'Group', options: ['Engineering', 'Design', 'Operations']},
  {label: '2FA', options: ['Enabled', 'Disabled']},
];

function MembersBar({count}: {readonly count: number}) {
  const [query, setQuery] = useState('');
  const [values, setValues] = useState<ReadonlyArray<string | null>>(filters.map(() => null));

  return (
    <SearchAndFilterBar.Root
      role="search"
      aria-label={`Member search with ${count === 1 ? 'one filter' : `${count} filters`}`}
    >
      <SearchAndFilterBar.Search>
        <Search
          density="compact"
          aria-label="Search members"
          placeholder="Search members"
          value={query}
          onValueChange={setQuery}
        />
      </SearchAndFilterBar.Search>
      <SearchAndFilterBar.Filters layout="tracks">
        {filters.slice(0, count).map((filter, index) => (
          <MemberFilter
            key={filter.label}
            label={filter.label}
            value={values[index] ?? null}
            options={filter.options}
            onChange={(value) =>
              setValues((current) => current.map((item, at) => (at === index ? value : item)))
            }
          />
        ))}
      </SearchAndFilterBar.Filters>
      <SearchAndFilterBar.FiltersTrigger density="compact">
        Filters
      </SearchAndFilterBar.FiltersTrigger>
      <SearchAndFilterBar.FiltersDrawer>
        <Drawer.Title>Filters</Drawer.Title>
        <SearchAndFilterBar.FiltersSlot />
        <SearchAndFilterBar.FiltersActions>
          <Drawer.Close render={<Button />}>Show members</Drawer.Close>
        </SearchAndFilterBar.FiltersActions>
      </SearchAndFilterBar.FiltersDrawer>
    </SearchAndFilterBar.Root>
  );
}

export default function SearchAndFilterBarTracks() {
  return (
    <ScrollArea.Root>
      <ScrollArea.Viewport role="region" aria-label="Member filters at desktop width">
        <ScrollArea.Content className={styles.desktop}>
          <Stack align="stretch">
            {[1, 2, 3, 4].map((count) => (
              <MembersBar key={count} count={count} />
            ))}
          </Stack>
        </ScrollArea.Content>
      </ScrollArea.Viewport>
      <ScrollArea.Scrollbar orientation="horizontal">
        <ScrollArea.Thumb />
      </ScrollArea.Scrollbar>
    </ScrollArea.Root>
  );
}
```

```tsx
// member-filter.tsx
import {Dropdown} from '@earendil-works/elanor-react';

export interface MemberFilterProps {
  readonly label: string;
  /** The selected option, or `null` for no filter. */
  readonly value: string | null;
  readonly options: readonly string[];
  readonly onChange: (value: string | null) => void;
}

export function MemberFilter({label, value, options, onChange}: MemberFilterProps) {
  return (
    <Dropdown.Root value={value} onValueChange={onChange}>
      <Dropdown.Control>
        <Dropdown.Trigger density="compact">
          <Dropdown.Prefix>{label}</Dropdown.Prefix>
          <Dropdown.Value placeholder="All" />
          <Dropdown.Icon />
        </Dropdown.Trigger>
        <Dropdown.Clear aria-label={`Clear ${label} filter`} />
      </Dropdown.Control>
      <Dropdown.Portal>
        <Dropdown.Positioner alignItemWithTrigger={false}>
          <Dropdown.Popup>
            <Dropdown.List>
              {options.map((option) => (
                <Dropdown.Item key={option} value={option}>
                  <Dropdown.ItemIndicator />
                  <Dropdown.ItemText>{option}</Dropdown.ItemText>
                </Dropdown.Item>
              ))}
            </Dropdown.List>
          </Dropdown.Popup>
        </Dropdown.Positioner>
      </Dropdown.Portal>
    </Dropdown.Root>
  );
}
```

```css
/* tracks.module.css */
/* Demo only: bar widths, scrolled sideways where the page is narrower. */
/* stylelint-disable design-tokens/no-hardcoded-token-values -- Demo frame sizes. */
.desktop {
  inline-size: 77.5rem;
}

.oneRow {
  inline-size: 60rem;
}

.twoRows {
  inline-size: 46rem;
}

.drawer {
  inline-size: 30rem;
}
/* stylelint-enable design-tokens/no-hardcoded-token-values */
```

The layout depends on the bar's width and its number of filters:

- **One row** while the search keeps 16rem. The search takes the space the filters leave.
- **Two rows** while each filter keeps 11.75rem: the search, then the filters at equal widths. One filter never takes a row of its own.
- **The drawer** below that, with a FiltersTrigger. Without one, the last layout stays and the controls shrink.

There's never a third row. With three filters, as below, that's one row from 54.25rem, two rows from 37.25rem, and the drawer below. Each child of Filters counts as one filter, up to eight.

```tsx
// tracks-modes.tsx
import {
  Button,
  Drawer,
  ScrollArea,
  Search,
  SearchAndFilterBar,
  SegmentedControl,
  Stack,
} from '@earendil-works/elanor-react';
import {useState} from 'react';

import {MemberFilter} from './member-filter.tsx';

import styles from './tracks.module.css';

const widths = {
  'one row': styles.oneRow,
  'two rows': styles.twoRows,
  drawer: styles.drawer,
};
type Width = keyof typeof widths;

export default function SearchAndFilterBarTracksModes() {
  const [width, setWidth] = useState<Width>('one row');
  const [query, setQuery] = useState('');
  const [status, setStatus] = useState<string | null>(null);
  const [role, setRole] = useState<string | null>(null);
  const [group, setGroup] = useState<string | null>(null);
  const activeFilters = [status, role, group].filter((value) => value !== null).length;

  return (
    <Stack align="stretch">
      <SegmentedControl.Root
        density="compact"
        aria-label="Bar width"
        value={[width]}
        onValueChange={(next: Width[]) => {
          const [value] = next;
          if (value) setWidth(value);
        }}
      >
        <SegmentedControl.Item value="one row">One row</SegmentedControl.Item>
        <SegmentedControl.Item value="two rows">Two rows</SegmentedControl.Item>
        <SegmentedControl.Item value="drawer">Drawer</SegmentedControl.Item>
      </SegmentedControl.Root>
      <ScrollArea.Root>
        <ScrollArea.Viewport role="region" aria-label="Member filters at the chosen width">
          <ScrollArea.Content className={widths[width]}>
            <SearchAndFilterBar.Root role="search" aria-label="Member search and filters">
              <SearchAndFilterBar.Search>
                <Search
                  density="compact"
                  aria-label="Search members"
                  placeholder="Search members"
                  value={query}
                  onValueChange={setQuery}
                />
              </SearchAndFilterBar.Search>
              <SearchAndFilterBar.Filters layout="tracks">
                <MemberFilter
                  label="Status"
                  value={status}
                  options={['Active', 'Invited', 'Suspended']}
                  onChange={setStatus}
                />
                <MemberFilter
                  label="Role"
                  value={role}
                  options={['Administrator', 'Member', 'Viewer']}
                  onChange={setRole}
                />
                <MemberFilter
                  label="Group"
                  value={group}
                  options={['Engineering', 'Design', 'Operations']}
                  onChange={setGroup}
                />
              </SearchAndFilterBar.Filters>
              <SearchAndFilterBar.FiltersTrigger density="compact">
                {activeFilters ? `Filters (${activeFilters})` : 'Filters'}
              </SearchAndFilterBar.FiltersTrigger>
              <SearchAndFilterBar.FiltersDrawer>
                <Drawer.Title>Filters</Drawer.Title>
                <SearchAndFilterBar.FiltersSlot />
                <SearchAndFilterBar.FiltersActions>
                  <Drawer.Close render={<Button />}>Show members</Drawer.Close>
                </SearchAndFilterBar.FiltersActions>
              </SearchAndFilterBar.FiltersDrawer>
            </SearchAndFilterBar.Root>
          </ScrollArea.Content>
        </ScrollArea.Viewport>
        <ScrollArea.Scrollbar orientation="horizontal">
          <ScrollArea.Thumb />
        </ScrollArea.Scrollbar>
      </ScrollArea.Root>
    </Stack>
  );
}
```

```tsx
// member-filter.tsx
import {Dropdown} from '@earendil-works/elanor-react';

export interface MemberFilterProps {
  readonly label: string;
  /** The selected option, or `null` for no filter. */
  readonly value: string | null;
  readonly options: readonly string[];
  readonly onChange: (value: string | null) => void;
}

export function MemberFilter({label, value, options, onChange}: MemberFilterProps) {
  return (
    <Dropdown.Root value={value} onValueChange={onChange}>
      <Dropdown.Control>
        <Dropdown.Trigger density="compact">
          <Dropdown.Prefix>{label}</Dropdown.Prefix>
          <Dropdown.Value placeholder="All" />
          <Dropdown.Icon />
        </Dropdown.Trigger>
        <Dropdown.Clear aria-label={`Clear ${label} filter`} />
      </Dropdown.Control>
      <Dropdown.Portal>
        <Dropdown.Positioner alignItemWithTrigger={false}>
          <Dropdown.Popup>
            <Dropdown.List>
              {options.map((option) => (
                <Dropdown.Item key={option} value={option}>
                  <Dropdown.ItemIndicator />
                  <Dropdown.ItemText>{option}</Dropdown.ItemText>
                </Dropdown.Item>
              ))}
            </Dropdown.List>
          </Dropdown.Popup>
        </Dropdown.Positioner>
      </Dropdown.Portal>
    </Dropdown.Root>
  );
}
```

```css
/* tracks.module.css */
/* Demo only: bar widths, scrolled sideways where the page is narrower. */
/* stylelint-disable design-tokens/no-hardcoded-token-values -- Demo frame sizes. */
.desktop {
  inline-size: 77.5rem;
}

.oneRow {
  inline-size: 60rem;
}

.twoRows {
  inline-size: 46rem;
}

.drawer {
  inline-size: 30rem;
}
/* stylelint-enable design-tokens/no-hardcoded-token-values */
```

## Narrow bars

Add a FiltersTrigger and a FiltersDrawer, and bars narrower than 24rem keep the search and replace the filters with a button that opens them in a [Drawer](/components/drawer.md). Without them, the filters stack in one column. With tracks, the drawer comes after two rows, as above. Below 18rem, such as on a phone at 200% zoom, the button goes under the search. A container query switches the layout, so it fits the bar's own width, including narrow columns on wide screens. The drawer closes when the bar grows past it, and focus moves to the inline filters.

```tsx
<SearchAndFilterBar.FiltersTrigger density="compact">Filters (2)</SearchAndFilterBar.FiltersTrigger>
<SearchAndFilterBar.FiltersDrawer>
  <Drawer.Title>Filters</Drawer.Title>
  <SearchAndFilterBar.FiltersSlot />
  <SearchAndFilterBar.FiltersStatus>12 members</SearchAndFilterBar.FiltersStatus>
  <SearchAndFilterBar.FiltersActions>
    <Button hierarchy="secondary" focusableWhenDisabled>Clear all</Button>
    <Drawer.Close render={<Button />}>Show 12 members</Drawer.Close>
  </SearchAndFilterBar.FiltersActions>
</SearchAndFilterBar.FiltersDrawer>
```

While the drawer is open, the controls in Filters render in its FiltersSlot instead, so they mount again each time; control their values from the application. The page behind the drawer is inert, so FiltersStatus announces the results instead of the page's status region.

FiltersTrigger also works without a bar, inside any Drawer.Root. With `density="compact"` it's 24px high, where a compact Button is 20px.

FiltersActions stacks the buttons at full width. Put the primary action last, nearest the thumb, so the visual order matches the focus order. Keep Clear all focusable while disabled, so focus stays on it after clearing.

```tsx
// narrow.tsx
import {
  Button,
  Drawer,
  Search,
  SearchAndFilterBar,
  Stack,
  Text,
} from '@earendil-works/elanor-react';
import {useState} from 'react';

import {MemberFilter} from './member-filter.tsx';

import styles from './narrow.module.css';

const members = [
  {name: 'Alex Morgan', status: 'Active', role: 'Admin'},
  {name: 'Sam Rivera', status: 'Active', role: 'Member'},
  {name: 'Jordan Lee', status: 'Invited', role: 'Member'},
  {name: 'Taylor Chen', status: 'Active', role: 'Viewer'},
  {name: 'Casey Patel', status: 'Suspended', role: 'Member'},
];

export default function SearchAndFilterBarNarrow() {
  const [query, setQuery] = useState('');
  const [status, setStatus] = useState<string | null>(null);
  const [role, setRole] = useState<string | null>(null);
  const matching = members.filter(
    (member) =>
      member.name.toLowerCase().includes(query.trim().toLowerCase()) &&
      (status === null || member.status === status) &&
      (role === null || member.role === role),
  );
  const activeFilters = [status, role].filter((value) => value !== null).length;
  const results = `${matching.length} ${matching.length === 1 ? 'member' : 'members'}`;

  function clearFilters() {
    setStatus(null);
    setRole(null);
  }

  return (
    <Stack align="stretch" className={styles.frame}>
      <SearchAndFilterBar.Root role="search" aria-label="Member search and filters">
        <SearchAndFilterBar.Search>
          <Search
            density="compact"
            aria-label="Search members"
            placeholder="Search members"
            value={query}
            onValueChange={setQuery}
          />
        </SearchAndFilterBar.Search>
        <SearchAndFilterBar.Filters>
          <MemberFilter
            label="Status"
            value={status}
            options={['Active', 'Invited', 'Suspended']}
            onChange={setStatus}
          />
          <MemberFilter
            label="Role"
            value={role}
            options={['Admin', 'Member', 'Viewer']}
            onChange={setRole}
          />
        </SearchAndFilterBar.Filters>
        <SearchAndFilterBar.FiltersTrigger density="compact">
          {activeFilters ? `Filters (${activeFilters})` : 'Filters'}
        </SearchAndFilterBar.FiltersTrigger>
        <SearchAndFilterBar.FiltersDrawer>
          <Drawer.Title>Filters</Drawer.Title>
          <SearchAndFilterBar.FiltersSlot />
          <SearchAndFilterBar.FiltersStatus>{results}</SearchAndFilterBar.FiltersStatus>
          <SearchAndFilterBar.FiltersActions>
            <Button
              hierarchy="secondary"
              focusableWhenDisabled
              disabled={activeFilters === 0}
              onClick={clearFilters}
            >
              Clear all
            </Button>
            <Drawer.Close render={<Button />}>Show {results}</Drawer.Close>
          </SearchAndFilterBar.FiltersActions>
        </SearchAndFilterBar.FiltersDrawer>
      </SearchAndFilterBar.Root>
      <Text role="status">
        {matching.length} of {members.length} members
      </Text>
    </Stack>
  );
}
```

```tsx
// member-filter.tsx
import {Dropdown} from '@earendil-works/elanor-react';

export interface MemberFilterProps {
  readonly label: string;
  /** The selected option, or `null` for no filter. */
  readonly value: string | null;
  readonly options: readonly string[];
  readonly onChange: (value: string | null) => void;
}

export function MemberFilter({label, value, options, onChange}: MemberFilterProps) {
  return (
    <Dropdown.Root value={value} onValueChange={onChange}>
      <Dropdown.Control>
        <Dropdown.Trigger density="compact">
          <Dropdown.Prefix>{label}</Dropdown.Prefix>
          <Dropdown.Value placeholder="All" />
          <Dropdown.Icon />
        </Dropdown.Trigger>
        <Dropdown.Clear aria-label={`Clear ${label} filter`} />
      </Dropdown.Control>
      <Dropdown.Portal>
        <Dropdown.Positioner alignItemWithTrigger={false}>
          <Dropdown.Popup>
            <Dropdown.List>
              {options.map((option) => (
                <Dropdown.Item key={option} value={option}>
                  <Dropdown.ItemIndicator />
                  <Dropdown.ItemText>{option}</Dropdown.ItemText>
                </Dropdown.Item>
              ))}
            </Dropdown.List>
          </Dropdown.Popup>
        </Dropdown.Positioner>
      </Dropdown.Portal>
    </Dropdown.Root>
  );
}
```

```css
/* narrow.module.css */
/* Demo only: a phone-sized column, so the bar switches to its narrow layout on any screen. */
.frame {
  /* stylelint-disable-next-line design-tokens/no-hardcoded-token-values -- Demo frame size. */
  max-inline-size: 22rem;
}
```

## API

### `SearchAndFilterBar.Root`

Places a search input and filter controls in one row. Give it `role="search"` and an `aria-label`.

Renders `<div>`. Accepts its HTML attributes and `ref`.

### `SearchAndFilterBar.Search`

Holds the search input, usually a Search.

Renders `<div>`. Accepts its HTML attributes and `ref`.

### `SearchAndFilterBar.Filters`

Holds the filter controls, such as Dropdowns. While the filters drawer is open, they render in its FiltersSlot instead, so control their values from the application.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `layout` | `'content' \| 'tracks'` | `'content'` | `content` sizes each filter to its content, up to half the filters' width. `tracks` gives up to eight filters one width, at least 11.75rem, at the bar's end: one row, then two rows, then the filters drawer. |

Renders `<div>`. Also accepts its HTML attributes and `ref`.

### `SearchAndFilterBar.FiltersTrigger`

Opens the filters drawer. Bars narrower than 24rem with a FiltersTrigger show it in place of the inline filters. Outside a bar, it opens the drawer of the Drawer.Root around it, such as a page's own filters drawer.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `density` | `'compact' \| 'default'` | `'default'` | `compact` makes the button as tall as compact controls, but keeps the touch target height. |

Renders `<button>`. Also accepts its HTML attributes and `ref`.

### `SearchAndFilterBar.FiltersDrawer`

The filters drawer. Compose a Drawer.Title, a FiltersSlot, and FiltersActions inside.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | - |  |

Also accepts Base UI [`Drawer.Popup`](https://base-ui.com/react/components/drawer.md) props.

### `SearchAndFilterBar.FiltersSlot`

Where the Filters render inside the filters drawer.

Renders `<div>`. Accepts its HTML attributes and `ref`.

### `SearchAndFilterBar.FiltersStatus`

Announces the results while the filters drawer is open, such as “12 members”. The page behind the drawer is inert, so its own status region is not announced.

Renders `<div>`. Accepts its HTML attributes and `ref`.

### `SearchAndFilterBar.FiltersActions`

Stacks the drawer's buttons at full width. Put the primary action last, nearest the thumb.

Renders `<div>`. Accepts its HTML attributes and `ref`.
