# DropdownMenu

> A value picker with a button trigger and a list of options.

Source: /docs/components/dropdown-menu

```tsx
import { DropdownMenu } from '@aksornproject/aksara-ui'
```

For a labelled form field, use [Select](/docs/components/select). To run actions, use [Menu](/docs/components/menu).

## Sort picker

`DropdownMenu` has 11 parts. `DropdownMenu.Root` is `Select.Root`. `DropdownMenu.Button` is a library `Button` that shows `leadingText`, the selected value and a `trailingNode` that swaps between its `open` and `closed` nodes. `DropdownMenu.List` is the portal, positioner, popup and list in one part.

The popup is at least 230px wide and at most 256px tall, with a border and a 10px radius. A selected item shows a 24px accent tick. `selectedIndicator` replaces the tick, and an empty fragment hides it.

```tsx
import {
  IconInterfaceEssentialArrow,
  IconInterfaceEssentialArrow1,
} from '@aksornproject/aksara-icons'
import { DropdownMenu } from '@aksornproject/aksara-ui'
import { useState } from 'react'

type Sort = 'newest' | 'oldest' | 'name'

const options: { value: Sort; label: string }[] = [
  { value: 'newest', label: 'Newest' },
  { value: 'oldest', label: 'Oldest' },
  { value: 'name', label: 'Name' },
]

export default function DropdownMenuSort() {
  const [sort, setSort] = useState<Sort>('newest')

  return (
    <DropdownMenu.Root items={options} value={sort}>
      <DropdownMenu.Button
        variant="secondary-accent"
        leadingText="Sort by:"
        trailingNode={{
          open: <IconInterfaceEssentialArrow1 className="ml-auto" />,
          closed: <IconInterfaceEssentialArrow className="ml-auto" />,
        }}
        className="w-[216px]"
      />
      <DropdownMenu.List align="end">
        {options.map((item) => (
          <DropdownMenu.Item
            key={item.value}
            value={item.value}
            onClick={() => setSort(item.value)}
          >
            {item.label}
          </DropdownMenu.Item>
        ))}
      </DropdownMenu.List>
    </DropdownMenu.Root>
  )
}
```

## With Select

This is the same sort picker built with `Select`. `Select.Trigger` puts the label above the field, and the popup is at least 280px wide. For a list of actions, use `Menu`, whose items are announced as `menuitem` rather than `option`.

```tsx
import { Select } from '@aksornproject/aksara-ui'
import { useState } from 'react'

type Sort = 'newest' | 'oldest' | 'name'

const options: { value: Sort; label: string }[] = [
  { value: 'newest', label: 'Newest' },
  { value: 'oldest', label: 'Oldest' },
  { value: 'name', label: 'Name' },
]

export default function DropdownMenuSelect() {
  const [sort, setSort] = useState<Sort>('newest')

  return (
    <div className="w-[216px]">
      <Select.Root
        items={options}
        value={sort}
        onValueChange={(value) => value && setSort(value)}
      >
        <Select.Trigger label="Sort by" />
        <Select.Portal>
          <Select.Positioner align="end">
            <Select.Popup>
              <Select.List>
                {options.map((item) => (
                  <Select.Item key={item.value} value={item.value}>
                    {item.label}
                  </Select.Item>
                ))}
              </Select.List>
            </Select.Popup>
          </Select.Positioner>
        </Select.Portal>
      </Select.Root>
    </div>
  )
}
```

## Accessibility

- `DropdownMenu.Button` is one native button with `role="combobox"`. Its default `type` is `button`, so opening it inside a form does not submit the form.
- A `DropdownMenu.Trigger` with a library `Button` child makes that button the trigger, so buttons do not nest.
- The popup is a `listbox` of `option`s. An action menu built on it announces values, not actions. Use `Menu` for actions.
- Arrow keys reach disabled items, but Enter and clicks do not select them.

## API

### DropdownMenu.Root

`DropdownMenu.Root` is `Select.Root`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` |  |  |
| `inputRef` | `React.Ref<HTMLInputElement>` |  | A ref to access the hidden input element. |
| `name` | `string` |  | Identifies the field when a form is submitted. |
| `form` | `string` |  | Identifies the form that owns the hidden input. Useful when the select is rendered outside the form. |
| `autoComplete` | `string` |  | Provides a hint to the browser for autofill. |
| `id` | `string` |  | The id of the Select. |
| `required` | `boolean` | `false` | Whether the user must choose a value before submitting a form. |
| `readOnly` | `boolean` | `false` | Whether the user should be unable to choose a different option from the select popup. |
| `disabled` | `boolean` | `false` | Whether the component should ignore user interaction. |
| `multiple` | `Multiple` | `false` | Whether multiple items can be selected. |
| `highlightItemOnHover` | `boolean` | `true` | Whether moving the pointer over items should highlight them. Disabling this prop allows CSS `:hover` to be differentiated from the `:focus` (`data-highlighted`) state. |
| `defaultOpen` | `boolean` | `false` | Whether the select popup is initially open. To render a controlled select popup, use the `open` prop instead. |
| `onOpenChange` | `((open: boolean, eventDetails: SelectRootOpenChangeEventDetails) => void)` |  | Event handler called when the select popup is opened or closed. |
| `onOpenChangeComplete` | `((open: boolean) => void)` |  | Event handler called after any animations complete when the select popup is opened or closed. |
| `open` | `boolean` |  | Whether the select popup is currently open. |
| `modal` | `boolean` | `true` | Determines if the select enters a modal state when open. - `true`: user interaction is limited to the select: document page scroll is locked and pointer interactions on outside elements are disabled. - `false`: user interaction with the rest of the document is allowed. On touch devices, a `true` modal blocks outside taps but leaves the page scrollable unless the popup spans nearly the full viewport width, matching native iOS behavior. |
| `actionsRef` | `React.RefObject<SelectRootActions \| null>` |  | A ref to imperative actions. - `unmount`: Ends the closing phase of the select after an externally controlled closing animation finishes. Call `preventUnmountOnClose()` in `onOpenChange` first, otherwise the select completes closing on its own. Whether it leaves the DOM is decided by `keepMounted` on the portal. - `close`: Closes the select imperatively when called. |
| `items` | `readonly Group<any>[] \| readonly { label: React.ReactNode; value: any; }[] \| Record<string, ReactNode>` |  | Data structure of the items rendered in the select popup. When specified, `<Select.Value>` renders the label of the selected item instead of the raw value. |
| `itemToStringLabel` | `((itemValue: Value) => string)` |  | When the item values are objects (`<Select.Item value={object}>`), this function converts the object value to a string representation for display in the trigger. If the shape of the object is `{ value, label }`, the label will be used automatically without needing to specify this prop. |
| `itemToStringValue` | `((itemValue: Value) => string)` |  | When the item values are objects (`<Select.Item value={object}>`), this function converts the object value to a string representation for form submission. If the shape of the object is `{ value, label }`, the value will be used automatically without needing to specify this prop. |
| `isItemEqualToValue` | `((itemValue: Value, value: Value) => boolean)` |  | Custom comparison logic used to determine if a select item value matches the current selected value. Useful when item values are objects without matching referentially. Defaults to `Object.is` comparison. |
| `defaultValue` | `SelectInputValue<Value, Multiple> \| null` |  | The uncontrolled value of the select when it's initially rendered. To render a controlled select, use the `value` prop instead. |
| `value` | `SelectInputValue<Value, Multiple> \| null` |  | The value of the select. Use when controlled. |
| `onValueChange` | `((value: SelectOutputValue<Value, Multiple> \| (Multiple extends true ? never : null), eventDetails: SelectRootChangeEventDetails) => void)` |  | Event handler called when the value of the select changes. |
| `loading` | `boolean` |  | Shows the skeleton bar in the trigger. The popup does not open and the value cannot change. The trigger stays focusable. |

### DropdownMenu.Button

A library `Button`, `size="lg"` by default, that takes every `Button` prop and these two. Its children are ignored. Its event handlers run before the Base UI handlers. `disabled` stops it opening.

| Prop           | Type                                     | Default  |
| -------------- | ---------------------------------------- | -------- |
| `leadingText`  | `string`                                 | required |
| `trailingNode` | `{ open: ReactNode; closed: ReactNode }` | none     |

| Prop | Type | Description |
| --- | --- | --- |
| `color` | `"brand" \| "destructive" \| "information" \| "sub-brand" \| "success" \| "warning"` |  |
| `variant` | `"link" \| "link-accent" \| "link-brand" \| "primary" \| "secondary" \| "secondary-accent" \| "secondary-brand" \| "tertiary" \| "tertiary-accent" \| "tertiary-brand" \| "tertiary-inverted"` |  |
| `leadingIcon` | `ReactNode` |  |
| `trailingIcon` | `ReactNode` |  |
| `size` | `"huge" \| "lg" \| "md" \| "sm" \| "xl" \| "xs"` |  |
| `iconOnly` | `boolean` | Renders a square button. A `leadingIcon` replaces the children; without one, the children render inside the icon box. `trailingIcon` still renders. |
| `wrapperClasses` | `{ leadingIcon?: string; trailingIcon?: string; iconOnlyChildren?: string; }` |  |
| `forceFocusVisible` | `boolean` | Shows the focus ring without keyboard focus, for example while a menu opened by this button is open. |
| `loading` | `boolean` | Hides the label, keeps the width, and shows three dots. The button is `aria-disabled` and stays focusable. Click, key, and pointer handlers do not run, including those on a `render` element. |
| `render` | `ComponentRenderFn<HTMLProps, ButtonState> \| React.ReactElement<unknown, string \| React.JSXElementConstructor<any>>` | Replaces the rendered element. See Base UI `render`. |
| `nativeButton` | `boolean` | Set to `false` when `render` returns an element other than `<button>`, such as `<a>` or `<div>`. Base UI then adds `role="button"`, the tab stop, and Enter and Space activation. See Base UI `nativeButton`. |

### DropdownMenu.List

Takes the Base UI Select Popup props, `children` and these positioning props. `className` merges on the popup.

| Prop                   | Type                       | Default    |
| ---------------------- | -------------------------- | ---------- |
| `side`                 | `Side`                     | `'bottom'` |
| `sideOffset`           | `number \| OffsetFunction` | `4`        |
| `align`                | `Align`                    | `'start'`  |
| `alignOffset`          | `number \| OffsetFunction` | `0`        |
| `alignItemWithTrigger` | `boolean`                  | `false`    |

### DropdownMenu.Item

Takes the Base UI Select Item props. `className` merges with the defaults instead of replacing them.

| Prop                | Type        | Default              |
| ------------------- | ----------- | -------------------- |
| `children`          | `ReactNode` | required             |
| `selectedIndicator` | `ReactNode` | the 24px accent tick |

### DropdownMenu.Trigger

Takes the Base UI Select Trigger props. A library `Button` child becomes the trigger. Other children render inside the Base UI trigger.

### DropdownMenu.Positioner

`DropdownMenu.Value`, `Icon`, `Positioner`, `Popup`, `ItemIndicator` and `ItemText` are the unstyled Base UI Select parts.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: SelectPositionerState) => string \| undefined)` |  | CSS class applied to the element, or a function that returns a class based on the component's state. |
| `render` | `ComponentRenderFn<HTMLProps, SelectPositionerState> \| React.ReactElement<unknown, string \| React.JSXElementConstructor<any>>` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |
| `style` | `React.CSSProperties \| ((state: SelectPositionerState) => React.CSSProperties \| undefined)` |  | Style applied to the element, or a function that returns a style object based on the component's state. |
| `anchor` | `Element \| React.RefObject<Element \| null> \| VirtualElement \| (() => Element \| VirtualElement \| null) \| null` |  | An element to position the popup against. By default, the popup will be positioned against the trigger. |
| `positionMethod` | `"absolute" \| "fixed"` | `'absolute'` | Determines which CSS `position` property to use. |
| `side` | `"bottom" \| "inline-end" \| "inline-start" \| "left" \| "right" \| "top"` | `'bottom'` | Which side of the anchor element to align the popup against. May automatically change to avoid collisions. |
| `sideOffset` | `number \| OffsetFunction` | `0` | Distance between the anchor and the popup in pixels. Also accepts a function that returns the distance to read the dimensions of the anchor and positioner elements, along with its side and alignment. The function takes a `data` object parameter with the following properties: - `data.anchor`: the dimensions of the anchor element with properties `width` and `height`. - `data.positioner`: the dimensions of the positioner element with properties `width` and `height`. - `data.side`: which side of the anchor element the positioner is aligned against. - `data.align`: how the positioner is aligned relative to the specified side. |
| `align` | `"center" \| "end" \| "start"` | `'center'` | How to align the popup relative to the specified side. |
| `alignOffset` | `number \| OffsetFunction` | `0` | Additional offset along the alignment axis in pixels. Also accepts a function that returns the offset to read the dimensions of the anchor and positioner elements, along with its side and alignment. The function takes a `data` object parameter with the following properties: - `data.anchor`: the dimensions of the anchor element with properties `width` and `height`. - `data.positioner`: the dimensions of the positioner element with properties `width` and `height`. - `data.side`: which side of the anchor element the positioner is aligned against. - `data.align`: how the positioner is aligned relative to the specified side. |
| `collisionBoundary` | `Boundary` | `'clipping-ancestors'` | An element or a rectangle that delimits the area that the popup is confined to. |
| `collisionPadding` | `Padding` | `5` | Additional space to maintain from the edge of the collision boundary. |
| `sticky` | `boolean` | `false` | Whether to maintain the popup in the viewport after the anchor element was scrolled out of view. |
| `arrowPadding` | `number` | `5` | Minimum distance to maintain between the arrow and the edges of the popup. Use it to prevent the arrow element from hanging out of the rounded corners of a popup. |
| `disableAnchorTracking` | `boolean` | `false` | Whether to disable the popup from tracking any layout shift of its positioning anchor. |
| `collisionAvoidance` | `CollisionAvoidance` |  | Determines how to handle collisions when positioning the popup. `side` controls overflow on the preferred placement axis (`top`/`bottom` or `left`/`right`): - `'flip'`: keep the requested side when it fits; otherwise try the opposite side (`top` and `bottom`, or `left` and `right`). - `'shift'`: never change side; keep the requested side and move the popup within the clipping boundary so it stays visible. - `'none'`: do not correct side-axis overflow. `align` controls overflow on the alignment axis (`start`/`center`/`end`): - `'flip'`: keep side, but swap `start` and `end` when the requested alignment overflows. - `'shift'`: keep side and requested alignment, then nudge the popup along the alignment axis to fit. - `'none'`: do not correct alignment-axis overflow. `fallbackAxisSide` controls fallback behavior on the perpendicular axis when the preferred axis cannot fit: - `'start'`: allow perpendicular fallback and try the logical start side first (`top` before `bottom`, or `left` before `right` in LTR). - `'end'`: allow perpendicular fallback and try the logical end side first (`bottom` before `top`, or `right` before `left` in LTR). - `'none'`: do not fallback to the perpendicular axis. When `side` is `'shift'`, explicitly setting `align` only supports `'shift'` or `'none'`. If `align` is omitted, it defaults to `'flip'`. |
| `alignItemWithTrigger` | `boolean` | `true` | Whether the positioner overlaps the trigger so the selected item's text is aligned with the trigger's value text. This only applies to mouse input and is automatically disabled if there is not enough space. |