# ActionIcon (/docs/action-icon)





`ActionIcon` is the icon-only counterpart to [`Button`](/docs/button). It shares the same orthogonal (variant × color) model and adds its own `radius` scale so circular triggers (toolbars, like buttons, dismiss controls) drop straight in.

```tsx
import { ActionIcon } from "@42-central/ui-react/action-icon";
import { Settings } from "lucide-react";

<ActionIcon aria-label="Open settings">
  <Settings />
</ActionIcon>;
```

The icon is auto-sized by the parent's `size` variant — drop in a bare `lucide-react` icon and you'll never need to set `size={n}` on it.

<Alert
  type="warning"
  className="my-4"
  description="
  <>
    <strong>
      <code>aria-label</code> is required.
    </strong>{&#x22; &#x22;}
    The icon children are decoration — screen readers won't announce them.
    Without a label, the trigger is effectively invisible to assistive tech.
  </>
"
/>

## Playground [#playground]

<story.Playground />

## Variants [#variants]

Five visual treatments, identical in semantics to [`Button`](/docs/button)'s. Each reads off the same color slots (`--c-solid`, `--c-soft`, `--c-text`) so palette swaps are uniform.

<Preview align="center" code="&#x22;import { Star } from 'lucide-react';\n\n<ActionIcon variant=\&#x22;filled\&#x22;  aria-label=\&#x22;Star\&#x22;><Star /></ActionIcon>\n<ActionIcon variant=\&#x22;light\&#x22;   aria-label=\&#x22;Star\&#x22;><Star /></ActionIcon>\n<ActionIcon variant=\&#x22;outline\&#x22; aria-label=\&#x22;Star\&#x22;><Star /></ActionIcon>\n<ActionIcon variant=\&#x22;subtle\&#x22;  aria-label=\&#x22;Star\&#x22;><Star /></ActionIcon>\n<ActionIcon variant=\&#x22;default\&#x22; aria-label=\&#x22;Star\&#x22;><Star /></ActionIcon>&#x22;">
  <div className="flex flex-wrap items-center gap-3">
    <ActionIcon variant="filled" aria-label="Filled">
      <Star />
    </ActionIcon>

    <ActionIcon variant="light" aria-label="Light">
      <Star />
    </ActionIcon>

    <ActionIcon variant="outline" aria-label="Outline">
      <Star />
    </ActionIcon>

    <ActionIcon variant="subtle" aria-label="Subtle">
      <Star />
    </ActionIcon>

    <ActionIcon variant="default" aria-label="Default">
      <Star />
    </ActionIcon>
  </div>
</Preview>

`subtle` remains the toolbar workhorse: borderless, no resting fill, hover tint reveals the palette.

## Colors [#colors]

Every variant (except `default`) accepts any color from the shared palette.

<Preview align="center" code="&#x22;<ActionIcon variant=\&#x22;filled\&#x22;  color=\&#x22;brand\&#x22; aria-label=\&#x22;Favorite\&#x22;><Heart /></ActionIcon>\n<ActionIcon variant=\&#x22;light\&#x22;   color=\&#x22;blue\&#x22;  aria-label=\&#x22;Info\&#x22;><Star /></ActionIcon>\n<ActionIcon variant=\&#x22;outline\&#x22; color=\&#x22;teal\&#x22;  aria-label=\&#x22;Toggle\&#x22;><Star /></ActionIcon>\n<ActionIcon variant=\&#x22;subtle\&#x22;  color=\&#x22;neutral\&#x22;  aria-label=\&#x22;Bold\&#x22;><Bold /></ActionIcon>\n<ActionIcon variant=\&#x22;filled\&#x22;  color=\&#x22;red\&#x22;   aria-label=\&#x22;Delete\&#x22;><Trash2 /></ActionIcon>&#x22;">
  <div className="flex flex-col gap-3">
    <div className="flex flex-wrap gap-2">
      {COLORS.map((c) => (
              <ActionIcon key={c} variant="filled" color={c} aria-label={c}>
                <Star />
              </ActionIcon>
            ))}
    </div>

    <div className="flex flex-wrap gap-2">
      {COLORS.map((c) => (
              <ActionIcon key={c} variant="light" color={c} aria-label={c}>
                <Star />
              </ActionIcon>
            ))}
    </div>

    <div className="flex flex-wrap gap-2">
      {COLORS.map((c) => (
              <ActionIcon key={c} variant="outline" color={c} aria-label={c}>
                <Star />
              </ActionIcon>
            ))}
    </div>

    <div className="flex flex-wrap gap-2">
      {COLORS.map((c) => (
              <ActionIcon key={c} variant="subtle" color={c} aria-label={c}>
                <Star />
              </ActionIcon>
            ))}
    </div>
  </div>
</Preview>

For destructive triggers, use `variant="filled" color="red"` — there is no separate `destructive` variant.

## Sizes [#sizes]

Five sizes from `xs` (compact toolbar) to `xl` (hero CTA). Each is square, with the icon scaling via `1em` against the size's font-size.

<Preview align="center" code="&#x22;<ActionIcon size=\&#x22;xs\&#x22; aria-label=\&#x22;Star\&#x22;><Star /></ActionIcon>\n<ActionIcon size=\&#x22;sm\&#x22; aria-label=\&#x22;Star\&#x22;><Star /></ActionIcon>\n<ActionIcon size=\&#x22;md\&#x22; aria-label=\&#x22;Star\&#x22;><Star /></ActionIcon>\n<ActionIcon size=\&#x22;lg\&#x22; aria-label=\&#x22;Star\&#x22;><Star /></ActionIcon>\n<ActionIcon size=\&#x22;xl\&#x22; aria-label=\&#x22;Star\&#x22;><Star /></ActionIcon>&#x22;">
  <div className="flex flex-wrap items-end gap-3">
    <ActionIcon size="xs" aria-label="XS">
      <Star />
    </ActionIcon>

    <ActionIcon size="sm" aria-label="SM">
      <Star />
    </ActionIcon>

    <ActionIcon size="md" aria-label="MD">
      <Star />
    </ActionIcon>

    <ActionIcon size="lg" aria-label="LG">
      <Star />
    </ActionIcon>

    <ActionIcon size="xl" aria-label="XL">
      <Star />
    </ActionIcon>
  </div>
</Preview>

## Radius [#radius]

Independent radius scale, including `full` for circular triggers — perfect for FABs, like buttons, and avatar-adjacent controls.

<Preview align="center" code="&#x22;<ActionIcon radius=\&#x22;xs\&#x22; aria-label=\&#x22;Radius xs\&#x22;><Star /></ActionIcon>\n<ActionIcon radius=\&#x22;sm\&#x22; aria-label=\&#x22;Radius sm\&#x22;><Star /></ActionIcon>\n<ActionIcon radius=\&#x22;md\&#x22; aria-label=\&#x22;Radius md\&#x22;><Star /></ActionIcon>\n<ActionIcon radius=\&#x22;lg\&#x22; aria-label=\&#x22;Radius lg\&#x22;><Star /></ActionIcon>\n<ActionIcon radius=\&#x22;xl\&#x22; aria-label=\&#x22;Radius xl\&#x22;><Star /></ActionIcon>\n<ActionIcon radius=\&#x22;full\&#x22; aria-label=\&#x22;Radius full\&#x22;><Plus /></ActionIcon>&#x22;">
  <div className="flex flex-wrap items-center gap-3">
    <ActionIcon radius="xs" aria-label="Radius xs">
      <Star />
    </ActionIcon>

    <ActionIcon radius="sm" aria-label="Radius sm">
      <Star />
    </ActionIcon>

    <ActionIcon radius="md" aria-label="Radius md">
      <Star />
    </ActionIcon>

    <ActionIcon radius="lg" aria-label="Radius lg">
      <Star />
    </ActionIcon>

    <ActionIcon radius="xl" aria-label="Radius xl">
      <Star />
    </ActionIcon>

    <ActionIcon radius="full" aria-label="Radius full">
      <Plus />
    </ActionIcon>
  </div>
</Preview>

## Loading [#loading]

The icon is swapped for an inline spinner, the element is marked `aria-busy="true"`, and pointer events are suppressed. The accessible name still comes from `aria-label` — screen readers will announce both the label and the busy state.

<Preview align="center" code="&#x22;import { Star } from 'lucide-react';\n\nconst [pending, startTransition] = useTransition();\n\n<ActionIcon\n  color=\&#x22;brand\&#x22;\n  loading={pending}\n  aria-label=\&#x22;Star repository\&#x22;\n  onClick={() => startTransition(toggleStar)}\n>\n  <Star />\n</ActionIcon>&#x22;">
  <div className="flex flex-wrap items-center gap-3">
    <ActionIcon aria-label="Saving" />

    <ActionIcon color="teal" aria-label="Submitting" />

    <ActionIcon color="red" aria-label="Deleting" />

    <ActionIcon variant="subtle" color="neutral" radius="full" aria-label="Refreshing" />
  </div>
</Preview>

## Toolbar pattern [#toolbar-pattern]

A row of `subtle` `ActionIcon`s with `radius="md"` is the workhorse for editor toolbars and table row actions.

<Preview align="center" code="&#x22;import { Bold, Code, Italic, Link2, Underline } from 'lucide-react';\n\n<div role=\&#x22;toolbar\&#x22; aria-label=\&#x22;Format\&#x22;>\n  <ActionIcon variant=\&#x22;subtle\&#x22; color=\&#x22;neutral\&#x22; size=\&#x22;sm\&#x22; aria-label=\&#x22;Bold\&#x22;><Bold /></ActionIcon>\n  <ActionIcon variant=\&#x22;subtle\&#x22; color=\&#x22;neutral\&#x22; size=\&#x22;sm\&#x22; aria-label=\&#x22;Italic\&#x22;><Italic /></ActionIcon>\n  <ActionIcon variant=\&#x22;subtle\&#x22; color=\&#x22;neutral\&#x22; size=\&#x22;sm\&#x22; aria-label=\&#x22;Underline\&#x22;><Underline /></ActionIcon>\n</div>&#x22;">
  <div className="inline-flex items-center gap-1 rounded-lg border border-fd-border bg-fd-card p-1" role="toolbar" aria-label="Format">
    <ActionIcon variant="subtle" color="neutral" size="sm" aria-label="Bold">
      <Bold />
    </ActionIcon>

    <ActionIcon variant="subtle" color="neutral" size="sm" aria-label="Italic">
      <Italic />
    </ActionIcon>

    <ActionIcon variant="subtle" color="neutral" size="sm" aria-label="Underline">
      <Underline />
    </ActionIcon>

    <span className="mx-1 h-4 w-px bg-fd-border" />

    <ActionIcon variant="subtle" color="neutral" size="sm" aria-label="Link">
      <Link2 />
    </ActionIcon>

    <ActionIcon variant="subtle" color="neutral" size="sm" aria-label="Code">
      <Code />
    </ActionIcon>
  </div>
</Preview>

## Polymorphic with `asChild` [#polymorphic-with-aschild]

Same Ark UI polymorphism as `Button`. Wrap a link to keep client-side navigation while inheriting the icon-button styling.

```tsx
import Link from "next/link";
import { User } from "lucide-react";

<ActionIcon asChild variant="subtle" color="neutral" aria-label="Open profile">
  <Link href="/profile">
    <User />
  </Link>
</ActionIcon>;
```

<Preview align="center" code="&#x22;<ActionIcon asChild variant=\&#x22;subtle\&#x22; color=\&#x22;neutral\&#x22; aria-label=\&#x22;Open profile\&#x22;>\n  <a href=\&#x22;/profile\&#x22;><User /></a>\n</ActionIcon>\n<ActionIcon asChild color=\&#x22;teal\&#x22; loading aria-label=\&#x22;Star repository\&#x22;>\n  <a href=\&#x22;#\&#x22;><Star /></a>\n</ActionIcon>&#x22;">
  <div className="flex flex-wrap items-center gap-3">
    <ActionIcon variant="subtle" color="neutral" aria-label="Open profile">
      <a href="#">
        <User />
      </a>
    </ActionIcon>

    <ActionIcon color="teal" aria-label="Star repository">
      <a href="#">
        <Star />
      </a>
    </ActionIcon>
  </div>
</Preview>

`loading` swaps the icon for a spinner here too — Ark's `asChild` only lets a single element through, so the spinner replaces the rendered child's own children rather than the child itself, same as `Button`'s `startSlot`/`endSlot`. The click stays blocked while loading, same as everywhere else.

## API [#api]

<TypeTable
  type="{
  variant: {
    type: &#x22;'filled' | 'light' | 'outline' | 'subtle' | 'default'&#x22;,
    default: &#x22;'filled'&#x22;,
    description: &#x22;Visual treatment. 'subtle' is the toolbar workhorse.&#x22;,
  },
  color: {
    type: &#x22;'brand' | 'neutral' | 'red' | 'orange' | 'yellow' | 'green' | 'teal' | 'cyan' | 'blue' | 'purple' | 'pink' | (string & {})&#x22;,
    default: &#x22;'brand'&#x22;,
    description:
      'Accent palette. Custom values are accepted as long as a matching [data-color=&#x22;…&#x22;] block exists in theme.css.',
  },
  size: {
    type: &#x22;'xs' | 'sm' | 'md' | 'lg' | 'xl'&#x22;,
    default: &#x22;'md'&#x22;,
    description: &#x22;Square dimensions. Icon scales via 1em.&#x22;,
  },
  radius: {
    type: &#x22;'sm' | 'md' | 'lg' | 'full'&#x22;,
    default: &#x22;'md'&#x22;,
    description: &#x22;Corner radius. Use 'full' for circular triggers.&#x22;,
  },
  loading: {
    type: &#x22;boolean&#x22;,
    default: &#x22;false&#x22;,
    description:
      &#x22;Replace icon with spinner, disable interaction, set aria-busy.&#x22;,
  },
  disabled: {
    type: &#x22;boolean&#x22;,
    default: &#x22;false&#x22;,
    description: &#x22;Native disabled. Also coerced to true when loading.&#x22;,
  },
  asChild: {
    type: &#x22;boolean&#x22;,
    default: &#x22;false&#x22;,
    description: &#x22;Render children as the root element (Ark UI polymorphism).&#x22;,
  },
  type: {
    type: &#x22;'button' | 'submit' | 'reset'&#x22;,
    default: &#x22;'button'&#x22;,
    description:
      &#x22;Native button type. Defaults to 'button' to avoid accidental submits.&#x22;,
  },
  &#x22;aria-label&#x22;: {
    type: &#x22;string&#x22;,
    required: true,
    description:
      &#x22;Accessible name. Required because the icon children are decoration.&#x22;,
  },
}"
/>

All other props are forwarded to the underlying `button` (or `asChild` element).

## When to use which [#when-to-use-which]

* Reach for [`Button`](/docs/button) when the trigger has a visible text label, with or without supporting icons.
* Reach for `ActionIcon` when the trigger is icon-only and the meaning lives in `aria-label` + tooltip.
* For an icon **plus** a text label, use `Button` with `startSlot` / `endSlot` — not `ActionIcon`.
