|Kit

ActionIcon

Polymorphic

Square, icon-only sibling of Button. Same (variant × color) model plus its own radius scale.

ActionIcon is the icon-only counterpart to 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.

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.

aria-label is required. The icon children are decoration — screen readers won't announce them. Without a label, the trigger is effectively invisible to assistive tech.

Playground

Component

Loading preview

Props

Code

<Component
  variant="default"
  color="brand"
  size="md"
/>

Variants

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

Code

import { Star } from 'lucide-react';

<ActionIcon variant="filled"  aria-label="Star"><Star /></ActionIcon>
<ActionIcon variant="light"   aria-label="Star"><Star /></ActionIcon>
<ActionIcon variant="outline" aria-label="Star"><Star /></ActionIcon>
<ActionIcon variant="subtle"  aria-label="Star"><Star /></ActionIcon>
<ActionIcon variant="default" aria-label="Star"><Star /></ActionIcon>

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

Colors

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

Code

<ActionIcon variant="filled"  color="brand" aria-label="Favorite"><Heart /></ActionIcon>
<ActionIcon variant="light"   color="blue"  aria-label="Info"><Star /></ActionIcon>
<ActionIcon variant="outline" color="teal"  aria-label="Toggle"><Star /></ActionIcon>
<ActionIcon variant="subtle"  color="neutral"  aria-label="Bold"><Bold /></ActionIcon>
<ActionIcon variant="filled"  color="red"   aria-label="Delete"><Trash2 /></ActionIcon>

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

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.

Code

<ActionIcon size="xs" aria-label="Star"><Star /></ActionIcon>
<ActionIcon size="sm" aria-label="Star"><Star /></ActionIcon>
<ActionIcon size="md" aria-label="Star"><Star /></ActionIcon>
<ActionIcon size="lg" aria-label="Star"><Star /></ActionIcon>
<ActionIcon size="xl" aria-label="Star"><Star /></ActionIcon>

Radius

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

Code

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

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.

Code

import { Star } from 'lucide-react';

const [pending, startTransition] = useTransition();

<ActionIcon
  color="brand"
  loading={pending}
  aria-label="Star repository"
  onClick={() => startTransition(toggleStar)}
>
  <Star />
</ActionIcon>

Toolbar pattern

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

Code

import { Bold, Code, Italic, Link2, Underline } from 'lucide-react';

<div 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>
</div>

Polymorphic with asChild

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

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

Code

<ActionIcon asChild variant="subtle" color="neutral" aria-label="Open profile">
  <a href="/profile"><User /></a>
</ActionIcon>
<ActionIcon asChild color="teal" loading aria-label="Star repository">
  <a href="#"><Star /></a>
</ActionIcon>

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

Prop

Type

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

When to use which

  • Reach for 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.

Changelog

v0.19.0
  • Spinner's accessible name comes from label, and label={null} takes it out of the accessibility tree, so loading Buttons and ActionIcons no longer expose an extra "Spinner" image

    #319
v0.6.0
  • With asChild, slots and the loading spinner stay visible, and the spinner dims correctly

    #79
v0.5.0
  • Match Button's typography and light variant border

    #58
v0.3.0
  • isLoading → loading on Button and ActionIcon

    #25
v0.0.1
  • ActionIcon: the icon-only button

On this page