Menu

Site and documentation links

Search documentation

Type to filter pages and sections. Use the arrow keys to move and Enter to open.

    Menu

    A button that opens a list of actions.

    You chose: Nothing yet

    View code
    'use client'
    
    import { useState } from 'react'
    import { Menu, MenuContent, MenuItem, MenuSeparator, MenuTrigger } from '@syncclouds/ui'
    
    export function MenuDemo() {
      const [choice, setChoice] = useState('Nothing yet')
    
      return (
        <div className="flex flex-col items-center gap-4">
          <Menu>
            <MenuTrigger>Actions</MenuTrigger>
            <MenuContent>
              <MenuItem onSelect={() => setChoice('Rename')}>Rename</MenuItem>
              <MenuItem onSelect={() => setChoice('Duplicate')}>Duplicate</MenuItem>
              <MenuItem onSelect={() => setChoice('Move')}>Move</MenuItem>
              <MenuSeparator />
              <MenuItem onSelect={() => setChoice('Archive')}>Archive</MenuItem>
            </MenuContent>
          </Menu>
          <p className="text-sm">You chose: {choice}</p>
        </div>
      )
    }

    Installation

    npm install @syncclouds/ui

    Usage

    import { Menu, MenuContent, MenuItem, MenuSeparator, MenuTrigger } from '@syncclouds/ui'
    <Menu>
      <MenuTrigger>Actions</MenuTrigger>
      <MenuContent>
        <MenuItem onSelect={rename}>Rename</MenuItem>
        <MenuSeparator />
        <MenuItem onSelect={archive}>Archive</MenuItem>
      </MenuContent>
    </Menu>

    Anatomy

    Menu
    ├── MenuTrigger
    └── MenuContent
        ├── MenuItem
        └── MenuSeparator

    Danger item

    Mark destructive items with data-tone="danger".

    You chose: Nothing yet

    View code
    'use client'
    
    import { useState } from 'react'
    import { Menu, MenuContent, MenuItem, MenuSeparator, MenuTrigger } from '@syncclouds/ui'
    
    /* data-tone="danger" marks a destructive item; the theme colours it. */
    export function MenuDanger() {
      const [choice, setChoice] = useState('Nothing yet')
    
      return (
        <div className="flex flex-col items-center gap-4">
          <Menu>
            <MenuTrigger>Member</MenuTrigger>
            <MenuContent>
              <MenuItem onSelect={() => setChoice('Change role')}>Change role</MenuItem>
              <MenuItem onSelect={() => setChoice('Resend invite')}>Resend invite</MenuItem>
              <MenuSeparator />
              <MenuItem data-tone="danger" onSelect={() => setChoice('Remove from team')}>
                Remove from team
              </MenuItem>
            </MenuContent>
          </Menu>
          <p className="text-sm">You chose: {choice}</p>
        </div>
      )
    }

    Disabled item

    View code
    import { Menu, MenuContent, MenuItem, MenuTrigger } from '@syncclouds/ui'
    
    /* Disabled items stay visible but the arrow keys skip them. */
    export function MenuDisabled() {
      return (
        <Menu>
          <MenuTrigger>Edit</MenuTrigger>
          <MenuContent>
            <MenuItem>Undo</MenuItem>
            <MenuItem disabled>Redo</MenuItem>
            <MenuItem>Cut</MenuItem>
            <MenuItem>Copy</MenuItem>
            <MenuItem disabled>Paste</MenuItem>
          </MenuContent>
        </Menu>
      )
    }

    Placement

    Choose where the menu opens. It flips and shifts to stay on screen.

    View code
    import { Menu, MenuContent, MenuItem, MenuTrigger } from '@syncclouds/ui'
    
    const PLACEMENTS = ['top-start', 'bottom-end', 'right-start'] as const
    
    export function MenuPlacement() {
      return (
        <div className="flex flex-wrap justify-center gap-3">
          {PLACEMENTS.map(placement => (
            <Menu key={placement}>
              <MenuTrigger>{placement}</MenuTrigger>
              <MenuContent placement={placement}>
                <MenuItem>Profile</MenuItem>
                <MenuItem>Settings</MenuItem>
                <MenuItem>Sign out</MenuItem>
              </MenuContent>
            </Menu>
          ))}
        </div>
      )
    }

    Without the theme

    View code
    import { Menu, MenuContent, MenuItem, MenuTrigger } from '@syncclouds/ui'
    
    export function MenuUnstyled() {
      return (
        <Menu>
          <MenuTrigger className="rounded-md bg-zinc-900 px-3 py-1.5 text-sm font-medium text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-zinc-100 dark:text-zinc-900">
            Sort by
          </MenuTrigger>
          <MenuContent className="min-w-40 rounded-lg border border-zinc-200 bg-white p-1 shadow-lg dark:border-zinc-700 dark:bg-zinc-900">
            {['Newest', 'Oldest', 'Name'].map(label => (
              <MenuItem
                key={label}
                className="cursor-pointer rounded-md px-3 py-1.5 text-sm text-zinc-900 outline-none focus:bg-indigo-600 focus:text-white dark:text-zinc-100 dark:focus:bg-indigo-400 dark:focus:text-zinc-950"
              >
                {label}
              </MenuItem>
            ))}
          </MenuContent>
        </Menu>
      )
    }

    Accessibility

    Follows the WAI-ARIA Menu Button pattern.

    KeysWhat they do
    Enter, Space, Arrow DownOn the trigger, opens the menu on the first item.
    Arrow UpOn the trigger, opens the menu on the last item.
    Arrow Down, Arrow UpMoves between items, skipping disabled ones and wrapping at the ends.
    Home, EndMoves to the first or last item.
    LettersMoves to the next item whose name starts with what you type.
    Enter, SpacePicks the focused item.
    EscapeCloses the menu and returns focus to the trigger.
    TabCloses the menu and moves on.
    • A press outside the menu closes it.
    • The pointer and the keyboard share one highlight: hovering an item focuses it.

    API reference

    Every part also accepts the props of the element it renders, including className and ref.

    Menurenders no element

    Holds the open state. Renders no element of its own.

    PropTypeDefaultDescription
    openbooleannoneWhether the menu is open, when you control it.
    defaultOpenbooleanfalseWhether it starts open.
    onOpenChange(open: boolean) => voidnoneCalled when it opens or closes.

    MenuTriggerrenders <button>

    Opens the menu. Carries aria-haspopup and aria-expanded, and gets focus back when the menu closes.

    PropTypeDefaultDescription
    asChildbooleanfalseUse your own button element.
    Data attributeValues
    data-state"open" | "closed"

    MenuContentrenders <div>

    The list of items. Rendered only while open, in the top layer, placed next to the trigger.

    PropTypeDefaultDescription
    placementPlacement"bottom-start"Where it opens. It flips and shifts to stay on screen.
    Data attributeValues
    data-state"open"
    data-side"top" | "right" | "bottom" | "left"

    MenuItemrenders <div>

    One action. Picking it runs onSelect, closes the menu and returns focus to the trigger.

    PropTypeDefaultDescription
    onSelect() => voidnoneRuns when the item is picked.
    disabledbooleanfalseSkipped by the keyboard and ignores clicks.
    Data attributeValues
    data-disabledpresent when disabled
    data-toneset it to "danger" for destructive items; the theme colours it

    MenuSeparatorrenders <div>

    A divider between groups of items.

    Built by Syncclouds. Syncclouds UI is in development.