Aksara UI

Action Button

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

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.

<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.

<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.

<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.

<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.

<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.

None yet

<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

Edit on GitHub

On this page