Scope breadcrumb
ProWhere the person is, level by level (client, then workspace, then project), each level a menu to move to another place at that level. A trigger shows the current value and its name starts with it ("Northwind, client: change"); a level with no value says "Choose client", never picking one for the person. The last level is marked as the current page.
It draws the levels it is given: when a higher level changes, the app clears or resets the deeper ones. Names are drawn as text in their own isolate, since they can be other tenants'. Its menus open through LayerPortal, so they sit above the app's own dialogs and sheets.
Version 5.2.0· other versions
This page is for 5.2.0, the current release. An earlier release’s page is the Markdown that release published, so the docs match the version you have installed (candor-lock.json records it).
Not in the docs of 1.3.0, 1.4.0, 1.5.0, 1.5.1, 1.6.0, 1.7.0, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 2.0.0, 2.1.0, 2.2.0, 2.3.0, 2.4.0, 2.5.0, 2.5.1, 2.6.0, 2.6.1, 2.6.2, 2.7.0, 2.8.0, 2.9.0, 2.10.0, 3.0.0, 4.0.0, 4.0.1, 4.0.2, 4.1.0 and 4.2.0: the page came later, or had another name then.
Docs pages weren’t kept for 1.0.0, 1.0.1, 1.1.0, 1.1.1, 1.2.0 and 1.2.1: they are kept from 1.3.0 on, and before that only llms.txt was published.
Preview
Loading the preview
Install
First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/scope-breadcrumb
Brings in: bidi, breadcrumb, dropdown-menu, link, locale, locked, tokens, utils · npm: lucide-react@^1.67.0
Usage
"use client"
import * as React from "react"
import { ScopeBreadcrumb, type ScopeLevel } from "@/components/candor/scope-breadcrumb"
import { TooltipProvider } from "@/components/candor/ui/tooltip"
const clients = [
{ id: "acme", label: "Acme" },
{ id: "globex", label: "Globex" },
]
const workspaces: Record<string, { id: string; label: string }[]> = {
acme: [
{ id: "acme-prod", label: "Production" },
{ id: "acme-sandbox", label: "Sandbox" },
],
globex: [{ id: "globex-prod", label: "Production" }],
}
export default function Example() {
const [client, setClient] = React.useState("acme")
const [workspace, setWorkspace] = React.useState<string | null>("acme-prod")
const options = Object.hasOwn(workspaces, client) ? workspaces[client]! : []
const levels: ScopeLevel[] = [
{ id: "client", kind: "client", value: clients.find((c) => c.id === client) ?? null, options: clients },
{ id: "workspace", kind: "workspace", value: options.find((w) => w.id === workspace) ?? null, options },
{ id: "region", kind: "region", value: { id: "eu", label: "EU West" }, locked: "Data stays in the region the workspace was created in." },
]
return (
<TooltipProvider>
<div className="rounded-xl border bg-card p-3">
<ScopeBreadcrumb
levels={levels}
onChange={(level, id) => {
if (level === "client") {
setClient(id)
// A new client clears the deeper level: the person chooses the workspace.
setWorkspace(null)
} else if (level === "workspace") setWorkspace(id)
}}
/>
</div>
</TooltipProvider>
)
}<ScopeBreadcrumb>
| Prop | Type | Default | Description |
|---|---|---|---|
| levelsrequired | ScopeLevel[] | — | The levels, outermost first. |
| onChange | ((levelId: string, optionId: string) => void) | undefined | — | Called with the level's id and the option's id when the person moves to another place at that level. An option with `href` is a link: a plain click (or Enter) calls this and also follows the link, so don't navigate here as well; a click that opens it in a new tab or window (a modifier key, the middle button) only opens the link, since this page's place doesn't change. |
| label | string | undefined | — | The navigation's accessible name. "Scope" by default. |
| className | string | undefined | — | Classes for the navigation, merged with Candor's. |
Strings
The fixed words, in English by default. Change any of them under scopeBreadcrumb in <LocaleProvider strings={{ … }}>; the table is exported as scopeBreadcrumbStrings.
| Key | English |
|---|---|
| label | Scope |
| change | {value}, {kind}: change |
| choose | Choose {kind} |
| unset | No {kind} chosen |
Motion
| Moment | Family | What it does |
|---|---|---|
| tint | tint | A level's trigger changes colour when the pointer is over it, and a ring fades in when it takes keyboard focus. Its menu opens as `DropdownMenu`'s does. |
Set with --cd-motion-scope-breadcrumb-<moment>-* in your brand file; the families are in the design reference.
Types
- ScopeOption
- One place a level can be set to: an id for `onChange`, its name, and a link to it when choosing it navigates.
{ /** Passed to `onChange` when it is chosen. */ id: string; /** Its name, drawn as text (it can be another tenant's). */ label: string; /** A link to the scope, when choosing it navigates (the item is then a link the browser can open in a new tab). */ href?: string | undefined; }- ScopeLevel
- One level of a `ScopeBreadcrumb`: a client, a workspace, a project, each the app's word.
{ /** Passed to `onChange` with the option chosen. */ id: string; /** The app's word for the level as it reads inside a sentence ("client", "workspace"), in the trigger's name and in "Choose client". */ kind: string; /** The scope the person is in at this level, or null when none is chosen yet. */ value: { id: string; label: string; } | null; /** What the level can be changed to. With options (or a lock) the level is a menu; without, plain text. */ options?: ScopeOption[] | undefined; /** Why this level can't be changed now. The trigger stays focusable, `aria-disabled`, and says why. `null` or left out: unlocked. */ locked?: string | null | undefined; }