User Menu React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React user menu: profile header, theme switch, account links, other accounts and sign out, as a dropdown or a bottom sheet on phones.
Click the avatar (or the card in a sidebar footer, or the compact avatar and first name) and the menu grows out of it: a profile header with the plan badge and a Manage account button, a Light / Dark / System row whose raised thumb slides to the pick, your links, the other signed-in accounts and Sign out. Picking a theme keeps the menu open. Picking another account spins its row until the switch finishes, then the menu closes; Sign out spins and stays open until your promise settles (a rejection only stops the spinner). Keyboard: Enter, Space or Down on the trigger opens it on the first row, Up / Down move through every row including the three theme options, Right opens a submenu (Language), Left closes it, typing a letter jumps to a row, item shortcuts (Mod+,) run while it is open, Escape closes and focus returns to the trigger. A shortcut prop opens it from anywhere. In a collapsed sidebar the card shrinks to the avatar. On a phone the menu is a bottom sheet: a centered profile card, the theme as a full-width segmented control, 48px rows, submenus that push in from the right with a Back row, and a handle to drag it away.
User Menu is a Pro React component in the React navigation components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add user-menu, together with Avatar, Badge, Button, Drawer, Dropdown Menu and 8 more and the npm packages lucide-react and motion.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add user-menuUser Menu is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import UserMenu from @/components/ui/user-menu.
"use client"
import { CreditCard, Settings, User } from "lucide-react"
import { useTheme } from "next-themes"
import { UserMenu, type UserMenuTheme } from "@/components/ui/user-menu"
export function AccountMenu({ onSignOut }: { onSignOut: () => Promise<void> }) {
const { theme, setTheme } = useTheme()
return (
<UserMenu
user={{ name: "Emma Carter", email: "emma.carter@northwind.com", avatar: "/avatars/emma.jpg", plan: "Pro", status: "online" }}
theme={theme as UserMenuTheme}
onThemeChange={setTheme}
themeLabels={{ title: "Theme" }}
manageHref="/settings/profile"
manageLabel="Manage account"
items={[
{ label: "Profile", icon: <User />, href: "/settings/profile" },
{ label: "Settings", icon: <Settings />, shortcut: "mod+,", href: "/settings" },
{ label: "Billing", icon: <CreditCard />, badge: "Pro", href: "/settings/billing" },
]}
signOutLabel="Sign out"
onSignOut={onSignOut}
/>
)
}Props
userdefault"{ name: "Emma Carter", email: "emma.carter@northwind.com" }"UserMenuUserThe signed-in person: { id?, name, email?, avatar?, status?: "online" | "offline" | "busy" | "away", plan?: ReactNode, role?: ReactNode }. Without a photo the avatar shows initials on a color from the name; long names truncate and keep the full name in a tooltip.
itemsdefault"[]"MenuEntry[]Rows between the header and the accounts, in dropdown-menu's entry format: items ({ label, icon, description, shortcut, badge, href, onSelect, tone, disabled, keepOpen }), submenus, checkboxes, radio sets, groups, labels and separators. href items render through NavLink (the app's router link).
triggerdefault"avatar""avatar" | "card" | "compact" | ReactNodeavatar: an avatar button for top bars. card: avatar, name, email and an up-down chevron filling the width, for sidebar footers; inside a collapsed sidebar (data-collapsed="true" on a group/sidebar parent) only the avatar stays. compact: avatar, first name and a chevron. Any element works too (it gets the trigger props). For custom compositions use UserMenuTrigger (see Parts).
sizedefault"md""sm" | "md"The trigger avatar: 32 / 40px (card 28 / 32px, compact 24 / 32px). Every size has a 44px hit area on touch.
themedefault"system""light" | "dark" | "system"The current theme. Pass next-themes' theme here; left out, the row starts on system and keeps its own state.
onThemeChange(theme: UserMenuTheme) => voidShows the theme row (Light / Dark / System) and fires on every pick. Wire next-themes: const { theme, setTheme } = useTheme().
themeLabels{ title?: string; light?: string; dark?: string; system?: string }The theme row's heading and option labels; each one falls back to English: { title: "Theme", light: "Light", dark: "Dark", system: "System" }.
accountsUserMenuUser[]Other signed-in accounts. They are listed under the current one (which gets a check), each switchable.
onAccountChange(account: UserMenuUser) => (void | Promise<unknown>)Picking another account. Return a promise and its row spins while the menu stays open, then the menu closes.
onAddAccount() => voidShows an Add account row under the accounts.
addAccountLabeldefault"Add account"stringThe Add account row's label.
accountsLabeldefault"Accounts"stringThe heading above the accounts.
manageHrefdefault""stringShows a Manage account button under the profile header that links here (through the router link).
onManage() => voidShows the Manage account button and runs on press (with or without manageHref).
manageLabeldefault"Manage account"stringThe Manage account button's label.
onSignOut() => (void | Promise<unknown>)Shows the red Sign out row. Return a promise to keep the menu open with a spinner on the row until it settles; a rejection stops the spinner and leaves the menu open.
signOutLabeldefault"Sign out"stringThe sign-out row's label.
sidedefault"auto""auto" | "top" | "right" | "bottom" | "left"Where the menu opens. auto: below the trigger, or above it for the card trigger (sidebar footers). It flips when there is no room; on phones (below 768px, with responsive off) left and right fall back to auto.
aligndefault"auto""auto" | "start" | "center" | "end"Alignment against the trigger. auto: end (the avatar sits at the end of a top bar), start for the card trigger.
openbooleanControlled open state; pair it with onOpenChange.
defaultOpendefaultfalsebooleanStarts open (uncontrolled).
onOpenChange(open: boolean) => voidFires when the menu or the sheet opens or closes.
responsivedefaulttruebooleanBelow 768px the menu becomes the account sheet. Off: the dropdown on every screen.
sheetTitledefault"Account"stringThe phone sheet's title, read by screen readers (it is not shown).
backLabeldefault"Back"stringThe back row of a submenu page in the phone sheet.
shortcutdefault""stringA key combo in mod+k notation that opens and closes the menu from anywhere (e.g. shift+mod+u); the trigger gets aria-keyshortcuts. Empty binds nothing.
triggerLabeldefault"(name) => "Account menu, " + name"(name: string) => stringThe trigger's accessible name.
linkComponent(props: LinkComponentProps) => ReactNodeThe router link for href items and the manage link. Default: the one set with LinkProvider, else a plain <a>.
containerHTMLElement | nullPortal target for the menu and the sheet (default document.body).
classNamesPartial<Record<"trigger" | "avatar" | "name" | "email" | "chevron" | "content" | "header" | "headerAvatar" | "plan" | "manage" | "theme" | "item" | "accounts" | "addAccount" | "signOut", string>>Extra classes for the inner parts.
classNamestringExtra classes for the built-in trigger, merged last.
Parts
UserMenuTrigger
componentThe built-in trigger alone, for custom compositions: another menu, a popover or a link to the profile page. Takes every button prop plus the ones below.
userdefault"{ name: "Emma Carter", email: "emma.carter@northwind.com" }"UserMenuUserThe person shown: name, email, avatar, status, plan and role.
variantdefault"avatar""avatar" | "card" | "compact"An avatar button, a full-width card with name and email for sidebar footers, or avatar plus first name.
sizedefault"md""sm" | "md"The avatar at 32 / 40px (card 28 / 32px, compact 24 / 32px).
triggerLabeldefault"(name) => "Account menu, " + name"(name: string) => stringThe accessible name.
classNamesPartial<Record<"avatar" | "name" | "email" | "chevron", string>>Extra classes for the inner parts.
UserMenuUser
typeThe signed-in person or another account: { id?, name, email?, avatar?, status?, plan?, role? }. id defaults to the email, then the name.
UserMenuEntry
typeOne entry of items, the same type as the dropdown menu: an item (a link or an action, with icon, shortcut and badge), a checkbox, a radio group, a submenu, a group, a label or a separator.
Examples
Sidebar footer
The card trigger in a collapsed rail (avatar only, the menu opens to the right) and in an expanded sidebar.
Compact trigger in a top bar
Avatar and first name next to search and notifications; the theme row switches this page.
Several accounts
Switch between signed-in accounts with a 700ms spinner on the row, plus Add account.
Custom items, no theme row
Groups, a checkbox and an action; without onThemeChange the theme row stays out.
Phone: the account sheet
What the menu becomes below 768px: a profile card, a segmented theme control and sheet rows.
Small, localized, with a shortcut
size sm in a dense bar, custom labels and aria-label, opened with Shift+Mod+U.
Dependencies
Files
- components/ui/user-menu.tsx
FAQ
Is the React User Menu component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install User Menu?
Run npx wingo-ui@latest add user-menu. It also adds the Wingo UI items it builds on (Avatar, Badge, Button, Drawer, Dropdown Menu and 8 more) and the npm packages lucide-react and motion. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/user-menu.
How does User Menu behave on phones?
Below 768px the menu becomes the account sheet. Off: the dropdown on every screen. The responsive prop controls this and is on by default.