# Filter Params

A URL-backed filter bar with searchable filter selection.

---

## Default

```tsx
'use client';

import {
  decodeDateRangeValue,
  encodeDateRangeValue,
  FilterParamsBar,
  type DateRangePreset,
  type DateRangeValue,
  type FilterParam,
} from '@vercel/geistcn/components/filter-params';
import type { JSX } from 'react';

const createdPresets: DateRangePreset[] = [
  {
    label: 'Last 7 Days',
    range: () => {
      const to = new Date();
      const from = new Date(to);
      from.setDate(to.getDate() - 7);
      return { from, to };
    },
  },
];

const filters: FilterParam<any>[] = [
  {
    key: 'Environment',
    type: 'select',
    label: (value) => value ?? 'All Environments',
    encode: (value) => ({ environment: value }),
    decode: (params) =>
      typeof params.environment === 'string' ? params.environment : undefined,
    options: ['Production', 'Preview', 'Development'],
  },
  {
    key: 'Branch',
    type: 'select',
    label: (value) => value ?? 'All Branches',
    encode: (value) => ({ branch: value }),
    decode: (params) =>
      typeof params.branch === 'string' ? params.branch : undefined,
    options: ['main', 'canary', 'feature/filter-bar'],
  },
  {
    key: 'Created',
    type: 'date-range',
    label: (value: DateRangeValue | undefined) =>
      value?.kind === 'preset' ? value.preset.label : 'Any Time',
    encode: (value: DateRangeValue | undefined) => ({
      created: encodeDateRangeValue(value),
    }),
    decode: (params) => decodeDateRangeValue(params.created, createdPresets),
    presets: createdPresets,
  },
];

export function FilterParamsDefault(): JSX.Element {
  return <FilterParamsBar params={filters} />;
}
```

## Suggestions

Provide a `suggestion` for filters that have a useful starting value. Suggestions
remain optional until users select them.

```tsx
'use client';

import {
  FilterParamsBar,
  type FilterParam,
} from '@vercel/geistcn/components/filter-params';
import type { JSX } from 'react';

const filters: FilterParam<any>[] = [
  {
    key: 'Environment',
    type: 'select',
    label: (value: string | undefined) => value ?? 'All Environments',
    encode: (value: string | undefined) => ({
      suggestedEnvironment: value,
    }),
    decode: (params) =>
      typeof params.suggestedEnvironment === 'string'
        ? params.suggestedEnvironment
        : undefined,
    options: ['Production', 'Preview', 'Development'],
    suggestion: () => 'Production',
  },
  {
    key: 'Status',
    type: 'multi-select',
    label: (value: string[] | undefined) =>
      value?.length ? `${value.length} Statuses` : 'All Statuses',
    optionLabel: (value) => value,
    encode: (value: string[] | undefined) => ({
      suggestedStatus: value?.join(','),
    }),
    decode: (params) => {
      const value = params.suggestedStatus;
      return typeof value === 'string' ? value.split(',') : undefined;
    },
    options: ['Ready', 'Building', 'Error'],
    suggestion: () => ['Error'],
  },
];

export function FilterParamsSuggestions(): JSX.Element {
  return <FilterParamsBar params={filters} />;
}
```

## AI

Pass an `ai` callback to offer natural-language filter suggestions. This example
simulates a request and always applies the Production environment filter.

```tsx
'use client';

import {
  FilterParamsBar,
  useSearchParamState,
  type FilterParam,
} from '@vercel/geistcn/components/filter-params';
import { useCallback, useRef, useState, type JSX } from 'react';

const filters: FilterParam<string>[] = [
  {
    key: 'Environment',
    type: 'select',
    label: (value) => value ?? 'All Environments',
    encode: (value) => ({ aiEnvironment: value }),
    decode: (params) =>
      typeof params.aiEnvironment === 'string'
        ? params.aiEnvironment
        : undefined,
    options: ['Production', 'Preview', 'Development'],
  },
];

export function FilterParamsAi(): JSX.Element {
  const [searchParams, setSearchParams] = useSearchParamState();
  const [isThinking, setIsThinking] = useState(false);
  const requestId = useRef(0);

  const onAbort = useCallback(() => {
    requestId.current += 1;
    setIsThinking(false);
  }, []);

  const onQuery = useCallback(async (): Promise<number | void> => {
    const id = ++requestId.current;
    setIsThinking(true);
    await new Promise((resolve) => setTimeout(resolve, 3000));

    if (id !== requestId.current) return;

    const nextSearchParams = new URLSearchParams(searchParams);
    nextSearchParams.set('aiEnvironment', 'Production');
    setSearchParams(nextSearchParams);
    setIsThinking(false);
    return 1;
  }, [searchParams, setSearchParams]);

  return (
    <FilterParamsBar ai={{ isThinking, onAbort, onQuery }} params={filters} />
  );
}
```

## Readonly filters

Use a readonly filter for page context that should be visible but cannot be
changed or removed. The page passes its value through `readonlyValues`, keyed by
the filter's `key`; it is never read from or written to the URL. A value can be
a promise, such as one passed down from a server component, and only that
filter's pill waits for it. Add a `note` to explain the context in a hover card.

```tsx
'use client';

import {
  FilterParamsBar,
  type FilterParam,
} from '@vercel/geistcn/components/filter-params';
import type { JSX } from 'react';

const filters: FilterParam<any>[] = [
  {
    key: 'Team',
    type: 'readonly',
    label: (value: string | undefined) => value ?? '',
    encode: (value: string | undefined) => ({ team: value }),
    decode: (params) =>
      typeof params.team === 'string' ? params.team : undefined,
    note: 'The current team is set by the page context and cannot be changed.',
  },
  {
    key: 'Environment',
    type: 'select',
    label: (value: string | undefined) => value ?? 'All Environments',
    encode: (value: string | undefined) => ({ environment: value }),
    decode: (params) =>
      typeof params.environment === 'string' ? params.environment : undefined,
    options: ['Production', 'Preview', 'Development'],
  },
];

export function FilterParamsReadonly(): JSX.Element {
  return <FilterParamsBar params={filters} readonlyValues={{ Team: 'Acme' }} />;
}
```

## Persistent filters

Set `persistent` on a filter that belongs to the page rather than to something
users opt into. Its pill is always in the bar and has no remove button.
Persistent filters render ahead of every other pill and suggestion, in the order
they are passed to `params`.

A persistent filter is still editable — open its pill to change the value — but
it cannot be turned off, so its options leave out the empty value: there is no
`All Environments` entry to clear it with. Give it a `default` so the pill
always reads as a real selection rather than an unfiltered label.

```tsx
'use client';

import {
  FilterParamsBar,
  type FilterParam,
} from '@vercel/geistcn/components/filter-params';
import type { JSX } from 'react';

const filters: FilterParam<any>[] = [
  {
    key: 'Environment',
    type: 'select',
    persistent: true,
    default: 'Production',
    label: (value: string | undefined) => value ?? 'All Environments',
    encode: (value: string | undefined) => ({ environment: value }),
    decode: (params) =>
      typeof params.environment === 'string'
        ? params.environment
        : 'Production',
    options: ['Production', 'Preview', 'Development'],
  },
  {
    key: 'Status',
    type: 'select',
    persistent: true,
    default: 'Ready',
    label: (value: string | undefined) => value ?? 'All Statuses',
    encode: (value: string | undefined) => ({ status: value }),
    decode: (params) =>
      typeof params.status === 'string' ? params.status : 'Ready',
    options: ['Ready', 'Building', 'Error'],
  },
  {
    key: 'Branch',
    type: 'select',
    label: (value: string | undefined) => value ?? 'All Branches',
    encode: (value: string | undefined) => ({ branch: value }),
    decode: (params) =>
      typeof params.branch === 'string' ? params.branch : undefined,
    options: ['main', 'canary', 'staging'],
  },
];

export function FilterParamsPersistent(): JSX.Element {
  return <FilterParamsBar params={filters} />;
}
```

## Best Practices

### When to use

* Use `FilterParamsBar` for list and search pages where the active constraints
  should be visible, linkable, and preserved on refresh.
* Keep filter names short and use option labels that explain the selected value
  without requiring users to reopen the menu.

### Behavior

* Encode only the URL parameters owned by each filter so removing a pill does
  not clear unrelated page state.
* Provide suggestions only when they are useful defaults, not as replacements
  for filters users have explicitly selected.

### Accessibility

* Make filter labels and option labels specific enough to identify the
  constraint and selected value when announced by a screen reader.
