Dropdown

PreviousNext

A composable dropdown with slot-based trigger content, outside-click handling, item-based closing, and open/close animations.

'use client';

import { Avatar } from '@/ui/Avatar';
import { Dropdown } from '@/ui/Dropdown';
import { Button } from '@/ui/buttons/Button';

export function DropdownDemo() {
  const user = { firstName: 'John', lastName: 'Doe' };

  return (
    <Dropdown aria-label="Open user actions">
      <span data-slot="label" className="hidden md:inline text-body">
        {user.firstName} {user.lastName}
      </span>
      <Avatar
        data-slot="label"
        alt={`${user.firstName} ${user.lastName}`}
        className="w-40 h-40 cursor-pointer hover:opacity-80 transition-opacity"
        style={{ width: '40px', height: '40px' }}
      />

      <a href="/account" className="text-b3 font-semibold">
        Account
      </a>
      <Button data-dropdown-item="">Logout</Button>
    </Dropdown>
  );
}

Usage

import { Dropdown } from '@/ui/Dropdown';
import { Avatar } from '@/ui/Avatar';
import { Button } from '@/ui/buttons/Button';
 
export function Component() {
  const user = { firstName: 'John', lastName: 'Doe' };
 
  return (
    <Dropdown aria-label="Open user actions">
      <span data-slot="label" className="hidden md:inline text-body">
        {user.firstName} {user.lastName}
      </span>
      <Avatar
        data-slot="label"
        alt={`${user.firstName} ${user.lastName}`}
        className="w-40 h-40 cursor-pointer hover:opacity-80 transition-opacity"
        style={{ width: '40px', height: '40px' }}
      />
 
      <a href="/account" className="text-b3 font-semibold">
        Account
      </a>
      <Button data-dropdown-item="">Logout</Button>
    </Dropdown>
  );
}

Render-Prop Usage (close / open)

import { Dropdown } from '@/ui/Dropdown';
 
export function Component() {
  return (
    <Dropdown aria-label="Open account actions">
      {({ close, open }) => (
        <>
          <span data-slot="label">{open ? 'Account (open)' : 'Account'}</span>
          <button type="button" onClick={close}>
            Logout
          </button>
        </>
      )}
    </Dropdown>
  );
}

API Reference

PropTypeDefaultDescription
iconIconicNamesArrowOutlinedDownIcon shown on the trigger button
classNamestringundefinedAdditional classes for the root dropdown container
childrenReactNode | ({ close, open, toggle }) => ReactNodeundefinedSlot content (label) and overlay content, or render-prop form
...propsbutton props-Standard button attributes passed to the trigger button

Slots

The component is composable through slots using data-slot.

SlotDescription
data-slot="label"Trigger content. You can provide one or multiple elements (text, avatar...).

Children without data-slot="label" are rendered inside the dropdown overlay panel.

Behavior

  • Clicking the trigger toggles the overlay.
  • Clicking outside closes the overlay.
  • Clicking a panel link (a[href]) closes the overlay.
  • Clicking a panel element with data-dropdown-item closes the overlay.
  • In render-prop mode, call close() to close programmatically.
  • The overlay stays mounted and uses animation classes for transitions:
    • open: animate-zoomIn
    • closing: animate-zoomOut
    • hidden state: opacity-0 scale-90 pointer-events-none

Accessibility

  • Provide an explicit aria-label on Dropdown for the trigger.
  • Ensure overlay actions are keyboard reachable (buttons/links).
  • Keep slot label content meaningful for assistive technologies.