Dropdown

A composable popover menu built on React Aria — where Select picks a value into its trigger, Dropdown is a free-form panel of grouped action rows with the BoardUI surface (radius 16, dropdown shadow) and the 150ms scale + blur appear animation. The same recipe behind the dashboard sidebar's team and account menus and the AI chat composer's add menu.

Installation

You can add this dropdown component using our CLI or manually:

The CLI copies the component source (with its internal dependencies) into your project and installs any required npm packages.

npx boardui@latest add dropdown

Model picker

The AI chat composer's model menu: rows act as a radio group — selectedkeeps the current row highlighted and a radio glyph sits at the row's end via justify-between.

{/* Radio-group rows: selected state on the row + a radio glyph at the end */}const [model, setModel] = useState("Fable 5"); <Dropdown isOpen={isOpen} onOpenChange={setIsOpen}> <DropdownTrigger className="…pill trigger with the current model + chevron…"> {model} <ChevronDownSmall className={cx("size-4", isOpen && "rotate-180")} /> </DropdownTrigger> <DropdownPopover aria-label="Models" placement="bottom start"> <DropdownGroup label="Models"> <div role="radiogroup" aria-label="Model" className="flex w-full flex-col gap-1"> {MODELS.map((name) => ( <DropdownItem key={name} selected={name === model} onSelect={() => { setModel(name); setIsOpen(false); }} className="justify-between gap-2.5" > <span className="truncate text-body-medium text-text-primary">{name}</span> <RadioDot selected={name === model} /> </DropdownItem> ))} </div> </DropdownGroup> </DropdownPopover></Dropdown>

Team menu

The sidebar's team card menu: a header and footer live alongside DropdownGroups as plain siblings, rows carry trailing badges, and DropdownDivider bleeds through the panel padding between sections.

{/* Header, grouped rows with badges, and a footer in one panel */}<Dropdown isOpen={isOpen} onOpenChange={setIsOpen}> <DropdownTrigger aria-label="Board team" className="…team card styles…"> <Avatar size="md" color="blue" src="/logo.png" alt="Board team" /> …name, email, chevron… </DropdownTrigger> <DropdownPopover aria-label="Board team menu" className="w-[265px]" dialogClassName="gap-[7px]"> <div className="flex items-center gap-2 px-2 pt-1"> <Avatar size="md" color="blue" src="/logo.png" alt="Board team" /> …name + email… </div> <DropdownGroup> <DropdownItem onSelect={close}> <span className="flex min-w-0 flex-1 items-center gap-2"> <RiMessage2Line className="size-5 text-foreground-icon-secondary" aria-hidden /> <span className="truncate text-body-medium text-text-primary">Messages</span> </span> <Badge color="neutral">94</Badge> </DropdownItem> </DropdownGroup> <DropdownDivider /> <DropdownGroup label="Company"></DropdownGroup> </DropdownPopover></Dropdown>

Account menu

The sidebar's account switcher: rows render avatars instead of icons, and the panel closes with a pair of secondary buttons — row content is free-form, so anything goes inside a DropdownItem.

{/* Row content is free-form: avatars, then buttons below a divider */}<DropdownPopover aria-label="Account menu" className="w-[265px]"> <DropdownGroup label="Users with access"> {users.map((user) => ( <DropdownItem key={user.name} onSelect={close} className="px-2 py-1.5"> <Avatar size="xs" color={user.color} initials={user.initials} /> <span className="truncate text-body-medium text-text-primary">{user.name}</span> </DropdownItem> ))} </DropdownGroup> <DropdownDivider className="my-3.5" /> <div className="flex items-center gap-3 px-2 pb-2"> <Button variant="secondary" size="small" leadingIcon={RiAddFill} className="flex-1"> Add user </Button> <Button variant="secondary" size="small" leadingIcon={RiEqualizer3Line} className="flex-1"> Manage </Button> </div></DropdownPopover>