Icon Button

stable

IconButton is a compact native button whose visible content is an icon rather than a text label. It is intended for familiar, frequently used actions where space is limited and the icon’s meaning is clear from established conventions or surrounding context.

Because the action is not visibly named, every IconButton requires an accessible label that describes what the control does.

Variants
Sizes
Disabled
Loading (click to simulate)

Basic usage

Provide a familiar icon and a non-empty aria-label that describes the action.

---
import { IconButton } from "@ulubit/ui";
---

<IconButton aria-label="Close dialog">
  <CloseIcon />
</IconButton>

Use an ordinary Button when visible text would make the action clearer.

Accessible label

Every icon button requires a non-empty aria-label because its icon is hidden from assistive technologies.

The label must describe the action, not the icon’s appearance.

Prefer:

  • Copy link
  • Close dialog
  • Delete project
  • Open settings

Avoid:

  • Copy icon
  • X
  • Trash can
  • Button
<IconButton aria-label="Copy link">
  <CopyIcon />
</IconButton>

The accessible name remains in place during loading. Announce progress and results separately rather than replacing aria-label with a loading message.

Variants

Choose a variant based on the action’s purpose and priority.

Variant Use for
primary A high-emphasis icon action
secondary A visible alternative or supporting action
tertiary Most compact supporting actions
danger A destructive icon action

tertiary is the default because icon-only controls are usually supporting actions that should not compete with the page’s primary action.

Use a specific accessible label for danger actions, such as Delete project rather than Delete when the affected item is not otherwise clear.

IconButton uses the same variant and disabled color tokens as Button. See Button color tokens for the complete reference.

Sizes

Icon buttons support sm, md, and lg. Size controls both the square target and the icon dimensions.

Size Use for
sm Compact interfaces with sufficient spacing between controls
md Most icon-button uses
lg Spacious interfaces or intentionally larger targets

md is the default.

<IconButton aria-label="Edit item" size="sm">
  <EditIcon />
</IconButton>

<IconButton aria-label="Edit item">
  <EditIcon />
</IconButton>

<IconButton aria-label="Edit item" size="lg">
  <EditIcon />
</IconButton>

Disabled state

Use the native disabled attribute when the action is unavailable.

<IconButton aria-label="Delete project" variant="danger" disabled>
  <TrashIcon />
</IconButton>

A disabled icon button cannot be focused or activated. Provide nearby context when the reason it is unavailable would otherwise be unclear.

Loading state

IconButton includes a spinner that can be displayed while an asynchronous action is pending. The component provides the visual state, but it does not run the operation or control its lifecycle.

When the operation begins, the consuming script must:

  • Set aria-busy="true" to show the spinner.
  • Set disabled to prevent repeated activation.
  • Update a separate status message to communicate progress.

When the operation finishes or fails, remove aria-busy and restore disabled. The spinner replaces the visible icon without changing the button’s dimensions. The required aria-label stays unchanged so the control retains its accessible name.

For example, to refresh some data through an Astro Action (assuming a refreshStatus Action is defined in your project):

---
import { IconButton } from "@ulubit/ui";
---

<IconButton aria-label="Refresh status" data-refresh-status-button>
  <RefreshIcon />
</IconButton>

<p role="status" data-refresh-status-feedback></p>

<script>
  import { actions } from "astro:actions";

  const button = document.querySelector<HTMLButtonElement>(
    "[data-refresh-status-button]"
  );
  const feedback = document.querySelector<HTMLElement>(
    "[data-refresh-status-feedback]"
  );

  if (button && feedback) {
    button.addEventListener("click", async () => {
      if (button.disabled) return;

      button.disabled = true;
      button.setAttribute("aria-busy", "true");
      feedback.textContent = "Refreshing status…";

      try {
        const { error } = await actions.refreshStatus();

        if (error) {
          feedback.textContent = "Could not refresh status.";
          return;
        }

        feedback.textContent = "Status refreshed.";
      } catch {
        feedback.textContent = "Connection error. Try again.";
      } finally {
        button.removeAttribute("aria-busy");
        button.disabled = false;
      }
    });
  }
</script>

The example uses an illustrative Action name. Define the Action and provide the icon in your project. The finally block restores the button whether the Action succeeds, returns an error, or throws unexpectedly.

For ordinary form submissions, prefer a text Button with type="submit". See Button loading state for the form submission pattern.

Accessibility: The spinner is decorative. aria-busy is not a replacement for a status announcement, so use a persistent status region such as role="status" for progress and results. Disabling a focused button can also affect keyboard focus; verify that behavior in the consuming application.

Tooltips

aria-label provides an accessible name, but it does not create visible help text.

Use a text Button when the icon is not clear enough on its own. Do not add a title attribute as a substitute for a well-designed visible label or a proper Tooltip component.

Boundaries

IconButton performs an action. It does not provide:

  • navigation behavior
  • toggle or pressed-state behavior
  • automatic tooltips
  • asynchronous operation management

Use a native <a> for icon-only navigation. Use ButtonLink when the navigation control has a visible text label.

Introduce a dedicated toggle-button pattern only when an action requires a persistent pressed state.

Accessibility

IconButton renders a native <button> and retains its built-in keyboard and focus behavior. The required aria-label supplies its accessible name, while the decorative icon is hidden from assistive technologies.

The loading spinner is also decorative. The required aria-label remains on the button in every state. The shared loading styles make the icon visually transparent while preserving its dimensions. Announce progress and results through a separate status region.

API

The component accepts valid native <button> attributes except aria-labelledby. aria-label is required through the component API, and aria-busy is forwarded as a native attribute that also controls the visual spinner.

Property Type Default Description
aria-label string Required Provides the button’s accessible name.
variant "primary" | "secondary" | "tertiary" | "danger" "tertiary" Defines visual hierarchy and intent.
size "sm" | "md" | "lg" "md" Defines the square target and icon dimensions.
aria-busy boolean or "true" | "false" (native attribute) — Displays the spinner when set to "true"; does not disable the button automatically.
type "button" | "submit" | "reset" "button" Defines native button behavior.
disabled boolean false Prevents interaction with the button.
class string — Adds a class alongside the component’s internal classes.
Default slot AstroComponentFactory Required Provides the decorative icon.

Native attributes such as name, value, form, aria-describedby, aria-controls, and data-* are forwarded to the underlying <button>.