Product
User menu
Sidebar account menu with links, theme selection, and sign-out action.
$ npx context-ds add user-menuUsage
Use it for
The sidebar footer.
Avoid
- Settings as a sidebar link (it lives here)
Dependencies: dropdown-menu, tooltip, theme-store, lucide-react
Example
examples/user-menu.tsx
"use client";import { useState } from "react";import { UserMenu } from "@/components/product/user-menu";import { Button } from "@/components/ui/button";import { ExampleMessage, ExampleNameDialog } from "./demo-feedback";export function Product() { const [name, setName] = useState("Ian Gerado"); const [settings, setSettings] = useState(false); const [loggedOut, setLoggedOut] = useState(false); return <div className="grid w-64 gap-4"> {loggedOut ? <><ExampleMessage>Signed out of this example.</ExampleMessage><Button variant="outline" onClick={() => setLoggedOut(false)}>Sign in again</Button></> : <UserMenu name={name} email="ian@acme.com" sections={[ [{ label: "Settings", onSelect: () => setSettings(true) }, { label: "Share & get rewards", href: "https://context.dev/referrals", external: true }, { label: "Changelog", href: "https://context.dev/changelog", external: true }], [{ label: "Terms & conditions", href: "https://context.dev/terms", external: true }, { label: "Privacy policy", href: "https://context.dev/privacy", external: true }], ]} onLogOut={() => setLoggedOut(true)} />} <ExampleNameDialog title="Profile settings" label="Full name" open={settings} onOpenChange={setSettings} initial={name} onSave={setName} /> </div>;}Source
components/product/user-menu.tsx
"use client";import * as React from "react";import { ChevronsUpDown, type LucideIcon, Monitor, Moon, Sun } from "lucide-react";import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";import { setTheme, type Theme, useTheme } from "@/lib/theme";import { cn } from "@/lib/utils";const THEMES: { value: Theme; label: string; icon: LucideIcon }[] = [ { value: "light", label: "Light", icon: Sun }, { value: "system", label: "System", icon: Monitor }, { value: "dark", label: "Dark", icon: Moon },];export type UserMenuLink = { label: string; href?: string; external?: boolean; onSelect?: () => void };export function initialsOf(name: string, email = "") { const words = (name.trim() || email).split(/[\s@._-]+/).filter(Boolean); return words .slice(0, 2) .map((w) => w[0]) .join("") .toUpperCase();}/** A round photo, or the initials on brand blue. */export function UserAvatar({ image, initials, size = "sm" }: { image?: string; initials: string; size?: "sm" | "md" }) { const sizeClass = size === "sm" ? "size-5 text-initial" : "size-8 text-axis"; if (image) return <img src={image} alt="" referrerPolicy="no-referrer" className={cn("shrink-0 rounded-pill object-cover", sizeClass)} />; return <span className={cn("grid shrink-0 place-items-center rounded-pill bg-brand text-on-brand", sizeClass)}>{initials}</span>;}// The account at the foot of the sidebar: avatar, name (or the email when there is no name) and the same up-down// chevrons as the workspace switcher, since it opens a menu and nothing about it moves. The menu opens upward: who you// are on a gray card, your links in groups, the theme switch (light, system, dark), then Log out in red.function UserMenu({ name, email = "", image, sections = [], onLogOut,}: { name: string; email?: string; image?: string; sections?: UserMenuLink[][]; onLogOut?: () => void;}) { const theme = useTheme(); const displayName = name.trim(); const hasName = displayName !== "" && displayName !== email; const shownName = hasName ? displayName : email || "User"; const initials = initialsOf(name, email); return ( <DropdownMenu> <DropdownMenuTrigger asChild> <button type="button" aria-label="Account menu" className="group flex h-9 w-full items-center gap-2.5 rounded-control px-3 text-body-sm text-fg outline-none transition-colors duration-150 hover:bg-wash-hover focus-visible:ring-2 focus-visible:ring-focus-ring data-[state=open]:bg-wash-hover" > <UserAvatar image={image} initials={initials} /> <span className="flex-1 truncate text-left">{shownName}</span> <ChevronsUpDown className="size-3.5 shrink-0 text-fg-subtle transition-colors duration-150 group-hover:text-fg-muted" strokeWidth={1.5} /> </button> </DropdownMenuTrigger> <DropdownMenuContent side="top" align="start" className="w-60 gap-0"> <div className="flex items-center gap-2.5 rounded-control bg-neutral-5 px-2.5 py-2"> <UserAvatar image={image} initials={initials} size="md" /> <div className="min-w-0"> <p className="truncate text-body-sm text-fg">{shownName}</p> {hasName && email && <p className="truncate text-caption text-fg-subtle">{email}</p>} </div> </div> {sections.map((section) => ( <React.Fragment key={section.map((item) => item.href ?? item.label).join("|")}> <DropdownMenuSeparator /> <DropdownMenuGroup className="flex flex-col gap-0.5"> {section.map((item) => ( <DropdownMenuItem key={item.label} onSelect={item.onSelect} asChild={!!item.href}> {item.href ? ( <a href={item.href} target={item.external ? "_blank" : undefined} rel={item.external ? "noopener noreferrer" : undefined}> {item.label} </a> ) : ( item.label )} </DropdownMenuItem> ))} </DropdownMenuGroup> </React.Fragment> ))} <DropdownMenuSeparator /> <div className="flex items-center justify-between py-0.5 pr-0.5 pl-2.5"> <span className="text-body-sm text-fg-muted">Theme</span> <div className="flex rounded-control border border-line p-0.5" role="radiogroup" aria-label="Theme"> {THEMES.map(({ value, label, icon: Icon }) => ( <Tooltip key={value}> <TooltipTrigger asChild> <button type="button" role="radio" aria-checked={theme === value} aria-label={label} onClick={() => setTheme(value)} className={cn( "grid size-6 place-items-center rounded-tag outline-none transition-colors duration-150 focus-visible:ring-2 focus-visible:ring-focus-ring", theme === value ? "bg-blue-10 text-fg-brand" : "text-fg-subtle hover:text-fg", )} > <Icon className="size-3.5" strokeWidth={1.5} /> </button> </TooltipTrigger> <TooltipContent side="top">{label}</TooltipContent> </Tooltip> ))} </div> </div> {onLogOut && ( <> <DropdownMenuSeparator /> <DropdownMenuItem variant="destructive" onSelect={onLogOut}> Log out </DropdownMenuItem> </> )} </DropdownMenuContent> </DropdownMenu> );}export { UserMenu };Reference: context-dashboard c0ddf7a src/components/shell/user-menu.tsx.