Propel

Icon Button

An icon-only button for compact, labeled actions like adding, editing, or closing.

An aria-label is required: icon-only buttons have no visible text, so the accessible name comes from it.

Show code
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { Plus } from "lucide-react";

export default function BasicDemo() {
  return (
    <IconButton variant="primary" size="sm" aria-label="Add item" icon={<Icon icon={Plus} />} />
  );
}

Installation

import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";

Usage

import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { Plus } from "lucide-react";

export default function BasicDemo() {
  return (
    <IconButton variant="primary" size="sm" aria-label="Add item" icon={<Icon icon={Plus} />} />
  );
}

Examples

Variants

Variant sets the visual weight and palette: primary, secondary, tertiary, and ghost, plus danger (solid fill) and danger-outline (bordered outline) for destructive actions.

Show code
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { Plus, Trash2 } from "lucide-react";

export default function VariantsDemo() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      <IconButton variant="primary" size="sm" aria-label="Add member" icon={<Icon icon={Plus} />} />
      <IconButton
        variant="secondary"
        size="sm"
        aria-label="Add member"
        icon={<Icon icon={Plus} />}
      />
      <IconButton
        variant="tertiary"
        size="sm"
        aria-label="Add member"
        icon={<Icon icon={Plus} />}
      />
      <IconButton variant="ghost" size="sm" aria-label="Add member" icon={<Icon icon={Plus} />} />
      <IconButton
        variant="danger"
        size="sm"
        aria-label="Delete project"
        icon={<Icon icon={Trash2} />}
      />
      <IconButton
        variant="danger-outline"
        size="sm"
        aria-label="Delete project"
        icon={<Icon icon={Trash2} />}
      />
    </div>
  );
}

Sizes

size sets a square box — xs 20 / sm 24 / md 28 / lg 32 px. The glyph runs one step larger than a labelled control at the same rung (14 / 16 / 16 / 20 px), because there is no label to balance it. xs is under WCAG 2.5.8’s 24px floor and ships a transparent ring that brings the pointer target to 24×24 without moving a painted pixel — which is also why it is unavailable inside contiguous groups, where adjacent rings would intersect.

Show code
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { Plus } from "lucide-react";

export default function SizesDemo() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      <IconButton
        variant="secondary"
        size="xs"
        aria-label="Add label"
        icon={<Icon icon={Plus} />}
      />
      <IconButton
        variant="secondary"
        size="sm"
        aria-label="Add label"
        icon={<Icon icon={Plus} />}
      />
      <IconButton
        variant="secondary"
        size="md"
        aria-label="Add label"
        icon={<Icon icon={Plus} />}
      />
      <IconButton
        variant="secondary"
        size="lg"
        aria-label="Add label"
        icon={<Icon icon={Plus} />}
      />
    </div>
  );
}

Loading

The loading state shows a spinner, sets aria-busy, and blocks interaction.

Show code
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { RefreshCw } from "lucide-react";

export default function LoadingDemo() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      <IconButton
        variant="primary"
        size="sm"
        aria-label="Syncing workspace"
        loading
        icon={<Icon icon={RefreshCw} />}
      />
      <IconButton
        variant="secondary"
        size="sm"
        aria-label="Syncing workspace"
        loading
        icon={<Icon icon={RefreshCw} />}
      />
      <IconButton
        variant="tertiary"
        size="sm"
        aria-label="Syncing workspace"
        loading
        icon={<Icon icon={RefreshCw} />}
      />
    </div>
  );
}

Disabled

disabled is the hard, non-focusable disabled state; unlike loading, it removes the button from the tab order.

Show code
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { Plus, Trash2 } from "lucide-react";

export default function DisabledDemo() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      <IconButton
        variant="primary"
        size="sm"
        aria-label="Add item"
        disabled
        icon={<Icon icon={Plus} />}
      />
      <IconButton
        variant="danger-outline"
        size="sm"
        aria-label="Delete item"
        disabled
        icon={<Icon icon={Trash2} />}
      />
    </div>
  );
}

API Reference

IconButton

The ready-made icon-only button: grafts Base UI's `Button` behavior onto the square `IconButton` box, filling it with the provided icon element and swapping in a spinner while `loading`. It can render as `<a>` via `nativeButton={false}` + `render={<a href=… />}` when needed. An `aria-label` is REQUIRED for the accessible name.

PropTypeDefaultDescription
variant(required)"primary" | "secondary" | "tertiary" | "ghost" | "danger" | "danger-outline"
size(required)"xs" | "sm" | "md" | "lg"
icon(required)ReactNodeThe single icon element to render, usually `<Icon icon={...} />`. The accessible name comes from `aria-label`.
disabledbooleanHard, non-focusable native disabled state (`loading` stays focusable instead).
type"button" | "submit" | "reset"buttonThe button's form behavior.
aria-labelstringDefines a string value that labels the current element. Required: icon-only buttons have no visible text, so they must be labeled. @see aria-labelledby.
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, {}>Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render.
nativeButtonbooleanSet `false` when `render` swaps the underlying tag away from `<button>` (e.g. an `<a>`): Base UI then adds `role`, tab focus, and Enter/Space activation as appropriate.
loadingbooleanfalseShows a spinner in place of the icon, sets `aria-busy`, and makes the button non-interactive.