Icon Button

Square icon-only button. Automatically wraps in a Tooltip when aria-label is provided.

API Reference

PropTypeDefault
variant"solid" | "outlined" | "ghost" | "minimal""solid"
color"root" | "primary" | "secondary" | "accent""root"
size"sm" | "md" | "lg""md"
aria-labelstring
tooltipSide"top" | "bottom" | "left" | "right""top"
loadingbooleanfalse
disabledbooleanfalse

Variants & Colors

Solid

default
<IconButton variant="solid" color="root" aria-label="Menu">
  <MenuIcon />
</IconButton>
<IconButton variant="solid" color="primary" aria-label="Share">
  <ShareIcon />
</IconButton>
<IconButton variant="solid" color="secondary" aria-label="Theme">
  <DarkModeIcon />
</IconButton>
<IconButton variant="solid" color="accent" aria-label="Close">
  <CloseIcon />
</IconButton>

Outlined

<IconButton variant="outlined" color="root" aria-label="Menu">
  <MenuIcon />
</IconButton>
<IconButton variant="outlined" color="primary" aria-label="Share">
  <ShareIcon />
</IconButton>
<IconButton variant="outlined" color="secondary" aria-label="Theme">
  <DarkModeIcon />
</IconButton>
<IconButton variant="outlined" color="accent" aria-label="Close">
  <CloseIcon />
</IconButton>

Ghost

<IconButton variant="ghost" color="root" aria-label="Menu">
  <MenuIcon />
</IconButton>
<IconButton variant="ghost" color="primary" aria-label="Share">
  <ShareIcon />
</IconButton>
<IconButton variant="ghost" color="secondary" aria-label="Theme">
  <DarkModeIcon />
</IconButton>
<IconButton variant="ghost" color="accent" aria-label="Close">
  <CloseIcon />
</IconButton>

Minimal

<IconButton variant="minimal" color="root" aria-label="Menu">
  <MenuIcon />
</IconButton>
<IconButton variant="minimal" color="primary" aria-label="Share">
  <ShareIcon />
</IconButton>
<IconButton variant="minimal" color="secondary" aria-label="Theme">
  <DarkModeIcon />
</IconButton>
<IconButton variant="minimal" color="accent" aria-label="Close">
  <CloseIcon />
</IconButton>

Sizes

<IconButton size="sm" aria-label="Small">
  <MenuIcon />
</IconButton>
<IconButton size="md" aria-label="Medium">
  <MenuIcon />
</IconButton>
<IconButton size="lg" aria-label="Large">
  <MenuIcon />
</IconButton>

Loading

<IconButton loading aria-label="Loading">
  <MenuIcon />
</IconButton>
<IconButton variant="outlined" loading aria-label="Loading">
  <MenuIcon />
</IconButton>
<IconButton variant="ghost" loading aria-label="Loading">
  <MenuIcon />
</IconButton>

Disabled

<IconButton disabled aria-label="Disabled">
  <MenuIcon />
</IconButton>
<IconButton variant="outlined" disabled aria-label="Disabled">
  <MenuIcon />
</IconButton>
<IconButton variant="ghost" disabled aria-label="Disabled">
  <MenuIcon />
</IconButton>

Tooltip

When aria-label is provided, the button is automatically wrapped in a Tooltip. Use tooltipSide to control placement.

<IconButton variant="ghost" aria-label="Open menu">
  <MenuIcon />
</IconButton>
<IconButton variant="ghost" aria-label="Share" tooltipSide="bottom">
  <ShareIcon />
</IconButton>

Basic Usage

import { IconButton } from "@/ui";
import { MenuIcon } from "@/ui/icons";

<IconButton aria-label="Menu">
  <MenuIcon />
</IconButton>

Source

src/ui/IconButton.tsx
import { cn } from "@/utils";
import { ButtonHTMLAttributes, forwardRef } from "react";
import { Loader } from "./Loader";
import { Tooltip } from "./Tooltip";

export interface IconButtonProps extends Omit<
  ButtonHTMLAttributes<HTMLButtonElement>,
  "color"
> {
  variant?: "solid" | "outlined" | "ghost" | "minimal";
  color?: "root" | "primary" | "secondary" | "accent";
  size?: "sm" | "md" | "lg";
  loading?: boolean;
  tooltipSide?: "top" | "bottom" | "left" | "right";
}

export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
  function IconButton(
    {
      variant = "solid",
      color = "root",
      size = "md",
      disabled = false,
      loading = false,
      tooltipSide,
      className,
      children,
      ...props
    },
    ref,
  ) {
    const label = props["aria-label"];
    const isDisabled = disabled || loading;

    const button = (
      <button
        ref={ref}
        aria-busy={loading || undefined}
        className={cn(
          "focus-visible:ring-primary inline-flex cursor-pointer items-center justify-center rounded-lg transition-colors focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50",
          size === "sm" && "size-9",
          size === "md" && "size-10",
          size === "lg" && "size-11",
          // solid
          variant === "solid" && color === "root" && "bg-root-contrast text-root hover:bg-root-hover",
          variant === "solid" && color === "primary" && "bg-primary text-primary-contrast hover:bg-primary-hover",
          variant === "solid" && color === "secondary" && "bg-secondary text-secondary-contrast hover:bg-secondary-hover",
          variant === "solid" && color === "accent" && "bg-accent text-accent-contrast hover:bg-accent-hover",
          // outlined
          variant === "outlined" && color === "root" && "border border-root-contrast text-root-contrast hover:text-root hover:bg-root-contrast",
          variant === "outlined" && color === "primary" && "border border-primary text-primary hover:bg-primary hover:text-primary-contrast",
          variant === "outlined" && color === "secondary" && "border border-secondary text-secondary hover:bg-secondary hover:text-secondary-contrast",
          variant === "outlined" && color === "accent" && "border border-accent text-accent hover:bg-accent hover:text-accent-contrast",
          // ghost
          variant === "ghost" && color === "root" && "bg-root-ghost text-root-contrast hover:bg-root-ghost-hover",
          variant === "ghost" && color === "primary" && "bg-primary-ghost text-primary hover:bg-primary-ghost-hover",
          variant === "ghost" && color === "secondary" && "bg-secondary-ghost text-secondary hover:bg-secondary-ghost-hover",
          variant === "ghost" && color === "accent" && "bg-accent-ghost text-accent hover:bg-accent-ghost-hover",
          // minimal
          variant === "minimal" && color === "root" && "text-root-contrast-subtle hover:bg-root-ghost hover:text-root-contrast",
          variant === "minimal" && color === "primary" && "text-primary hover:bg-primary-ghost",
          variant === "minimal" && color === "secondary" && "text-secondary hover:bg-secondary-ghost",
          variant === "minimal" && color === "accent" && "text-accent hover:bg-accent-ghost",
          className,
        )}
        disabled={isDisabled}
        {...props}
      >
        {loading ? (
          <Loader className="size-[1em] text-current!" aria-hidden="true" />
        ) : (
          children
        )}
      </button>
    );

    return label ? <Tooltip label={label} side={tooltipSide}>{button}</Tooltip> : button;
  },
);