React AriaExamples

Account Menu

A Menu with an interactive header, built with a Dialog and Popover.

Example#


For accessibility reasons, standalone Menus cannot contain any children other than menu items (optionally grouped into sections). To build patterns that include such elements, place the Menu into a Dialog with additional interactive elements as siblings.

import {Button, composeRenderProps, Dialog, DialogTrigger, Menu, MenuItem, Popover, Separator, Switch} from 'react-aria-components';
import type {MenuItemProps, SwitchProps} from 'react-aria-components';

function AccountMenuExample() {
  return (
    <div className="p-8 bg-gray-50 dark:bg-zinc-900 rounded-lg flex items-start justify-center">
      <DialogTrigger>
        <Button
          aria-label="Account"
          className="inline-flex items-center justify-center rounded-md p-1.5 text-white bg-transparent border-none hover:bg-gray-200 pressed:bg-gray-300 dark:hover:bg-zinc-800 dark:pressed:bg-zinc-700 transition-colors cursor-default outline-none focus-visible:ring-2 focus-visible:ring-blue-600"
        >
          <img
            alt=""
            src="https://i.imgur.com/xIe7Wlb.png"
            className="w-7 h-7 rounded-full"
          />
        </Button>
        <Popover
          placement="bottom end"
          className="p-2 overflow-auto rounded-lg bg-white dark:bg-zinc-950 shadow-lg ring-1 ring-black dark:ring-white ring-opacity-10 dark:ring-opacity-15 entering:animate-in entering:fade-in entering:placement-bottom:slide-in-from-top-1 entering:placement-top:slide-in-from-bottom-1 exiting:animate-out exiting:fade-out exiting:placement-bottom:slide-out-to-top-1 exiting:placement-top:slide-out-to-bottom-1 fill-mode-forwards origin-top-left"
        >
          <Dialog className="outline-none">
            <div className="flex gap-2 items-center mx-3 mt-2">
              <img
                alt=""
                src="https://i.imgur.com/xIe7Wlb.png"
                className="w-16 h-16 rounded-full"
              />
              <div className="flex flex-col gap-1">
                <div className="text-[15px] font-bold text-gray-900 dark:text-gray-100 leading-none">
                  Marissa Whitaker
                </div>
                <div className="text-base text-gray-900 dark:text-gray-100 leading-none mb-1">
                  user@example.com
                </div>
                <MySwitch>Dark Mode</MySwitch>
              </div>
            </div>
            <Separator className="border-none bg-gray-300 dark:bg-zinc-600 h-[1px] mx-3 mt-4 mb-2" />
            <Menu className="outline-none">
              <MyMenuItem id="new">Account Settings</MyMenuItem>
              <MyMenuItem id="open">Support</MyMenuItem>
              <Separator className="bg-gray-300 dark:bg-zinc-600 h-[1px] mx-3 my-2" />
              <MyMenuItem id="save">Legal notices</MyMenuItem>
              <MyMenuItem id="save-as">About</MyMenuItem>
              <Separator className="bg-gray-300 dark:bg-zinc-600 h-[1px] mx-3 my-2" />
              <MyMenuItem id="print">Sign out</MyMenuItem>
            </Menu>
          </Dialog>
        </Popover>
      </DialogTrigger>
    </div>
  );
}

function MyMenuItem(props: MenuItemProps) {
  return (
    <MenuItem
      {...props}
      className="group flex w-full items-center rounded-md px-3 py-2 box-border outline-none cursor-default text-gray-900 dark:text-gray-100 focus:bg-blue-500 focus:text-white"
    />
  );
}

function MySwitch(props: SwitchProps) {
  return (
    <Switch className="group flex gap-2 items-center text-gray-800 dark:text-zinc-200 text-base transition">
      {composeRenderProps(props.children, (children) => (
        <>
          <div className="flex h-3 w-6 p-[2px] items-center shrink-0 cursor-default rounded-full transition duration-200 ease-in-out shadow-inner border border-transparent bg-gray-400 dark:bg-zinc-400 group-pressed:bg-gray-500 dark:group-pressed:bg-zinc-300 group-selected:bg-gray-700 group-selected:dark:bg-zinc-300 group-selected:forced-colors:!bg-[Highlight] group-selected:group-pressed:bg-gray-800 group-selected:dark:group-pressed:bg-zinc-200 outline outline-0 outline-blue-600 dark:outline-blue-500 forced-colors:outline-[Highlight] outline-offset-2 group-focus-visible:outline-2">
            <div className="h-3 w-3 transform rounded-full bg-white dark:bg-zinc-900 outline outline-1 -outline-offset-1 outline-transparent shadow transition duration-200 ease-in-out translate-x-0 group-selected:translate-x-[100%]" />
          </div>
          {children}
        </>
      ))}
    </Switch>
  );
}
import {
  Button,
  composeRenderProps,
  Dialog,
  DialogTrigger,
  Menu,
  MenuItem,
  Popover,
  Separator,
  Switch
} from 'react-aria-components';
import type {
  MenuItemProps,
  SwitchProps
} from 'react-aria-components';

function AccountMenuExample() {
  return (
    <div className="p-8 bg-gray-50 dark:bg-zinc-900 rounded-lg flex items-start justify-center">
      <DialogTrigger>
        <Button
          aria-label="Account"
          className="inline-flex items-center justify-center rounded-md p-1.5 text-white bg-transparent border-none hover:bg-gray-200 pressed:bg-gray-300 dark:hover:bg-zinc-800 dark:pressed:bg-zinc-700 transition-colors cursor-default outline-none focus-visible:ring-2 focus-visible:ring-blue-600"
        >
          <img
            alt=""
            src="https://i.imgur.com/xIe7Wlb.png"
            className="w-7 h-7 rounded-full"
          />
        </Button>
        <Popover
          placement="bottom end"
          className="p-2 overflow-auto rounded-lg bg-white dark:bg-zinc-950 shadow-lg ring-1 ring-black dark:ring-white ring-opacity-10 dark:ring-opacity-15 entering:animate-in entering:fade-in entering:placement-bottom:slide-in-from-top-1 entering:placement-top:slide-in-from-bottom-1 exiting:animate-out exiting:fade-out exiting:placement-bottom:slide-out-to-top-1 exiting:placement-top:slide-out-to-bottom-1 fill-mode-forwards origin-top-left"
        >
          <Dialog className="outline-none">
            <div className="flex gap-2 items-center mx-3 mt-2">
              <img
                alt=""
                src="https://i.imgur.com/xIe7Wlb.png"
                className="w-16 h-16 rounded-full"
              />
              <div className="flex flex-col gap-1">
                <div className="text-[15px] font-bold text-gray-900 dark:text-gray-100 leading-none">
                  Marissa Whitaker
                </div>
                <div className="text-base text-gray-900 dark:text-gray-100 leading-none mb-1">
                  user@example.com
                </div>
                <MySwitch>Dark Mode</MySwitch>
              </div>
            </div>
            <Separator className="border-none bg-gray-300 dark:bg-zinc-600 h-[1px] mx-3 mt-4 mb-2" />
            <Menu className="outline-none">
              <MyMenuItem id="new">
                Account Settings
              </MyMenuItem>
              <MyMenuItem id="open">Support</MyMenuItem>
              <Separator className="bg-gray-300 dark:bg-zinc-600 h-[1px] mx-3 my-2" />
              <MyMenuItem id="save">
                Legal notices
              </MyMenuItem>
              <MyMenuItem id="save-as">About</MyMenuItem>
              <Separator className="bg-gray-300 dark:bg-zinc-600 h-[1px] mx-3 my-2" />
              <MyMenuItem id="print">Sign out</MyMenuItem>
            </Menu>
          </Dialog>
        </Popover>
      </DialogTrigger>
    </div>
  );
}

function MyMenuItem(props: MenuItemProps) {
  return (
    <MenuItem
      {...props}
      className="group flex w-full items-center rounded-md px-3 py-2 box-border outline-none cursor-default text-gray-900 dark:text-gray-100 focus:bg-blue-500 focus:text-white"
    />
  );
}

function MySwitch(props: SwitchProps) {
  return (
    <Switch className="group flex gap-2 items-center text-gray-800 dark:text-zinc-200 text-base transition">
      {composeRenderProps(props.children, (children) => (
        <>
          <div className="flex h-3 w-6 p-[2px] items-center shrink-0 cursor-default rounded-full transition duration-200 ease-in-out shadow-inner border border-transparent bg-gray-400 dark:bg-zinc-400 group-pressed:bg-gray-500 dark:group-pressed:bg-zinc-300 group-selected:bg-gray-700 group-selected:dark:bg-zinc-300 group-selected:forced-colors:!bg-[Highlight] group-selected:group-pressed:bg-gray-800 group-selected:dark:group-pressed:bg-zinc-200 outline outline-0 outline-blue-600 dark:outline-blue-500 forced-colors:outline-[Highlight] outline-offset-2 group-focus-visible:outline-2">
            <div className="h-3 w-3 transform rounded-full bg-white dark:bg-zinc-900 outline outline-1 -outline-offset-1 outline-transparent shadow transition duration-200 ease-in-out translate-x-0 group-selected:translate-x-[100%]" />
          </div>
          {children}
        </>
      ))}
    </Switch>
  );
}
import {
  Button,
  composeRenderProps,
  Dialog,
  DialogTrigger,
  Menu,
  MenuItem,
  Popover,
  Separator,
  Switch
} from 'react-aria-components';
import type {
  MenuItemProps,
  SwitchProps
} from 'react-aria-components';

function AccountMenuExample() {
  return (
    <div className="p-8 bg-gray-50 dark:bg-zinc-900 rounded-lg flex items-start justify-center">
      <DialogTrigger>
        <Button
          aria-label="Account"
          className="inline-flex items-center justify-center rounded-md p-1.5 text-white bg-transparent border-none hover:bg-gray-200 pressed:bg-gray-300 dark:hover:bg-zinc-800 dark:pressed:bg-zinc-700 transition-colors cursor-default outline-none focus-visible:ring-2 focus-visible:ring-blue-600"
        >
          <img
            alt=""
            src="https://i.imgur.com/xIe7Wlb.png"
            className="w-7 h-7 rounded-full"
          />
        </Button>
        <Popover
          placement="bottom end"
          className="p-2 overflow-auto rounded-lg bg-white dark:bg-zinc-950 shadow-lg ring-1 ring-black dark:ring-white ring-opacity-10 dark:ring-opacity-15 entering:animate-in entering:fade-in entering:placement-bottom:slide-in-from-top-1 entering:placement-top:slide-in-from-bottom-1 exiting:animate-out exiting:fade-out exiting:placement-bottom:slide-out-to-top-1 exiting:placement-top:slide-out-to-bottom-1 fill-mode-forwards origin-top-left"
        >
          <Dialog className="outline-none">
            <div className="flex gap-2 items-center mx-3 mt-2">
              <img
                alt=""
                src="https://i.imgur.com/xIe7Wlb.png"
                className="w-16 h-16 rounded-full"
              />
              <div className="flex flex-col gap-1">
                <div className="text-[15px] font-bold text-gray-900 dark:text-gray-100 leading-none">
                  Marissa
                  Whitaker
                </div>
                <div className="text-base text-gray-900 dark:text-gray-100 leading-none mb-1">
                  user@example.com
                </div>
                <MySwitch>
                  Dark
                  Mode
                </MySwitch>
              </div>
            </div>
            <Separator className="border-none bg-gray-300 dark:bg-zinc-600 h-[1px] mx-3 mt-4 mb-2" />
            <Menu className="outline-none">
              <MyMenuItem id="new">
                Account
                Settings
              </MyMenuItem>
              <MyMenuItem id="open">
                Support
              </MyMenuItem>
              <Separator className="bg-gray-300 dark:bg-zinc-600 h-[1px] mx-3 my-2" />
              <MyMenuItem id="save">
                Legal
                notices
              </MyMenuItem>
              <MyMenuItem id="save-as">
                About
              </MyMenuItem>
              <Separator className="bg-gray-300 dark:bg-zinc-600 h-[1px] mx-3 my-2" />
              <MyMenuItem id="print">
                Sign out
              </MyMenuItem>
            </Menu>
          </Dialog>
        </Popover>
      </DialogTrigger>
    </div>
  );
}

function MyMenuItem(
  props: MenuItemProps
) {
  return (
    <MenuItem
      {...props}
      className="group flex w-full items-center rounded-md px-3 py-2 box-border outline-none cursor-default text-gray-900 dark:text-gray-100 focus:bg-blue-500 focus:text-white"
    />
  );
}

function MySwitch(
  props: SwitchProps
) {
  return (
    <Switch className="group flex gap-2 items-center text-gray-800 dark:text-zinc-200 text-base transition">
      {composeRenderProps(
        props.children,
        (children) => (
          <>
            <div className="flex h-3 w-6 p-[2px] items-center shrink-0 cursor-default rounded-full transition duration-200 ease-in-out shadow-inner border border-transparent bg-gray-400 dark:bg-zinc-400 group-pressed:bg-gray-500 dark:group-pressed:bg-zinc-300 group-selected:bg-gray-700 group-selected:dark:bg-zinc-300 group-selected:forced-colors:!bg-[Highlight] group-selected:group-pressed:bg-gray-800 group-selected:dark:group-pressed:bg-zinc-200 outline outline-0 outline-blue-600 dark:outline-blue-500 forced-colors:outline-[Highlight] outline-offset-2 group-focus-visible:outline-2">
              <div className="h-3 w-3 transform rounded-full bg-white dark:bg-zinc-900 outline outline-1 -outline-offset-1 outline-transparent shadow transition duration-200 ease-in-out translate-x-0 group-selected:translate-x-[100%]" />
            </div>
            {children}
          </>
        )
      )}
    </Switch>
  );
}

Tailwind config#

This example uses the following plugins:

Add them to your tailwind.config.js:

module.exports = {
  // ...
  plugins: [
    require('tailwindcss-react-aria-components'),
    require('tailwindcss-animate')
  ]
};
module.exports = {
  // ...
  plugins: [
    require('tailwindcss-react-aria-components'),
    require('tailwindcss-animate')
  ]
};
module.exports = {
  // ...
  plugins: [
    require(
      'tailwindcss-react-aria-components'
    ),
    require(
      'tailwindcss-animate'
    )
  ]
};

Components#


Menu
A menu displays a list of actions or options that a user can choose.
Button
A button allows a user to perform an action.
Popover
A popover displays content in context with a trigger element.
Dialog
A dialog is an overlay shown above other content in an application.