Skip to content
Tier

Scope breadcrumb

Pro

Where 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.

Markdown
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

With the Candor CLI

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

TSX · The example above
"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>

PropTypeDefaultDescription
levelsrequiredScopeLevel[]—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.
labelstring | undefined—The navigation's accessible name. "Scope" by default.
classNamestring | 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.

KeyEnglish
labelScope
change{value}, {kind}: change
chooseChoose {kind}
unsetNo {kind} chosen

Motion

MomentFamilyWhat it does
tinttintA 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; }