ComponentsCSS
Account Menu
The signed-in person's avatar as a menu button: name and email on top, then profile, settings and sign out.
Open the menu from the avatar.
Installation
npx shadcn@latest add https://ui-collections.vercel.app/r/tide/account-menu.jsonAlso installs the theme, cn() and avatar, dropdown-menu. Packages: clsx, tailwind-merge, radix-ui.
Usage
account-menu-demo.tsx
"use client";
import { useState } from "react";
import { AccountMenu } from "@/registry/kit/ui/account-menu";
const icon = (d: string) => (
<svg viewBox="0 0 24 24" className="size-4" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
<path d={d} />
</svg>
);
export default function AccountMenuDemo() {
// In an app, pass `href` (and `linkAs` for your router) instead of onSelect.
const [last, setLast] = useState<string | null>(null);
const pick = (label: string) => () => setLast(label);
return (
<div className="flex flex-col items-center gap-6">
<AccountMenu
name="Ada Kaya"
email="ada@lumen.example"
items={[
{ label: "Your profile", icon: icon("M20 21a8 8 0 0 0-16 0M12 13a5 5 0 1 0 0-10 5 5 0 0 0 0 10"), onSelect: pick("Your profile") },
{ label: "Settings", icon: icon("M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM19 12h2M3 12h2M12 3v2M12 19v2"), onSelect: pick("Settings") },
{ label: "Help center", icon: icon("M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18zM9.5 9a2.5 2.5 0 1 1 3.5 2.3c-.6.3-1 .9-1 1.6M12 17h.01"), onSelect: pick("Help center") },
{ label: "Sign out", icon: icon("M15 4h3a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-3M10 17l5-5-5-5M15 12H4"), separated: true, variant: "danger", onSelect: pick("Sign out") },
]}
/>
<p className="text-sm text-muted" aria-live="polite">
{last ? `Chose ${last}` : "Open the menu from the avatar."}
</p>
</div>
);
}API
name / emailstringShown at the top of the menu; initials come from the name.
avatarstringImage URL. Initials otherwise.
items{ label; href?; onSelect?; icon?; separated?; variant? }[]Menu entries. `separated` draws a divider above; `variant: "danger"` colours a destructive one such as Sign out.
linkAs= "a"
ElementTypeRouter link for item hrefs, e.g. Next's Link.
Source
components/ui/account-menu.tsx
"use client";
import type { ElementType, ReactNode } from "react";
import { cn } from "@/registry/kit/lib/utils";
import { Avatar } from "@/registry/kit/ui/avatar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/registry/kit/ui/dropdown-menu";
export interface AccountMenuItem {
label: string;
/** Navigates here; otherwise `onSelect` runs. */
href?: string;
onSelect?: () => void;
icon?: ReactNode;
/** Draws a divider above the item, e.g. before "Sign out". */
separated?: boolean;
variant?: "default" | "danger";
}
export interface AccountMenuProps {
name: string;
email?: string;
/** Image URL; initials otherwise. */
avatar?: string;
items: AccountMenuItem[];
/** Router link component for item hrefs, e.g. Next's Link. Defaults to `a`. */
linkAs?: ElementType;
/** Classes for the initials fallback, e.g. a tinted gradient. */
fallbackClassName?: string;
className?: string;
}
/**
* The signed-in person's avatar as a menu button: name and email at the top,
* then account links such as profile, settings and sign out.
*/
export function AccountMenu({
name,
email,
avatar,
items,
linkAs: Link = "a",
fallbackClassName = "bg-accent/15 font-semibold text-accent",
className,
}: AccountMenuProps) {
return (
<DropdownMenu>
<DropdownMenuTrigger
aria-label={`Account menu for ${name}`}
className={cn(
"cursor-pointer rounded-full outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
className,
)}
>
<Avatar src={avatar} alt="" fallback={initials(name)} fallbackClassName={fallbackClassName} />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-60">
<div className="px-3 py-2">
<p className="truncate text-sm font-medium text-fg">{name}</p>
{email && <p className="truncate text-xs text-muted">{email}</p>}
</div>
<DropdownMenuSeparator />
{items.map((item) => (
<MenuEntry key={item.label} item={item} Link={Link} />
))}
</DropdownMenuContent>
</DropdownMenu>
);
}
function MenuEntry({ item, Link }: { item: AccountMenuItem; Link: ElementType }) {
return (
<>
{item.separated && <DropdownMenuSeparator />}
{item.href ? (
<DropdownMenuItem asChild variant={item.variant}>
<Link href={item.href}>
{item.icon}
{item.label}
</Link>
</DropdownMenuItem>
) : (
<DropdownMenuItem variant={item.variant} onSelect={item.onSelect}>
{item.icon}
{item.label}
</DropdownMenuItem>
)}
</>
);
}
const initials = (name: string) =>
name
.split(/\s+/)
.map((part) => part[0])
.join("")
.slice(0, 2)
.toUpperCase();