Person menu
ProThe signed-in person and what they can do about their account: a header with their initials, name and email; groups of choices (the theme, the app's own) each a radio group with the current one checked and none chosen for them; then Profile, Keyboard shortcuts, Help, the app's own entries and Sign out, each only when the app wired it. A person is drawn as a person: nothing here names an agent.
form="avatar" (the default) is the top bar's: the person's initials in a circle, on a button named "{initials}, {name}, account menu" ("PS, Priya Shah, account menu"), so its name starts with what it shows. form="row" is a sidebar's foot: the initial, the name, the role (or email) and a chevron, as AppSidebar draws it, whose foot is this menu. The menu opens through LayerPortal (DropdownMenu's), so it sits above the app's own dialogs, Radix's included, and focus goes back to the button when it closes.
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, 4.2.0 and 4.3.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/person-menu
Brings in: bidi, dropdown-menu, initials, link, locale, motion, theme-setting, tokens, utils · npm: lucide-react@^1.67.0
Usage
"use client"
import * as React from "react"
import { Download } from "lucide-react"
import { PersonMenu } from "@/components/candor/person-menu"
import type { ThemeChoice } from "@/components/candor/theme-setting"
const sam = { name: "Sam Rivera", email: "sam@acme.example", role: "Admin" }
// Each entry shows only when the app wires it. The avatar form is the top bar's; the row form is a sidebar's foot
// (AppSidebar's foot is this menu).
export default function Example() {
// The app keeps the theme and puts it on the page; the menu only shows and changes the choice.
const [theme, setTheme] = React.useState<ThemeChoice>("light")
return (
<div className="flex flex-wrap items-start gap-8">
<div className="flex h-14 w-80 items-center justify-end gap-2 rounded-lg border bg-chrome px-3">
<span className="me-auto text-xs text-muted-foreground">Top bar</span>
<PersonMenu
user={sam}
onProfile={() => {}}
onShortcuts={() => {}}
theme={{ value: theme, onValueChange: setTheme }}
help={{ href: "#help" }}
items={[{ id: "export", label: "Export my data", Icon: Download, onSelect: () => {} }]}
onSignOut={() => {}}
/>
</div>
<div className="flex h-40 w-60 flex-col justify-end rounded-lg border bg-sidebar p-2">
<PersonMenu user={sam} form="row" onProfile={() => {}} onSignOut={() => {}} />
</div>
</div>
)
}<PersonMenu>
| Prop | Type | Default | Description |
|---|---|---|---|
| userrequired | PersonMenuUser | — | The signed-in person. |
| onProfile | (() => void) | undefined | — | Adds Profile; called when it's chosen. |
| onShortcuts | (() => void) | undefined | — | Adds Keyboard shortcuts; called when it's chosen. |
| onSignOut | (() => void) | undefined | — | Adds Sign out, last; called when it's chosen. |
| help | PersonMenuHelp | undefined | — | Adds Help: a link (`href`, opened as a link) or your own handler (`onOpen`). |
| items | PersonMenuItem[] | undefined | — | The app's own entries, after Help and before Sign out, each drawn as text. |
| theme | PersonMenuTheme | undefined | — | Adds the person's theme as a group of choices (Light, Dark, High contrast), the current one checked. |
| choices | PersonMenuChoices[] | undefined | — | The app's own groups of choices, after the theme, each a radio group with the current one checked. |
| form | "avatar" | "row" | undefined | "avatar" | `avatar` (the default): initials in a circle, for a top bar. `row`: the initial, name and role, for a sidebar's foot. |
| compact | boolean | undefined | false | In the row form, only the initial shows (a collapsed sidebar); the name and role stay in the button's name. |
| side | "top" | "bottom" | "left" | "right" | "inline-start" | "inline-end" | undefined | — | Which side of the button the menu opens on. Defaults to "bottom" for the avatar and "top" for the row. |
| align | "start" | "center" | "end" | undefined | — | How the menu lines up with the button. Defaults to "end" for the avatar and "start" for the row. |
| labels | Partial<Record<keyof typeof personMenuStrings, string>> | undefined | — | Words for this menu over LocaleProvider's `personMenu`, for an item that keeps its own table (AppSidebar passes its `appSidebar` words, so overrides set there still apply). |
| wrapTrigger | ((trigger: React.ReactElement) => React.ReactNode) | undefined | — | Wraps the button, for an item that adds something round it (AppSidebar's tooltip while it is collapsed). It gets the button and returns what to draw. |
| popupClassName | string | undefined | — | Classes for the menu's popup, which is portalled; Candor's own layer class (`foundation/layer`'s `layerClass`) is always kept. |
| className | string | undefined | — | Classes for the button, merged with Candor's. |
Strings
The fixed words, in English by default. Change any of them under personMenu in <LocaleProvider strings={{ … }}>; the table is exported as personMenuStrings.
| Key | English |
|---|---|
| accountMenu | {name}, account menu |
| withInitials | {initials}, {label} |
| profile | Profile |
| keyboardShortcuts | Keyboard shortcuts |
| help | Help |
| signOut | Sign out |
Motion
| Moment | Family | What it does |
|---|---|---|
| tint | tint | The avatar button dims a little when the pointer is over it, and a ring fades in on keyboard focus. The menu opens as DropdownMenu's does. |
Set with --cd-motion-person-menu-<moment>-* in your brand file; the families are in the design reference.
Types
- PersonMenuUser
- The signed-in person: their name, their email, and their role when the app has one to show in place of the email.
{ /** As the person wrote it; its first letters make the avatar's initials. */ name: string; /** Shown under the name in the menu, and in the row form when there is no `role`. */ email: string; /** Shown in the row form in place of the email ("Admin"). */ role?: string | undefined; }- PersonMenuItem
- One of the app's own entries in the menu, drawn as text.
{ /** A key for the entry. */ id: string; /** The entry's words, drawn as text. */ label: string; /** An icon before the words. */ Icon?: LucideIcon | undefined; /** Called when the person chooses it. */ onSelect: () => void; }- PersonMenuChoices
- A group of choices in the menu, one of them current, drawn as a radio group under its label (a density, a language). Nothing is chosen for the person: with `value` null, none is checked.
{ /** A key for the group. */ id: string; /** The group's name, shown above its choices and read as the group's name. */ label: string; /** The current choice's value, as the app last stored it, or null when none is chosen. */ value: string | null; /** The choices, in order. */ options: { value: string; label: string; }[]; /** Called with the value the person chose. */ onValueChange: (value: string) => void; }- PersonMenuTheme
- The person's theme in the menu: ThemeSetting's choices (and its words, under `themeSetting`), as a radio group.
{ /** The person's theme as the app last stored it. Left out, none is checked. */ value?: ThemeChoice | undefined; /** Called with the theme the person chose; the app keeps it and puts it on the page, as for ThemeSetting. */ onValueChange: (next: ThemeChoice) => void; /** Which themes to offer, in order. Defaults to light, dark and high contrast; add "system" for "Match my system". */ options?: ThemeChoice[] | undefined; }- PersonMenuHelp
- Where people get help: a link, or the app's own handler. `label` defaults to "Help".
{ href?: string | undefined; onOpen?: (() => void) | undefined; label?: string | undefined }