Atrium // Components

Dropdown

A menu component that displays a list of navigable options in a popover.

The Dropdown provides a button-triggered menu for presenting a list of actions or navigation links. It combines a-popover for positioning and a-list for keyboard-navigable options.

Example

Click the button to reveal the dropdown menu with selectable options.

/* @jsxImportSource vue */
import type { Story } from "../../../components/stories/stories.js";

export default {
  tags: ["public"],
  args: {
    label: "Choose",
    data: [
      { title: "Test 1", url: "" },
      { title: "Test 2", url: "" },
    ],
  },
  argTypes: {},
} satisfies Story;

export const Default = {
  render: (args) => {
    return (
      <div class="flex max-w-full items-center justify-center pt-[50px] pb-[200px]">
        <a-popover-trigger class="w-auto">
          <button
            slot="trigger"
            type="button"
            class="group w-full cursor-pointer rounded-md border border-gray-300 bg-white p-2 px-4 text-left text-gray-900 hover:bg-gray-200 sm:w-auto"
          >
            <a-box class="relative w-auto min-w-[180px] md:max-w-[300px]">
              <span class="block flex-1">{args.label}</span>
            </a-box>
          </button>

          <a-popover class="group">
            <div class="group my-1 inline-block rounded-md border border-gray-300 bg-white opacity-0 shadow-lg transition-opacity duration-100 group-[&[enabled]]:opacity-100">
              <a-list
                style={`width: ${210}px`}
                class="group scrollbar-thin scrollbar-transparent -translate-y-1 block max-h-[300px] w-auto overflow-auto overflow-hidden rounded-md bg-white transition-all duration-150 group-[&[enabled]]:translate-y-0"
                onChange={(e: CustomEvent) => {
                  const option = e.detail.selected;
                  const link = option.querySelector("a[href]");
                  link?.click();
                }}
              >
                {args.data?.map((item, index) => {
                  return (
                    <a-list-item
                      key={index}
                      class="mb-1 last:mb-0 focus-within:bg-blue-100 group-focus-within:aria-selected:bg-blue-100"
                    >
                      <a
                        tabindex="-1"
                        href={item.url}
                        class="block overflow-hidden text-ellipsis whitespace-nowrap px-4 py-2 text-gray-900 hover:bg-blue-100 active:bg-blue-200"
                      >
                        {item.title}
                      </a>
                    </a-list-item>
                  );
                })}
              </a-list>
            </div>
          </a-popover>
        </a-popover-trigger>
      </div>
    );
  },
};

const DropdownMenu = (props: {
  label: string;
  items: { title: string; url: string }[];
}) => (
  <a-popover-trigger class="w-auto">
    <button
      slot="trigger"
      type="button"
      class="group cursor-pointer rounded-md bg-transparent p-2 px-3 text-left text-gray-700 hover:bg-gray-100"
    >
      <span class="block">{props.label}</span>
    </button>

    <a-popover class="group">
      <div class="group my-1 inline-block rounded-md border border-gray-200 bg-white opacity-0 shadow-lg transition-opacity duration-100 group-[&[enabled]]:opacity-100">
        <a-list
          style="width: 180px"
          class="group scrollbar-thin scrollbar-transparent -translate-y-1 max-h-[300px] overflow-auto rounded-md bg-white transition-all duration-150 group-[&[enabled]]:translate-y-0"
        >
          {props.items.map((item, index) => (
            <a-list-item key={index} class="mb-1 last:mb-0 focus-within:bg-blue-100">
              <a
                tabindex="-1"
                href={item.url}
                class="block px-4 py-2 text-gray-900 hover:bg-blue-100"
              >
                {item.title}
              </a>
            </a-list-item>
          ))}
        </a-list>
      </div>
    </a-popover>
  </a-popover-trigger>
);

export const SettingsPanel = {
  render: () => {
    return (
      <div class="w-full bg-gray-50 p-6">
        <div class="mx-auto max-w-xl rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
          {/* Header */}
          <div class="mb-6 flex items-center gap-3">
            <div class="h-10 w-10 rounded-lg bg-gray-200" />
            <div class="flex flex-col gap-1">
              <div class="h-5 w-32 rounded bg-gray-300" />
              <div class="h-3 w-48 rounded bg-gray-200" />
            </div>
          </div>

          {/* Divider */}
          <div class="mb-6 h-px w-full bg-gray-200" />

          {/* Settings rows */}
          <div class="flex flex-col gap-5">
            {/* Language setting */}
            <div class="flex items-center justify-between">
              <div class="flex flex-col gap-1">
                <span class="font-medium text-gray-700 text-sm">Language</span>
                <span class="text-gray-500 text-xs">Select your preferred language</span>
              </div>
              <a-popover-trigger>
                <button
                  slot="trigger"
                  type="button"
                  class="flex cursor-pointer items-center gap-2 rounded-md border border-gray-300 bg-white px-3 py-2 text-gray-700 text-sm hover:bg-gray-50"
                >
                  <span>English</span>
                  <span class="text-gray-400">▼</span>
                </button>
                <a-popover class="group" placements="bottom-end,top-end">
                  <div class="group my-1 inline-block rounded-md border border-gray-200 bg-white opacity-0 shadow-lg transition-opacity duration-100 group-[&[enabled]]:opacity-100">
                    <a-list
                      style="width: 150px"
                      class="group -translate-y-1 max-h-[200px] overflow-auto rounded-md bg-white transition-all duration-150 group-[&[enabled]]:translate-y-0"
                    >
                      {["English", "Deutsch", "Français", "Español"].map((lang, i) => (
                        <a-list-item key={i} class="focus-within:bg-blue-100">
                          <button
                            type="button"
                            class="block w-full px-4 py-2 text-left text-gray-900 hover:bg-blue-100"
                          >
                            {lang}
                          </button>
                        </a-list-item>
                      ))}
                    </a-list>
                  </div>
                </a-popover>
              </a-popover-trigger>
            </div>

            {/* Timezone setting */}
            <div class="flex items-center justify-between">
              <div class="flex flex-col gap-1">
                <span class="font-medium text-gray-700 text-sm">Timezone</span>
                <span class="text-gray-500 text-xs">Set your local timezone</span>
              </div>
              <a-popover-trigger>
                <button
                  slot="trigger"
                  type="button"
                  class="flex cursor-pointer items-center gap-2 rounded-md border border-gray-300 bg-white px-3 py-2 text-gray-700 text-sm hover:bg-gray-50"
                >
                  <span>UTC+0</span>
                  <span class="text-gray-400">▼</span>
                </button>
                <a-popover class="group" placements="bottom-end,top-end">
                  <div class="group my-1 inline-block rounded-md border border-gray-200 bg-white opacity-0 shadow-lg transition-opacity duration-100 group-[&[enabled]]:opacity-100">
                    <a-list
                      style="width: 180px"
                      class="group -translate-y-1 max-h-[200px] overflow-auto rounded-md bg-white transition-all duration-150 group-[&[enabled]]:translate-y-0"
                    >
                      {[
                        "UTC-8 Pacific",
                        "UTC-5 Eastern",
                        "UTC+0 London",
                        "UTC+1 Berlin",
                        "UTC+9 Tokyo",
                      ].map((tz, i) => (
                        <a-list-item key={i} class="focus-within:bg-blue-100">
                          <button
                            type="button"
                            class="block w-full px-4 py-2 text-left text-gray-900 hover:bg-blue-100"
                          >
                            {tz}
                          </button>
                        </a-list-item>
                      ))}
                    </a-list>
                  </div>
                </a-popover>
              </a-popover-trigger>
            </div>

            {/* Theme skeleton row */}
            <div class="flex items-center justify-between">
              <div class="flex flex-col gap-1">
                <div class="h-4 w-16 rounded bg-gray-300" />
                <div class="h-3 w-36 rounded bg-gray-200" />
              </div>
              <div class="h-9 w-24 rounded-md bg-gray-200" />
            </div>

            {/* Notifications skeleton row */}
            <div class="flex items-center justify-between">
              <div class="flex flex-col gap-1">
                <div class="h-4 w-24 rounded bg-gray-300" />
                <div class="h-3 w-44 rounded bg-gray-200" />
              </div>
              <div class="h-6 w-11 rounded-full bg-gray-200" />
            </div>
          </div>

          {/* Divider */}
          <div class="my-6 h-px w-full bg-gray-200" />

          {/* Save button skeleton */}
          <div class="h-10 w-full rounded-lg bg-gray-300" />
        </div>
      </div>
    );
  },
};

Examples with context

Settings Panel

Accessibility

  • The menu can be navigated using arrow keys
  • Pressing Enter or Space activates the focused option
  • Escape dismisses the dropdown and returns focus to the trigger
  • The trigger button indicates the expanded/collapsed state via ARIA attributes

References