More Navigation Snippets
Profile Dropdown Menu — Account Menu HTML CSS JS
Profile Dropdown Menu · Navigation · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Profile Dropdown Menu — Avatar Account Menu with Keyboard Navigation

The avatar in the top-right corner that opens to reveal your account details, settings links, and sign-out is on virtually every app's navbar — and it's the kind of component developers rebuild constantly. This snippet provides a complete, polished profile dropdown in plain HTML, CSS, and vanilla JavaScript: a trigger showing the user's avatar and name, and a rich menu with account info, a plan badge, navigation links, and a distinct sign-out action, all keyboard accessible.
A trigger that shows who's signed in
The trigger isn't just an avatar — it pairs the user's initials-avatar with their name and a caret, so a glance confirms which account is active (important for anyone who switches between work and personal logins). The avatar uses a gradient background with initials, so no profile image is required, and the whole trigger is a pill that subtly highlights on hover and when open. The caret rotates to signal the menu's state.
A menu that's more than links
The dropdown opens with a header repeating the avatar plus the full name and email — the standard confirmation of the signed-in identity — followed by a plan badge ("Pro plan · Manage") that surfaces subscription status and a quick path to billing. Below that sit the navigation links (profile, settings, billing, help), each with an icon, and finally a visually separated, red-tinted "Sign out" — deliberately set apart so the destructive/exit action is never confused with a normal nav link. This information hierarchy (identity → status → navigation → exit) is what users expect from an account menu.
Keyboard accessible by design
The trigger carries aria-haspopup="menu" and a toggled aria-expanded, the menu is role="menu" with role="menuitem" entries, and the script wires the expected keyboard behavior: Escape closes the menu and returns focus to the trigger (so keyboard users aren't stranded), and Arrow Up/Down move focus through the menu items, wrapping at the ends. This is the accessibility most hand-built profile menus skip — a menu you can only operate with a mouse excludes keyboard and screen-reader users from a core navigation control.
Smooth, correctly-anchored animation
The menu animates in with opacity and transform (a slight rise and scale) from a top right transform origin, so it appears to grow out of the avatar in the corner rather than sliding in from nowhere. Using transform and opacity only keeps it smooth and crash-safe across every framework export. It's right-aligned to the trigger so it never overflows the viewport edge on a top-right navbar placement.
Outside-click dismissal and clean state
A document-level click listener closes the menu when you click anywhere outside it, and the trigger toggles open/closed on click — the standard, expected behavior. The component is self-contained: drop it into a navbar, swap the name, email, initials, plan, and links for your real user data, and wire the sign-out button to your auth logout. The structure stays identical whether the user is on a free or paid plan.
Build with AI
Build, Understand, Optimize, and Extend It With AI
You don't have to reverse-engineer every accessibility wire-up by hand. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to walk through exactly how the document-level click listener decides an outside click occurred using wrap.contains(e.target), or why the menu animates with opacity and transform from a top-right transform-origin instead of sliding in from the edge. The same assistant is useful for optimizing it too, for example checking whether attaching a new document click listener is necessary at all versus a single delegated listener, or whether the arrow-key handler's items query should be cached instead of rebuilt on every keydown. It's just as good for extending the menu: ask it to add a submenu for account switching, trap Tab focus inside the menu while open, or animate the caret and avatar together on hover. Treat the code less like a finished artifact and more like a starting point for a conversation.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a "profile dropdown menu" (the avatar-triggered account menu found in app navbars) in plain HTML, CSS, and JavaScript with no framework and no libraries.
Requirements:
- A trigger button showing an initials avatar, the user's name, and a caret icon, carrying aria-haspopup="menu" and an aria-expanded attribute that toggles between "true" and "false".
- A menu panel with role="menu" containing: a header repeating the avatar (larger) plus full name and email, a plan/status badge with a manage link, a list of navigation links each with role="menuitem", and a visually distinct sign-out control (different color, separated by a divider) as its own role="menuitem".
- The menu must be positioned absolutely relative to the trigger's wrapper, right-aligned so it never overflows a top-right navbar placement, and animate open/closed using only opacity and transform (translateY plus scale) from a top-right transform-origin — no width/height animation, no JavaScript-driven position calculation.
- Clicking the trigger must toggle the menu open state by adding/removing a class on the wrapper (which CSS then keys off to reveal the menu), not by directly toggling inline styles.
- A document-level click listener must close the menu whenever a click lands outside the wrapper element (checked via a contains() call), and a document-level keydown listener must close the menu on Escape and return keyboard focus to the trigger button.
- A keydown listener scoped to the menu must let ArrowDown and ArrowUp move focus between the menu's focusable link and button elements, wrapping around from the last item back to the first and vice versa.Step by step
How to Use
- 1Paste HTML, CSS, and JSA navbar renders with an avatar + name trigger in the top right. Click it to open the account menu.
- 2Explore the menuSee the header (avatar, name, email), a plan badge, navigation links with icons, and a separated sign-out.
- 3Use the keyboardOpen it, then Arrow Up/Down to move between items, and Escape to close and return focus to the trigger.
- 4Click outside to dismissClicking anywhere outside the menu closes it — the standard dropdown behavior.
- 5Add your user dataReplace the name, email, initials, plan, and links with your real signed-in user's details.
- 6Wire up sign-outConnect the "Sign out" button to your auth provider's logout, and point the links at your real routes.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Replace the static name, email, and initials with your user object's values (compute initials from the name, e.g. first letters of the first two words), set the plan badge from their subscription, and point the links at your routes. If you have a profile image, swap the initials avatar for an <img> with the initials div as a fallback when no image is set.
This snippet handles Escape-to-close-with-focus-return and Arrow key navigation. For complete menu semantics, also move focus to the first item when the menu opens, trap Tab within the menu while open, and ensure each item is reachable — the role="menu"/"menuitem" attributes are already in place so screen readers announce it correctly.
Attach a handler to the .pfd-signout button that calls your auth provider's logout (Auth0 logout(), Firebase signOut(), Supabase auth.signOut(), or your own endpoint), then redirect to the login or home page. Keep it as a button (not a link) since it triggers an action rather than navigating to a URL.
Sign-out is a state-changing, session-ending action, so visually grouping it with ordinary navigation links risks accidental clicks. Giving it a divider, distinct red color, and its own position at the bottom follows the established convention that makes the exit action deliberate and unmistakable.
In React, hold the open state in useState, render the user data from props, and handle outside-click with a useEffect document listener cleaned up on unmount; in Vue, use ref() with onMounted/onUnmounted; in Angular, use a component field with HostListener('document:click'). The keyboard and ARIA wiring port directly — only the open state and outside-click move into the framework's lifecycle.