# Resource Switcher

A button that opens a list of projects, workspaces or teams with a search field.

```tsx
import * as React from 'react'
import { ResourceSwitcher, type ResourceSwitcherItem } from 'ferry-ui'
import { FolderKanban } from 'lucide-react'

const PROJECTS: ResourceSwitcherItem[] = [
  { id: 'billing-portal', label: 'Billing portal', icon: <FolderKanban /> },
  { id: 'customer-app', label: 'Customer app', icon: <FolderKanban /> },
  { id: 'status-page', label: 'Status page', icon: <FolderKanban /> },
  { id: 'data-export', label: 'Data export', icon: <FolderKanban /> },
]

export default function ResourceSwitcherHero() {
  const [project, setProject] = React.useState('billing-portal')

  return (
    <ResourceSwitcher
      label="switch project"
      searchPlaceholder="Find a project…"
      items={PROJECTS}
      value={project}
      onValueChange={setProject}
    />
  )
}
```

## Usage guidelines

- **For items of one kind.** Use it to go from one project, workspace or team to a different one.
- **For the trail of the top bar.** Put it after a `TopBarSeparator` in a [Top Bar](/docs/components/top-bar). Its trigger is a `TopBarSegment`.
- **Not a field of a form.** For a value in a form, use [Select](/docs/components/select).
- **Not a list of actions.** For a few actions, use [Dropdown Menu](/docs/components/dropdown-menu).

## Anatomy

The switcher has one part. `items` gives the list, and `actions` gives the commands below the list.

```tsx title="Anatomy"

<ResourceSwitcher items={items} value={value} onValueChange={setValue} actions={actions} />
```

## Examples

### Current item

The trigger shows the label and the icon of the current item. To control the current item, pass `value` and `onValueChange`. With `defaultValue`, the component holds the state.

```tsx
<ResourceSwitcher items={projects} defaultValue="billing-portal" />
```

### Actions

`actions` adds commands below the list, for example "New project". The search does not hide them. `heading` adds a heading above the items.

```tsx
import * as React from 'react'
import { ResourceSwitcher, toast, type ResourceSwitcherAction, type ResourceSwitcherItem } from 'ferry-ui'
import { FolderKanban, LayoutGrid, Plus } from 'lucide-react'

const PROJECTS: ResourceSwitcherItem[] = [
  { id: 'billing-portal', label: 'Billing portal', icon: <FolderKanban /> },
  { id: 'customer-app', label: 'Customer app', icon: <FolderKanban /> },
  { id: 'status-page', label: 'Status page', icon: <FolderKanban /> },
]

// A real app opens a page or a dialog from `onSelect`.
const ACTIONS: ResourceSwitcherAction[] = [
  { id: 'all', label: 'All projects', icon: <LayoutGrid />, onSelect: () => toast('The list of projects opens here') },
  { id: 'new', label: 'New project…', icon: <Plus />, onSelect: () => toast('The dialog for a new project opens here') },
]

export default function ResourceSwitcherActions() {
  const [project, setProject] = React.useState('billing-portal')

  return (
    <ResourceSwitcher
      label="switch project"
      heading="Projects"
      items={PROJECTS}
      value={project}
      onValueChange={setProject}
      actions={ACTIONS}
    />
  )
}
```

### Items with more content

An item accepts a `description` below its label and `meta` content on the right. `keywords` gives more words to the search. The user cannot select a `disabled` item.

```tsx
import { ResourceSwitcher, StatusDot, type ResourceSwitcherItem } from 'ferry-ui'
import { UsersRound } from 'lucide-react'

const TEAMS: ResourceSwitcherItem[] = [
  {
    id: 'design',
    label: 'Design',
    icon: <UsersRound />,
    description: '8 members, Pro plan',
    meta: <StatusDot tone="success" label="Active" />,
  },
  {
    id: 'engineering',
    label: 'Engineering',
    icon: <UsersRound />,
    description: '24 members, Pro plan',
    // The query "developers" finds this team.
    keywords: ['developers'],
    meta: <StatusDot tone="success" label="Active" />,
  },
  {
    id: 'growth',
    label: 'Growth',
    icon: <UsersRound />,
    description: '5 members, trial ends in 3 days',
    meta: <StatusDot tone="warning" label="Trial" />,
  },
  { id: 'support', label: 'Support', icon: <UsersRound />, description: 'Archived team', disabled: true },
]

export default function ResourceSwitcherRichItems() {
  // Uncontrolled: the component holds the current team.
  return <ResourceSwitcher label="switch team" heading="Teams" items={TEAMS} defaultValue="engineering" />
}
```

### States

While the items load, set `loading`. The trigger then shows a skeleton until the current item is in `items`. With no current item, the trigger shows `placeholder`. `disabled` blocks the trigger.

```tsx
import { ResourceSwitcher, type ResourceSwitcherItem } from 'ferry-ui'
import { FolderKanban } from 'lucide-react'

const PROJECTS: ResourceSwitcherItem[] = [
  { id: 'billing-portal', label: 'Billing portal', icon: <FolderKanban /> },
  { id: 'customer-app', label: 'Customer app', icon: <FolderKanban /> },
]

const NO_PROJECTS: ResourceSwitcherItem[] = []

export default function ResourceSwitcherStates() {
  return (
    <>
      {/* The items are not there yet: the trigger shows a skeleton. */}
      <ResourceSwitcher label="switch project" items={NO_PROJECTS} value="billing-portal" loading />
      {/* No current item: the trigger shows the placeholder. */}
      <ResourceSwitcher label="switch project" items={PROJECTS} placeholder="Select a project…" />
      <ResourceSwitcher label="switch project" items={PROJECTS} defaultValue="customer-app" disabled />
    </>
  )
}
```

### Custom trigger

The children replace the text of the trigger. `icon` replaces its icon. Long text stops at 180px.

```tsx
import * as React from 'react'
import { ResourceSwitcher, type ResourceSwitcherItem } from 'ferry-ui'
import { Building2 } from 'lucide-react'

const WORKSPACES: ResourceSwitcherItem[] = [
  { id: 'acme', label: 'Acme', icon: <Building2 /> },
  { id: 'acme-labs', label: 'Acme Labs', icon: <Building2 /> },
  { id: 'acme-europe', label: 'Acme Europe', icon: <Building2 /> },
]

export default function ResourceSwitcherCustomTrigger() {
  const [workspace, setWorkspace] = React.useState('acme-labs')
  const current = WORKSPACES.find((item) => item.id === workspace)

  return (
    <ResourceSwitcher
      label="switch workspace"
      items={WORKSPACES}
      value={workspace}
      onValueChange={setWorkspace}
      icon={
        <span className="flex size-5 items-center justify-center rounded-sm bg-primary-soft text-[10px] font-medium text-primary">
          {current?.label.charAt(0)}
        </span>
      }
    >
      {`Workspace: ${current?.label}`}
    </ResourceSwitcher>
  )
}
```

### Links

An item with `href` is a link. The link component of the app renders it. An action accepts `href` too.

```tsx
<ResourceSwitcher
  items={projects.map((project) => ({ ...project, href: `/projects/${project.id}` }))}
  value={projectId}
  actions={[{ id: 'new', label: 'New project', icon: <Plus />, href: '/projects/new' }]}
/>
```

### Results from a server

Set `shouldFilter` to `false`. Get the results in `onSearchChange`. If the results do not contain the current item, pass the text of the trigger as children.

```tsx
<ResourceSwitcher
  items={results}
  value={projectId}
  onValueChange={setProjectId}
  shouldFilter={false}
  onSearchChange={setQuery}
  loading={pending}
>
  {currentProject.name}
</ResourceSwitcher>
```

## Accessibility

- `label` tells what the trigger does, for example "switch project". The accessible name of the trigger is its visible text, then the label.
- `label` is also the accessible name of the popover.
- The focus moves to the search field when the popover opens. <Kbd>Enter</Kbd> selects the item. <Kbd>Esc</Kbd> closes the popover.

## API reference

`ResourceSwitcher` accepts only the props of this table.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `items` (required) | `ResourceSwitcherItem[]` |  | Entities to switch between. |
| `value` | `string` |  | Id of the current item (controlled). Marked with a check and shown in the trigger. |
| `defaultValue` | `string` |  | Initial current item when uncontrolled (the switcher then tracks selections itself). |
| `onValueChange` | `((value: string, item: ResourceSwitcherItem) => void)` |  | Called when an item is chosen, with its id and the item. For `href` items navigation happens through the link; use this for in-place switching or to observe the choice. |
| `actions` | `ResourceSwitcherAction[]` |  | Footer commands, shown under a separator and never filtered out by the search. |
| `heading` | `ReactNode` |  | Optional mono uppercase heading above the items ("Projects"). |
| `searchPlaceholder` | `string` | `Find…` | Placeholder of the search field (default "Find…"). |
| `placeholder` | `string` | `Select…` | Trigger text when no item is current (default "Select…"). |
| `loading` | `boolean` | `false` | Items are being fetched: the list shows `loadingMessage` instead of `emptyMessage`, and the trigger shows a skeleton while the current item is not in `items` yet (unless `children` is given). |
| `disabled` | `boolean` | `false` | Dims the trigger and prevents opening (e.g. while the user cannot switch). |
| `emptyMessage` | `ReactNode` |  | Message when nothing matches (default "Nothing found."). |
| `loadingMessage` | `ReactNode` |  | Message while loading with no match yet (default "Loading…"). |
| `emptyText` | `ReactNode` |  |  |
| `loadingText` | `ReactNode` |  |  |
| `shouldFilter` | `boolean` |  | Set `false` to filter yourself (server-side search) from `onSearchChange`. The trigger reads the current item from `items`: when your results may not contain it, pass the trigger text as `children` (and `icon`). |
| `onSearchChange` | `((query: string) => void)` |  | Called with the search query as the user types (and with "" when the popover reopens). |
| `label` | `string` |  | What the trigger does, e.g. "switch project". It is appended to the trigger's visible text to form its accessible name ("Web app, switch project"), so the current item is always announced; a label that already contains the visible text ("Project Web app, switch project") is used as is. Also names the popover. Without it the trigger is named by its visible text and the popover "Switcher". |
| `children` | `ReactNode` |  | Custom trigger text, truncated at 180px like any `TopBarSegment`. Defaults to the current item's label, or `placeholder` when no item is current. Put an icon, avatar or logo in `icon`, not here. |
| `icon` | `ReactNode` |  | Leading icon of the trigger. Defaults to the current item's icon; pass `null` to show none. |
| `open` | `boolean` |  | Controlled open state of the popover (pair with `onOpenChange`). |
| `defaultOpen` | `boolean` | `false` | Initial open state when uncontrolled. |
| `onOpenChange` | `((open: boolean) => void)` |  | Called with the next open state. |
| `align` | `"center" \| "start" \| "end"` | `start` | Alignment of the popover against the trigger (default "start"). |
| `linkComponent` | `LinkComponent` |  | Link component override for `href` items and actions. |
| `className` | `string` |  | Classes for the trigger. |
| `contentClassName` | `string` |  | Classes for the popover panel (default 288px wide, no padding). |

### ResourceSwitcherItem

One item of the list.

| Field | Type | Role |
| --- | --- | --- |
| `id` | `string` | A stable id. The component compares it with `value`. Required. |
| `label` | `string` | The name in the list and in the trigger. Required. |
| `icon` | `ReactNode` | An icon before the label. |
| `description` | `ReactNode` | A second line below the label. |
| `meta` | `ReactNode` | Content on the right, before the check mark. |
| `href` | `string` | The destination. The row is then a link. |
| `keywords` | `string[]` | More words that the search matches. |
| `disabled` | `boolean` | The item shows, but the user cannot select it. |

### ResourceSwitcherAction

One command below the list.

| Field | Type | Role |
| --- | --- | --- |
| `id` | `string` | A stable key. Required. |
| `label` | `string` | The visible text. Required. |
| `icon` | `ReactNode` | An icon before the label. |
| `href` | `string` | The destination. The row is then a link. |
| `onSelect` | `() => void` | Runs when the user selects the action. The popover closes first. |
| `disabled` | `boolean` | The action shows, but the user cannot select it. |
