Component
Icon Button
A square icon-only control. No visible label, so an accessible name is required. Matches control height; the icon slot is square (width equals height).
- Component
- React
- TSX
- Tailwind CSS
- MIT
Preview
Live component
9:41 100%
devsnips.dev/library/react/components/buttons/icon-button/ fluid · no fixed width 100%
Install
Add to your project
npx devsnips add React/Components/Buttons/icon-button React/Components/Buttons/icon-button import type { ButtonHTMLAttributes, ReactNode } from "react";
/* DevSnips React — IconButton
* Square icon-only control. `label` is required for an accessible name
* (rendered as aria-label). Maintains the size token's height; the --icon
* modifier makes the button square.
*/
export type ButtonSize = "xs" | "sm" | "md" | "lg" | "xl";
export type IconButtonVariant = "ghost" | "outline" | "secondary" | "solid";
export interface IconButtonProps
extends ButtonHTMLAttributes<HTMLButtonElement> {
/** Icon node (rendered at the size token for the chosen button size). */
icon: ReactNode;
/** Required accessible name (rendered as aria-label). */
label: string;
variant?: IconButtonVariant;
size?: ButtonSize;
/** Pressed / selected state. Sets aria-pressed. */
active?: boolean;
}
function cx(...parts: Array<string | false | null | undefined>): string {
return parts.filter(Boolean).join(" ");
}
const SIZES: Record<ButtonSize, string> = {
xs: "h-7 gap-1 px-2 text-xs [&_svg]:size-[14px]",
sm: "h-8 gap-1.5 px-3 text-xs [&_svg]:size-[14px]",
md: "h-9 gap-2 px-3.5 text-[13px] [&_svg]:size-4",
lg: "h-10 gap-2 px-4 text-[13px] [&_svg]:size-[18px]",
xl: "h-11 gap-2 px-5 text-sm [&_svg]:size-5",
};
const VARIANTS: Record<IconButtonVariant, string> = {
ghost: "border-transparent bg-transparent text-[var(--ds-color-foreground)] hover:bg-[var(--ds-color-surface-hover)] active:bg-[var(--ds-color-surface-active)]",
outline: "border-[var(--ds-color-border-strong)] bg-transparent text-[var(--ds-color-foreground)] hover:bg-[var(--ds-color-surface-hover)] active:bg-[var(--ds-color-surface-active)]",
secondary: "border-[var(--ds-color-border)] bg-[var(--ds-color-secondary)] text-[var(--ds-color-secondary-foreground)] hover:bg-[var(--ds-color-surface-active)] active:bg-[var(--ds-color-surface-active)]",
solid: "border-transparent bg-[var(--ds-color-primary)] text-[var(--ds-color-primary-foreground)] hover:bg-[color-mix(in_srgb,var(--ds-color-primary)_88%,#000)] active:bg-[color-mix(in_srgb,var(--ds-color-primary)_80%,#000)]",
};
const ICON_ONLY: Record<ButtonSize, string> = {
xs: "h-7 w-7 px-0 [&_svg]:size-[14px]",
sm: "h-8 w-8 px-0 [&_svg]:size-[14px]",
md: "h-9 w-9 px-0 [&_svg]:size-4",
lg: "h-10 w-10 px-0 [&_svg]:size-[18px]",
xl: "h-11 w-11 px-0 [&_svg]:size-5",
};
export function IconButton({
icon,
label,
variant = "ghost",
size = "md",
active = false,
disabled,
className,
type = "button",
...rest
}: IconButtonProps) {
return (
<button
type={type}
aria-label={label}
aria-pressed={active || undefined}
className={cx(
"inline-flex select-none items-center justify-center whitespace-nowrap rounded-[var(--ds-radius-sm)] border font-medium leading-none transition-colors duration-150 ease-out motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ds-color-focus-ring)] disabled:pointer-events-none disabled:opacity-50",
VARIANTS[variant],
active && "bg-[var(--ds-color-surface-active)]",
ICON_ONLY[size],
className,
)}
disabled={disabled}
{...rest}
>
{icon}
</button>
);
}
export default IconButton; /* DevSnips React — JavaScript parity build.
* Same API, behavior, and classes as code.tsx; TypeScript types removed.
* Regenerated from code.tsx — edit code.tsx and re-run the generator.
*/
function cx(...parts) {
return parts.filter(Boolean).join(" ");
}
const SIZES = {
xs: "h-7 gap-1 px-2 text-xs [&_svg]:size-[14px]",
sm: "h-8 gap-1.5 px-3 text-xs [&_svg]:size-[14px]",
md: "h-9 gap-2 px-3.5 text-[13px] [&_svg]:size-4",
lg: "h-10 gap-2 px-4 text-[13px] [&_svg]:size-[18px]",
xl: "h-11 gap-2 px-5 text-sm [&_svg]:size-5"
};
const VARIANTS = {
ghost: "border-transparent bg-transparent text-[var(--ds-color-foreground)] hover:bg-[var(--ds-color-surface-hover)] active:bg-[var(--ds-color-surface-active)]",
outline: "border-[var(--ds-color-border-strong)] bg-transparent text-[var(--ds-color-foreground)] hover:bg-[var(--ds-color-surface-hover)] active:bg-[var(--ds-color-surface-active)]",
secondary: "border-[var(--ds-color-border)] bg-[var(--ds-color-secondary)] text-[var(--ds-color-secondary-foreground)] hover:bg-[var(--ds-color-surface-active)] active:bg-[var(--ds-color-surface-active)]",
solid: "border-transparent bg-[var(--ds-color-primary)] text-[var(--ds-color-primary-foreground)] hover:bg-[color-mix(in_srgb,var(--ds-color-primary)_88%,#000)] active:bg-[color-mix(in_srgb,var(--ds-color-primary)_80%,#000)]"
};
const ICON_ONLY = {
xs: "h-7 w-7 px-0 [&_svg]:size-[14px]",
sm: "h-8 w-8 px-0 [&_svg]:size-[14px]",
md: "h-9 w-9 px-0 [&_svg]:size-4",
lg: "h-10 w-10 px-0 [&_svg]:size-[18px]",
xl: "h-11 w-11 px-0 [&_svg]:size-5"
};
export function IconButton({
icon,
label,
variant = "ghost",
size = "md",
active = false,
disabled,
className,
type = "button",
...rest
}) {
return <button
type={type}
aria-label={label}
aria-pressed={active || undefined}
className={cx(
"inline-flex select-none items-center justify-center whitespace-nowrap rounded-[var(--ds-radius-sm)] border font-medium leading-none transition-colors duration-150 ease-out motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ds-color-focus-ring)] disabled:pointer-events-none disabled:opacity-50",
VARIANTS[variant],
active && "bg-[var(--ds-color-surface-active)]",
ICON_ONLY[size],
className
)}
disabled={disabled}
{...rest}
>
{icon}
</button>;
}
export default IconButton; # Icon Button
A square icon-only control. No visible label, so an accessible name is required. Matches control height; the icon slot is square (width equals height).
## Installation
This component requires **React** and **Tailwind CSS**. Drop `code.tsx` (or `code.jsx` for JavaScript projects) into your project. Tailwind utility classes are included directly in the component, so no separate CSS file is required.
The component consumes the DevSnips semantic design tokens through Tailwind arbitrary values (for example `bg-[var(--ds-color-primary)]`). Define the `--ds-*` tokens once in your theme — see [React/DESIGN_TOKENS.md](../../../DESIGN_TOKENS.md) for the full token spec.
## Usage
```tsx
<IconButton icon={<Trash className="shrink-0" />} label="Delete row" />
```
## JavaScript
A `code.jsx` build is provided for projects that ship plain JSX. It exposes the same API and behavior as `code.tsx` — only the TypeScript types are removed.
## Props
| Prop | Type | Default |
|---|---|---|
| `icon` | `ReactNode` | — (required) |
| `label` | `string` | — (required: accessible name) |
| `variant` | `ghost \| outline \| secondary \| solid` | `ghost` |
| `size` | `ButtonSize` | `md` |
| `active` | `boolean` | `false` |
| `disabled` | `boolean` | `false` |
Plus all native `ButtonHTMLAttributes<HTMLButtonElement>`. `label` is always rendered as `aria-label`.
## Variants
ghost (default) · outline · secondary · solid. Same emphasis semantics as the labeled variants.
## Sizes
xs (28px) · sm (32px) · **md (36px, default)** · lg (40px) · xl (44px). Horizontal padding scales 8 → 20px; icons scale 14 → 20px. Icon-only: the button is square (`w` == `h`); padding is removed.
## States
default · hover · active · focus-visible · selected (`active` → `aria-pressed` + `surface-active`) · disabled (reduced opacity).
## Accessibility
**Icon-only buttons must have an accessible name.** `label` is required and renders as `aria-label`. Renders a native `<button>`; focus-visible ring uses `color.focus-ring`. Meets 44px touch target at lg/xl. Never omit `label` — a button with no text and no aria-label is unnamed to screen readers.
## Styling
Tailwind classes are included directly in the component and consume the DevSnips semantic design tokens (`--ds-*`) via arbitrary values. The button themes with the surface automatically in light and dark mode. No component-specific CSS file is needed.
## Design Tokens
See [React/DESIGN_TOKENS.md](../../../DESIGN_TOKENS.md) for the authoritative token specification. This button uses the semantic color, radius, and motion tokens; define them once in your project theme and every button in the family stays in sync.
## Notes
Use where a label would be redundant given surrounding context (toolbar, card header, table row). When space allows, prefer a labeled button — it's more discoverable. 82 lines UTF-8 · LF · Spaces: 2
Continue browsing