Balance Logo
Balance
Reckon Design System
Open playroom

Dropdown V2

Dropdowns are contextual overlays for displaying lists of links or actions that the user can take.
Install
pnpm add @balance-web/dropdown-v2
Import usage
import {
Dropdown,
createTriggerSelectedLabel
} from '@balance-web/dropdown-v2';
  • Code
  • API

Alternatives

  • SelectMenu — For presenting a list of selectable options to a user

Primitives

This package exposes a number of primitives that can be used to compose different parts of a dropdown menu. Below are the things that can be composed with these primitives.

Renders the root of a dropdown menu.

Basic Usage

All dropdown primitives must be used as children of this component.

<Dropdown>{/* dropdown primitives */}</Dropdown>
defaultOpen

Controls the default open/closed state of a dropdown menu. This prop only controls the initial state of a dropdown, see the following section in order to full control the open/closed state of a dropdown.

<Dropdown defaultOpen={true}></Dropdown>
programmatic control of open/closed state

The open and onOpenChange props can be used to manually control the open/closed state of a dropdown menu, making it behave like a controlled input.

const [open, setIsOpen] = useState(open);
return <Dropdown open={open} onOpenChange={setIsOpen}></Dropdown>;
align

Sets the horizontal alignment of the dropdown relative to the trigger.

Accepts 3 values: start, center, end. Default value is center.

<Dropdown align="start"></Dropdown>
verticalOffset

Sets the vertical offset of the dropdown relative to the trigger.

A positive value moves the dropdown lower, a negative value moves the dropdown higher.

<Dropdown verticalOffset={-5}></Dropdown>

Renders an interactive element, taken as a child, that controls the open/closed state of a dropdown menu.

Basic Usage

The contents of a trigger can be passed as children which should be a string in most cases.

This component can be used in 2 flavors:

1 - via Attributes: By default, this component will write the following attributes to the child component provided: aria-haspopup and data-closed. Consumers can use the data-closed attribute via css to style the open/closed state of their triggers. The css would look something like:

{
background: 'grey';
`&[data-closed=true]`: {
background: 'blue';
}
}
<Dropdown.Trigger>
<Dropdown.TriggerButton label="Items" />
</Dropdown.Trigger>

2 - via Function: If the child provided to this component is a function, it will be executed with the following parameters.

<Dropdown.Trigger>
{({ isOpen, triggerProps }: CustomTriggerProps) => {
return (
<IconButton
aria-label="Customise options"
icon={MenuIcon}
label="Customise options"
{/* Use the isOpen prop to style trigger correctly. */}
tone={isOpen ? 'active' : 'passive'}
{/* Make sure to pass the trigger props. */}
{...triggerProps}
/>
);
}}
</Dropdown.Trigger>

The first parameter represents the open/closed state of the menu which can be used to style the trigger.

The second parameter is props that need to be passed to your trigger in order for the component to determine the open/closed state correctly. This parameter contains a ref that must be passed correctly to the interactive DOM element, which is a button in the above example. The open/closed state will not correctly if the ref is not applied properly to the trigger.

Renders a trigger button that seamlessly works with the Dropdown.Trigger primitive.

<Dropdown.Trigger>
<Dropdown.TriggerButton label="Dropdown" />
</Dropdown.Trigger>

This primitive represents a single clickable item in a dropdown menu.

Basic Usage

The contents of a menu item can be passed as children which should be a string in most cases.

<Dropdown.Item onSelect={/* do stuff */}>Will Smith</Dropdown.Item>
startElement

Renders a ReactNode to the left of the children.

<Dropdown.Item startElement={<UserAvatar size="xsmall" name="Will Smith" />}>
Will Smith
</Dropdown.Item>
endElement

Renders a ReactNode to the right of children.

<Dropdown.Item endElement={<Badge tone="positive" label="Active" />}>
Will Smith
</Dropdown.Item>
tone

Sets the tone of the menu item to one of: passive, critical. The default tone is passive.

<Dropdown.Item tone="critical">Delete user</Dropdown.Item>
disabled

Disables the menu item.

<Dropdown.Item disabled="critical">Will Smith</Dropdown.Item>
onSelect

Emits an event when user clicks on a menu item. Use this to perform actions.

<Dropdown.Item
onSelect={() => {
/* do stuff */
}}
>
Will Smith
</Dropdown.Item>
href

Converts the menu item to a link.

<Dropdown.Item href="#">Will Smith</Dropdown.Item>
target

A supporting prop for when using the menu item as a link.

Setting target=_blank will render the link as an external link, it will have pop out icon as an endElement.

<Dropdown.Item href="#" target="_blank">
Will Smith
</Dropdown.Item>

Note: A menu item with href and target=blank will not render a custom endElement if provided. It renders an external link icon instead.

Renders a nested submenu in a dropdown.

Basic Usage

The contents of a submenu item can be passed as children which should be a string in most cases.

<Dropdown.SubMenu>{/* submenu primitives */}</Dropdown.SubMenu>
defaultOpen

Controls the default open/closed state of a dropdown menu. This prop only controls the initial state of a dropdown, see the following section in order to full control the open/closed state of a dropdown.

<Dropdown.SubMenu defaultOpen={true}></Dropdown.SubMenu>
programmatic control of open/closed state

The open and onOpenChange props can be used to manually control the open/closed state of a dropdown menu, making it behave like a controlled input.

const [open, setIsOpen] = useState(open);
return (
<Dropdown.SubMenu open={open} onOpenChange={setIsOpen}></Dropdown.SubMenu>
);

Renders an interactive element that provides access to a submenu. This component mostly behaves like a Dropdown.Item with minor differences.

Basic Usage

The contents of a submenu trigger can be passed as children which should be a string in most cases.

<Dropdown.SubMenuTrigger>submenu</Dropdown.SubMenuTrigger>
startElement

Renders a ReactNode to the left of children.

<Dropdown.SubMenuTrigger
startElement={<UserAvatar size="small" name="submenu" />}
>
>submenu
</Dropdown.SubMenuTrigger>
tone

Sets the tone of the menu item to one of: passive, critical. The default tone is passive.

<Dropdown.SubMenuTrigger tone="critical">submenu</Dropdown.SubMenuTrigger>
disabled

Disables the menu item, preventing the user from interacting with the submenu.

<Dropdown.SubMenuTrigger disabled>submenu</Dropdown.SubMenuTrigger>
selected & selectedLabel

For cases when a submenu trigger wants to show that an item from its submenu has been selected. We can use the selected and selectedLabel props to achieve make the submenu trigger behave like a radio item.

Use the createTriggerSelectedLabel to generate the selectedLabel value.

<Dropdown.SubMenuTrigger
selected={Number(selected) > 2}
selectedLabel={createTriggerSelectedLabel('Group')}
startElement={<UserAvatar size="xsmall" name="2" />}
>
Group
</Dropdown.SubMenuTrigger>

Check out this section for a detailed example.

Renders a list of menu items as radios for the user to make a selection.

Basic Usage

Menu radio items must be used in side Dropdown.RadioGroup.

const [value, setValue] = useState('0');
return (
<Dropdown.RadioGroup
title="Radio Group"
value={value}
onValueChange={setValue}
>
{/* radio group primitives */}
</Dropdown.RadioGroup>
);
title

Title of the radio group.

value

The currently selected value of the radio group. This value must match one of the child radio items.

onValueChange

Emits the value of the item selected by the user. Use this to update state.

A menu item that behaves like a radio. Can only be used inside Dropdown.RadioGroup.

Basic Usage

The value that's emitted when user selects the radio menu item.

The contents of a radio menu item can be passed as children which should be a string in most cases.

Dropdown.RadioItem can only be used inside Dropdown.RadioGroup.

<Dropdown.RadioItem value="will_smith">Will Smith</Dropdown.RadioItem>

Use to draw a horizontal divider between items to create visual grouping.

<Dropdown.Item>Item 1<Dropdown.Item>
<Dropdown.Item>Item 2<Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item>Item 3<Dropdown.Item>
<Dropdown.Item>Item 4<Dropdown.Item>

Use to render label for the menu.

<Dropdown.MenuTitle>Group Title</Dropdown.MenuTitle>
<Dropdown.Item>Item 1<Dropdown.Item>
<Dropdown.Item>Item 2<Dropdown.Item>
<Dropdown.Item>Item 3<Dropdown.Item>

Note: A dropdown menu can only have 1 menu title.

Use to render label for a group of items.

<Dropdown.Item>Item 1<Dropdown.Item>
<Dropdown.Item>Item 2<Dropdown.Item>
<Dropdown.GroupTitle>Group Title</Dropdown.GroupTitle>
<Dropdown.Item>Item 3<Dropdown.Item>
<Dropdown.Item>Item 4<Dropdown.Item>

It can be used in combination with Dropdown.Divider to create a visually distinct group.

<Dropdown.Item>Item 1<Dropdown.Item>
<Dropdown.Item>Item 2<Dropdown.Item>
<Dropdown.Divider />
<Dropdown.GroupTitle>Group Title</Dropdown.GroupTitle>
<Dropdown.Item>Item 3<Dropdown.Item>
<Dropdown.Item>Item 4<Dropdown.Item>

Testing

The dropdown component and its primitives support the data attribute to facilitate testing with some extras. It will prefix testids with the parent testids (if provided), recursively for a better developer experience. The prefix behaviour is only applied to the data attribute testid, all other data attributes are passed as is.

Below are annotated samples of all the primitives that support the data attribute and nesting testid.

/** Supports testid. 'testable-dropdown' will the the testid of the root dropdown element, all accessible children will be prefixed with it */
<Dropdown data={{ testid: 'testable-dropdown' }}>
{/** No need to apply testid to triggers, it will be prefixed with the parent testid based on context. The testid for this item will be 'testable-dropdown-trigger' */}
<Dropdown.Trigger>...</Dropdown.Trigger>
{/** Supports testid. The will be prefixed with the parent testid based on context. The testid for this item will be 'testable-dropdown-item-1' */}
<Dropdown.Item data={{ testid: 'item-1' }}>...</Dropdown.Item>
{/** Supports testid. The will be prefixed with the parent testid based on context. The testid for this item will be 'testable-dropdown-item-2' */}
<Dropdown.Item data={{ testid: 'item-2' }}>...</Dropdown.Item>
{/** Supports testid. The will be prefixed with the parent testid based on context. The testid for this item will be 'testable-dropdown-radio-group-1'. All accessible children of this element will be prefixed with 'testable-dropdown-radio-group-1' */}
<Dropdown.RadioGroup data={{ testid: 'radiogroup-1' }}>
{/** Supports testid. The will be prefixed with the parent testid based on context. The testid for this item will be 'testable-dropdown-radio-group-1-item-1' */}
<Dropdown.RadioItem data={{ testid: 'item-1' }}>...</Dropdown.RadioItem>
{/** Supports testid. The will be prefixed with the parent testid based on context. The testid for this item will be 'testable-dropdown-radio-group-1-item-2' */}
<Dropdown.RadioItem data={{ testid: 'item-2' }}>...</Dropdown.RadioItem>
</Dropdown.RadioGroup>
{/** SubMenu behaves similar to root Dropdown in terms of testid except that it is prefixed with parent testid based on context. The testid for this item will be 'testable-dropdown-submenu-1'. All accessible children of this element will be prefixed with 'testable-dropdown-submenu-1'. */}
<Dropdown.SubMenu data={{ testid: 'submenu-1' }}>
{/** No need to apply testid to triggers, it will be prefixed with the parent testid. The testid for this item will be 'testable-dropdown-submenu-1-trigger' */}
<Dropdown.SubMenuTrigger>Submenu</Dropdown.SubMenuTrigger>
{/** Supports testid. The will be prefixed with the parent testid based on context. The testid for this item will be 'testable-dropdown-submenu-1-item-1' */}
<Dropdown.Item
data={{ testid: 'item-1' }}
onSelect={() => alert('Selected submenu 1 item 1')}
>
Submenu 1 Item 1
</Dropdown.Item>
{/** Supports testid. The will be prefixed with the parent testid based on context. The testid for this item will be 'testable-dropdown-submenu-1-item-2' */}
<Dropdown.Item
data={{ testid: 'item-2' }}
onSelect={() => alert('Selected submenu 1 item 2')}
>
Submenu 1 Item 2
</Dropdown.Item>
{/** Nested SubMenu behaves similar to root Dropdown in terms of testid except that it is prefixed with parent testid based on context. The testid for this item will be 'testable-dropdown-submenu-1-nestedsubmenu-1'. All accessible children of this element will be prefixed with 'testable-dropdown-submenu-1-nestedsubmenu-1'.*/}
<Dropdown.SubMenu data={{ testid: 'nestedsubmenu-1' }}>
{/** No need to apply testid to triggers, it will be prefixed with the parent testid. The testid for this item will be 'testable-dropdown-submenu-1-nestedsubmenu-1-trigger' */}
<Dropdown.SubMenuTrigger>Nested Submenu</Dropdown.SubMenuTrigger>
{/** Supports testid. The will be prefixed with the parent testid based on context. The testid for this item will be 'testable-dropdown-submenu-1-nestedsubmenu-1-item-1' */}
<Dropdown.Item
data={{ testid: 'item-1' }}
onSelect={() => alert('Selected nested submenu 1 item 1')}
>
Nested Submenu 1 Item 1
</Dropdown.Item>
{/** Supports testid. The will be prefixed with the parent testid based on context. The testid for this item will be 'testable-dropdown-submenu-1-nestedsubmenu-1-item-2' */}
<Dropdown.Item
data={{ testid: 'item-2' }}
onSelect={() => alert('Selected nested submenu 1 item 2')}
>
Nested Submenu 1 Item 2
</Dropdown.Item>
</Dropdown.SubMenu>
</Dropdown.SubMenu>
</Dropdown>

Inspect the recipes below to see the testids in action.

Receipes

Full examples of how to compose dropdown menus of varying complexity.

Simple items

Edit in Playroom

Custom trigger

Start element items

Radio items

Nested dropdown menus

Custom grouping

App switcher

A dropdown menu that's effectively a single radio group, but nested.

Copyright © 2024 Reckon. Designed and developed in partnership with Thinkmill.
Bitbucket logoJira software logoConfluence logo