Layout
Resource Switcher
A button that opens a list of projects, workspaces or teams with a search field.
Layout
A button that opens a list of projects, workspaces or teams with a search field.
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}
/>
)
}TopBarSeparator in a Top Bar. Its trigger is a TopBarSegment.The switcher has one part. items gives the list, and actions gives the commands below the list.
import { ResourceSwitcher } from 'ferry-ui'
<ResourceSwitcher items={items} value={value} onValueChange={setValue} actions={actions} />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.
<ResourceSwitcher items={projects} defaultValue="billing-portal" />actions adds commands below the list, for example "New project". The search does not hide them. heading adds a heading above the items.
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}
/>
)
}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.
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" />
}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.
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 />
</>
)
}The children replace the text of the trigger. icon replaces its icon. Long text stops at 180px.
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>
)
}An item with href is a link. The link component of the app renders it. An action accepts href too.
<ResourceSwitcher
items={projects.map((project) => ({ ...project, href: `/projects/${project.id}` }))}
value={projectId}
actions={[{ id: 'new', label: 'New project', icon: <Plus />, href: '/projects/new' }]}
/>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.
<ResourceSwitcher
items={results}
value={projectId}
onValueChange={setProjectId}
shouldFilter={false}
onSearchChange={setQuery}
loading={pending}
>
{currentProject.name}
</ResourceSwitcher>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.ResourceSwitcher accepts only the props of this table.
| Prop | Type | Default |
|---|---|---|
itemsRequired | 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 | ||
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 | ||
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…"). | ||
emptyTextDeprecated | ReactNode | - |
| See the element or the Radix UI primitive this part renders. | ||
loadingTextDeprecated | ReactNode | - |
| See the element or the Radix UI primitive this part renders. | ||
shouldFilter | boolean | - |
Set | ||
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 | ||
icon | ReactNode | - |
Leading icon of the trigger. Defaults to the current item's icon; pass | ||
open | boolean | - |
Controlled open state of the popover (pair with | ||
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 | ||
className | string | - |
Classes for the trigger. | ||
contentClassName | string | - |
Classes for the popover panel (default 288px wide, no padding). | ||
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. |
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. |