Dropdown Menu — Free HTML CSS JS Snippet

Dropdown Menu · Navigation · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

scale(0.97) + opacity + translateY(-6px) open animation anchored at top left
transform-origin: top left — menu emerges from the trigger corner
Click-outside close via e.target.closest(".dropdown") on a document listener
ESC key close via keydown event for keyboard accessibility
pointer-events: none when closed — invisible menu does not intercept clicks
.divider class for horizontal separator between menu sections
.danger class for red destructive action items
8 lines of vanilla JavaScript for open/close/click-outside/keyboard
Export as HTML file, React JSX, or React + Tailwind CSS
Live split-pane editor — preview updates as you type

About this UI Snippet

Dropdown Menu — Scale-Opacity Animation, e.target.closest() & Keyboard Close

Screenshot of the Dropdown Menu snippet rendered live

A dropdown menu reveals a list of options from a trigger button — account menus, action menus (see the popover and right-click context menu variants), settings navigation, filter controls. It is one of the most common interactive patterns in web applications. The difference between a good and bad dropdown implementation comes down to three things: how it opens, how it closes, and whether it responds to keyboard input.

How the open animation works

The .menu starts in its closed state: opacity: 0, transform: translateY(-6px) scale(0.97), and pointer-events: none. The transform-origin: top left anchors the scale animation at the corner closest to the trigger button, so the menu appears to emerge from that point. When the parent .dropdown gains the .open class via JavaScript, a 0.15s CSS transition fires: opacity: 1, transform: translateY(0) scale(1). The 3% scale difference and 6px vertical shift are subtle but give the menu a polished "expanding" quality rather than just appearing.

The click-outside close pattern

A single document.addEventListener('click', e => { if (!e.target.closest('.dropdown')) { ... remove .open } }) handles closing on outside clicks. e.target.closest('.selector') traverses the DOM upward from the click target and returns the nearest ancestor matching the selector — or null if none exists. If the click was on the trigger or anywhere inside the dropdown container, closest returns the element and the menu stays open. If the click was outside, it returns null and the menu closes. This pattern works with any number of dropdowns on the page.

ESC key keyboard support

A keydown listener closes all open dropdowns when e.key === 'Escape'. This is required for basic keyboard accessibility — users who opened the menu with Enter should be able to close it with Escape.

Menu item variants

Menu items use .menu-item for standard items. .divider creates a 1px horizontal separator between sections. .danger changes the text to red and applies a red-tinted hover background — the standard convention for destructive actions like "Delete account" or "Revoke access" (confirm them with a confirm dialog).

Pointer-events: none when closed

The closed state uses pointer-events: none. Without this, the invisible menu would intercept clicks on elements behind it. The .open state removes this restriction.

The scale + opacity animation

The dropdown starts at transform: scale(0.97); opacity: 0; pointer-events: none. Adding .open switches to scale(1); opacity: 1; pointer-events: all with CSS transition. The scale(0.97) start point creates a subtle zoom-in that makes the dropdown feel like it "pops" from the trigger. The 0.97 value is small enough to be subtle but enough to create the appearance of depth — the dropdown is slightly smaller than its final size when it first appears.

The transform-origin

transform-origin: top right (for a right-aligned dropdown) or top left (for left-aligned) ensures the scale animation originates from the corner closest to the trigger button. Without a correct transform-origin, the dropdown scales from its centre, which looks disconnected from the trigger.

Click-outside detection

e.target.closest('.dropdown') returns the nearest .dropdown ancestor of the clicked element. If null, the click was outside all dropdowns and the close() function fires. This single document click listener handles all dropdowns on the page without per-dropdown listeners. Add document.addEventListener inside the open() function and remove inside close() to avoid accumulating listeners.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Rather than tracing the click-outside logic by hand, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how the single document-level click listener uses e.target.closest('.dropdown') to distinguish an inside click from an outside one, and why pointer-events: none on the closed .menu matters even though its opacity is already zero. The same assistant can help optimize it, for instance checking whether one shared document listener really scales cleanly to a page with dozens of independent dropdowns, or whether each needs its own scoped handling. It's also useful for extending the menu: ask it to add arrow-key navigation between menu items, support nested submenus that open on hover, or add the Escape-key close handler this version is missing. 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:

text
Build a dropdown menu in plain HTML, CSS, and JavaScript with no library.

Requirements:
- A trigger button that toggles an "open" class on its parent container when clicked, and a menu list that is absolutely positioned below the trigger.
- The closed menu state must use opacity 0, a combined translateY and scale transform (so it looks like it is shrinking toward the trigger corner, anchored with the correct transform-origin), and pointer-events none, transitioning to opacity 1 and the untransformed state when the open class is present.
- Implement closing on outside clicks with exactly one document-level click listener (not one per dropdown instance) that uses closest to check whether the click target is inside any dropdown container, and removes the open class from every open dropdown if not, so the same single listener correctly supports multiple independent dropdown instances on the same page.
- Add a document-level keydown listener that closes all open dropdowns when the Escape key is pressed, for basic keyboard accessibility.
- Support a visually distinct destructive menu item (e.g. "Sign out" or "Delete") with its own red-tinted hover state, and a thin horizontal divider element that can be placed between groups of menu items.
- Ensure the closed menu never intercepts clicks on whatever is positioned behind it, and that reopening the menu after scrolling the page still positions it correctly relative to the trigger.

Step by step

How to Use

  1. 1
    Click the trigger buttonClick "My Account" in the preview to open the dropdown. Click anywhere outside or press ESC to close. The menu uses a scale + opacity + translateY animation.
  2. 2
    Update the trigger label and menu itemsIn the HTML panel, change the trigger button text and each .menu-item label to your own actions.
  3. 3
    Add dividers between groupsAdd <li class="divider"></li> between groups of related actions in the menu list.
  4. 4
    Mark destructive actionsAdd the .danger class to any menu item that is destructive — Delete, Remove, Revoke. It renders in red with a red hover background.
  5. 5
    Adjust the menu positionThe menu has right: 0 by default (right-aligned). Change to left: 0 for left-aligned. Adjust top: calc(100% + 6px) to change the gap between trigger and menu.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a React + Tailwind version.

Real-world uses

Common Use Cases

Account and profile menus
Show Profile, Settings, Billing, and Sign out grouped under a user avatar or name. Use a divider to separate Sign out from the other items.
Table row action menus
Attach a three-dot trigger button to each table row. The dropdown reveals Edit, Duplicate, and Delete actions without consuming column space.
Navigation sub-menus
Reveal sub-pages under a top-level navigation link on click. The trigger becomes the nav link and the menu contains the child page links.
Learn e.target.closest() and event delegation
The click-outside pattern uses closest() to traverse the DOM upward. Edit the JS panel to understand how this pattern works with one listener for all dropdowns.
Filter and sort controls
Trigger a dropdown from a "Sort by" or "Filter" button in a data table or search results page. The menu contains the sort or filter options.
Contextual action menus
Use the dropdown as a context menu for files, cards, or list items. The .danger class marks the Delete action with red styling to communicate its destructive nature.

Got questions?

Frequently Asked Questions

A document.addEventListener("click") listener runs on every click. e.target.closest(".dropdown") walks up the DOM from the clicked element looking for a .dropdown ancestor. If found, the click was inside the dropdown and it stays open. If null, the click was outside and all dropdowns close via querySelectorAll(".dropdown").forEach(d => d.classList.remove("open")).

The .menu starts at transform: translateY(-6px) scale(0.97) with opacity: 0. When .open is added to the parent .dropdown, CSS transitions fire: transform returns to translateY(0) scale(1) and opacity to 1 over 0.15s. The transform-origin: top left anchors the scale at the trigger corner so the menu appears to expand from that point.

The hidden menu is still in the DOM with opacity: 0. Without pointer-events: none, it would intercept clicks on elements positioned behind it. pointer-events: none makes it completely transparent to mouse and touch events while invisible.

Add CSS: .dropdown:hover .menu { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; } and remove the onclick attribute from the trigger button. The CSS hover transition handles open/close without JavaScript.

Add an SVG element before the text inside the .menu-item anchor or button. The flex layout and gap on .menu-item will align the icon and text automatically.

Yes. Click "JSX" to download a React component. In React, manage the open state with useState. Replace the onclick handler with setState, and add a useEffect with a document click listener that checks e.target.closest() — clean up the listener on unmount. The CSS animations work unchanged.