# Action Button

> Action Button joins a main action to a chevron that opens a menu of other actions.

Source: /docs/components/action-button

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

## Basic action button

`children` label the main button, and its `onClick` runs the main action. The chevron opens a menu built from the `Menu.Item`, `Menu.Separator` and `Menu.Group` parts you pass to `menu`. Put destructive items last. `toggleLabel` names the chevron for screen readers and is required.

Clicking the main button never opens the menu, and the chevron never runs the main action. `ref`, `type`, `form` and the other button attributes go to the main button. The chevron is always `type="button"`, so it cannot submit a form. `className` goes to the group around both.

```tsx
import {
  IconContentEditCalendarSchedule,
  IconInterfaceEssentialCopy,
  IconInterfaceEssentialTrashDeleteBin2,
} from '@aksornproject/aksara-icons'
import { ActionButton, Menu } from '@aksornproject/aksara-ui'

export default function ActionButtonBasic() {
  return (
    <ActionButton
      toggleLabel="More send options"
      onClick={() => {}}
      menu={
        <>
          <Menu.Item leadingIcon={<IconContentEditCalendarSchedule />}>
            Schedule send
          </Menu.Item>
          <Menu.Item leadingIcon={<IconInterfaceEssentialCopy />}>
            Save as draft
          </Menu.Item>
          <Menu.Separator />
          <Menu.Item
            destructive
            leadingIcon={<IconInterfaceEssentialTrashDeleteBin2 />}
          >
            Discard
          </Menu.Item>
        </>
      }
    >
      Send
    </ActionButton>
  )
}
```

## Tones

`tone="brand"` fills both halves with the brand colour and is the default. `tone="accent"` draws white halves with a grey stroke for a secondary action. While the menu is open, the chevron half takes a darker fill and the chevron points up.

```tsx
import { ActionButton, Menu } from '@aksornproject/aksara-ui'

export default function ActionButtonTones() {
  const menu = (
    <>
      <Menu.Item>Export as CSV</Menu.Item>
      <Menu.Item>Export as spreadsheet</Menu.Item>
    </>
  )
  return (
    <div className="flex flex-wrap items-center gap-spacing-16">
      <ActionButton tone="brand" toggleLabel="Other formats" menu={menu}>
        Export PDF
      </ActionButton>
      <ActionButton tone="accent" toggleLabel="Other formats" menu={menu}>
        Export PDF
      </ActionButton>
    </div>
  )
}
```

## Sizes

`size` is `sm`, `md` or `lg`, 36, 40 and 44px tall. The default is `md`. The outer corners stay 10px at every size and the inner corners are square.

```tsx
import { ActionButton, Menu } from '@aksornproject/aksara-ui'

export default function ActionButtonSizes() {
  const menu = <Menu.Item>Save and open next</Menu.Item>
  return (
    <div className="flex flex-wrap items-center gap-spacing-16">
      <ActionButton size="sm" toggleLabel="More save options" menu={menu}>
        Save
      </ActionButton>
      <ActionButton size="md" toggleLabel="More save options" menu={menu}>
        Save
      </ActionButton>
      <ActionButton size="lg" toggleLabel="More save options" menu={menu}>
        Save
      </ActionButton>
    </div>
  )
}
```

## States

`state="disabled"` greys both halves. Neither takes focus, runs its action or opens the menu.

`state="loading"` draws a placeholder for content that is not ready yet. The label keeps its width but is hidden, the chevron goes and the group sets `aria-busy`. Both halves are disabled. It is not a progress indicator for a click.

Either state closes an open menu. An uncontrolled menu stays closed when the state returns to `default`.

```tsx
import { ActionButton, Menu } from '@aksornproject/aksara-ui'

export default function ActionButtonStates() {
  const menu = <Menu.Item>Save and open next</Menu.Item>
  return (
    <div className="flex flex-wrap items-center gap-spacing-16">
      <ActionButton
        state="disabled"
        toggleLabel="More save options"
        menu={menu}
      >
        Save
      </ActionButton>
      <ActionButton state="loading" toggleLabel="More save options" menu={menu}>
        Save
      </ActionButton>
      <ActionButton
        tone="accent"
        state="loading"
        toggleLabel="More save options"
        menu={menu}
      >
        Save
      </ActionButton>
    </div>
  )
}
```

## Placement

`placement` is `bottom-start`, the default, `bottom-end`, `top-start` or `top-end`. It picks the side of the whole button the menu opens on and the edge of the button it lines up with. The menu sits 4px away. It flips to the other side when the viewport has no room, so leave about 148px free on the menu side.

```tsx
import {
  ActionButton,
  Menu,
  type ActionButtonPlacement,
} from '@aksornproject/aksara-ui'

const PLACEMENTS: ActionButtonPlacement[] = [
  'bottom-start',
  'bottom-end',
  'top-start',
  'top-end',
]

export default function ActionButtonPlacement() {
  return (
    <div className="grid grid-cols-2 gap-spacing-24 py-spacing-160">
      {PLACEMENTS.map((placement) => (
        <ActionButton
          key={placement}
          placement={placement}
          toggleLabel={`${placement} options`}
          menu={
            <>
              <Menu.Item>Schedule send</Menu.Item>
              <Menu.Item>Save as draft</Menu.Item>
            </>
          }
        >
          {placement}
        </ActionButton>
      ))}
    </div>
  )
}
```

## Controlled

`open`, `defaultOpen` and `onOpenChange` control the menu like `Menu.Root`. A controlled `open` stays yours while the button is disabled or loading. If it is still `true` when the state returns to `default`, the menu opens again.

```tsx
import { ActionButton, Menu, Typography } from '@aksornproject/aksara-ui'
import { useState } from 'react'

export default function ActionButtonControlled() {
  const [open, setOpen] = useState(false)
  const [action, setAction] = useState('None yet')

  return (
    <div className="flex items-center gap-spacing-16">
      <ActionButton
        open={open}
        onOpenChange={setOpen}
        toggleLabel="More grade options"
        onClick={() => setAction('Saved')}
        menu={
          <>
            <Menu.Item
              onClick={() => setAction('Saved, opening the next student')}
            >
              Save and open next
            </Menu.Item>
            <Menu.Item onClick={() => setAction('Saved as a draft')}>
              Save as draft
            </Menu.Item>
          </>
        }
      >
        Save grade
      </ActionButton>
      <Typography variant="body-medium" className="text-color-text-secondary">
        {open ? 'Menu open' : action}
      </Typography>
    </div>
  )
}
```

## Accessibility

- The group has `role="group"` and holds two native buttons, so Tab reaches the main button, then the chevron.
- The chevron has `aria-label={toggleLabel}`, `aria-haspopup="menu"` and `aria-expanded`. The chevron icon and the divider are `aria-hidden`.
- The Menu module handles the keyboard. Escape, a press outside and choosing an item close the menu and return focus to the chevron.
- `state="loading"` keeps the label as the main button's accessible name.
- The focus ring on either half paints over its neighbour. Opening the menu does not add a ring.

## API

### ActionButton

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `defaultOpen` | `boolean` | `false` | Whether the menu is initially open. To render a controlled menu, use the `open` prop instead. |
| `onOpenChange` | `((open: boolean, eventDetails: MenuRoot.ChangeEventDetails) => void)` |  | Event handler called when the menu is opened or closed. |
| `open` | `boolean` |  | Whether the menu is currently open. |
| `children` (required) | `ReactNode` |  | The main button label. |
| `leadingIcon` | `ReactNode` |  | An icon before the main label. |
| `menu` (required) | `ReactNode` |  | `Menu.Item`, `Menu.Separator` and `Menu.Group` parts for the popup. Put destructive items last. |
| `toggleLabel` (required) | `string` |  | The accessible name of the chevron button, such as "More send options". |
| `tone` | `"accent" \| "brand"` | `'brand'` |  |
| `size` | `"lg" \| "md" \| "sm"` | `'md'` |  |
| `state` | `"default" \| "disabled" \| "loading"` | `'default'` |  |
| `placement` | `"bottom-end" \| "bottom-start" \| "top-end" \| "top-start"` | `'bottom-start'` |  |
| `className` | `string` |  | Classes merged last with `cn()` onto the group. Other attributes go to the main button. |