The account menu for an app bar: workspace switch, usage, settings and sign out.
Import it and pass your content. The props that matter most are listed next.
"use client";
import * as React from "react";
import { UserMenu, type UserMenuTheme, type UserMenuWorkspace } from "@/components/ui/notchset/user-menu";
const workspaces: UserMenuWorkspace[] = [
{ id: "relay", name: "Relay", env: "PROD", slug: "relay", agents: "12 AGENTS" },
{ id: "relay-staging", name: "Relay", env: "STAGING", slug: "relay-staging" },
];
// The theme lives where ThemeSwitch and NotchsetThemeScript keep it: the dark class on <html> and
// localStorage "theme" (no entry means follow the system).
const KEY = "theme";
const listeners = new Set<() => void>();
function savedTheme(): UserMenuTheme {
try {
const t = localStorage.getItem(KEY);
return t === "dark" || t === "light" ? t : "system";
} catch {
return "system";
}
}
function applyTheme(t: UserMenuTheme) {
const dark = t === "dark" || (t === "system" && matchMedia("(prefers-color-scheme: dark)").matches);
document.documentElement.classList.toggle("dark", dark);
document.documentElement.style.colorScheme = dark ? "dark" : "light";
try {
if (t === "system") localStorage.removeItem(KEY);
else localStorage.setItem(KEY, t);
} catch {
// Blocked storage: the page still changes for this visit.
}
listeners.forEach((l) => l());
}
function subscribe(l: () => void) {
listeners.add(l);
window.addEventListener("storage", l);
return () => {
listeners.delete(l);
window.removeEventListener("storage", l);
};
}
export function AccountMenu({ openProfile, signOut }: { openProfile: () => void; signOut: () => Promise<void> }) {
const [workspace, setWorkspace] = React.useState("relay");
const theme = React.useSyncExternalStore(subscribe, savedTheme, () => "system" as const);
return (
<UserMenu
user={{ name: "Maya Kerr", initials: "MK", role: "OWNER · 2FA ON" }}
workspaces={workspaces}
workspace={workspace}
onWorkspaceChange={setWorkspace}
links={[{ id: "profile", label: "PROFILE", hint: "⌘P", onSelect: openProfile }]}
theme={theme}
onThemeChange={applyTheme}
onSignOut={signOut}
/>
);
}user / session–
{ name, initials, role? } / { device, expires }The identity cells.
workspaces / workspace / onWorkspaceChange–
{ id, name, env, slug, agents? }[] / string / fnThe switch; also shown on the trigger.
usage–
{ daily, used, cap, days?, marker?, title?, axis? }The lollipop plot of daily use.
cliCommand–
(slug) => stringThe CLI line with Copy.
links–
{ id, label, hint?, onSelect? }[]The link rows.
theme / onThemeChange–
"light" | "system" | "dark" / fnThe theme choice; the menu only reports it. Apply it the way Theme Switch does: the dark class on <html> and localStorage "theme" (see the example).
onSignOut–
() => PromiseSign out shows its scanner until this settles, then closes.
triggerdata-slot="user-menu-trigger"
The two-part key.
paneldata-slot="user-menu-content"
The menu.
components/ui/notchset/user-menu.tsx
pnpm dlx shadcn@latest add https://notchset.dev/r/user-menu.jsonpnpm dlx shadcn@latest add https://notchset.dev/r/radix-nova/user-menu.jsonFree, no account or key. Use the command for your project's primitives: a radix-*, new-york or default style in components.json is Radix, a base-* style is Base UI. With the @notchset registry added, @notchset/<name> picks for you.
pnpm add @base-ui/react@^1The CLI adds the styles to the CSS file named in components.json (tailwind.css). Check that it points at your global stylesheet, that the project uses Tailwind CSS v4, and that the file is imported in your root layout. Installed by hand: paste styles.css into that file.
Give your coding assistant the Markdown guide instead of screenshots.
https://notchset.dev/api/mcp (setup)v1.0.0 · 2026-10-02
First release