IconButton
A button whose only content is an icon. label is a required prop and the type system enforces it.
Playground
Change a prop and watch it. The code below follows along.
Live
Open full cardIconButton — Variants, sizes and states.
Install and import
IconButton is in @dovetail-ds/react. Install it with React, if you haven't:
npm install @dovetail-ds/react react react-dom
import { IconButton } from "@dovetail-ds/react";
Import @dovetail-ds/react/styles.css once at your app's root, then your theme after it. Setup covers the stylesheets, fonts and dark mode.
Guidelines
A button whose only content is an icon. label is a required prop and the type system enforces it.
Use it when
- Space is genuinely tight and the icon is unambiguous: close, search, more, edit, delete.
Don't use it when
- The icon needs explaining. If you would add a caption, use a
ButtonwithiconStart. - It is the primary action in a form or dialog. Those get words.
Example
<IconButton label="Close dialog" onClick={close}>
<X size={20} />
</IconButton>
Variants
ghost (default) for toolbars and table rows. solid for a floating or primary action.
Accessibility
label becomes both aria-label and the native tooltip. On touch surfaces use size="md" or larger: xs and sm fall below the 44px target and need a padded hit area around them.
Content
Label is a verb phrase naming the action and its object: "Close dialog", not "Close" or "X".
Props
The typed contract, from IconButton.d.ts.
import * as React from "react";
/** Icon-only button. The accessible label is required, not optional. */
export interface IconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
/** Accessible name and tooltip. Required — an unlabelled icon button is a bug. */
label: string;
/** @default "ghost" */
variant?: "ghost" | "solid";
/** @default "md" */
size?: "xs" | "sm" | "md" | "lg";
disabled?: boolean;
/** A single icon, 20px, currentColor. */
children: React.ReactNode;
}
export declare function IconButton(props: IconButtonProps): React.JSX.Element;
Tokens it reads
Every custom property IconButton.jsx resolves, read from the source: 12 component, 4 semantic. Component tier first, because that is the one to reach for.
- component Its own tier. Override one of these and only this component moves.
- semantic Shared roles. Override one and everything using that role moves with it.
| Token | Tier | Declared as |
|---|---|---|
--dt-button-disabled-bg | component | var(--dt-surface-action-disabled) |
--dt-button-disabled-fg | component | var(--dt-text-on-action-disabled) |
--dt-button-ghost-bg | component | var(--dt-surface-action-ghost) |
--dt-button-ghost-bg-active | component | var(--dt-surface-action-ghost-active) |
--dt-button-ghost-bg-hover | component | var(--dt-surface-action-ghost-hover) |
--dt-button-ghost-fg | component | var(--dt-text-on-action-ghost) |
--dt-button-primary-bg | component | var(--dt-surface-action) |
--dt-button-primary-bg-active | component | var(--dt-surface-action-active) |
--dt-button-primary-bg-hover | component | var(--dt-surface-action-hover) |
--dt-button-primary-fg | component | var(--dt-text-on-action) |
--dt-button-radius | component | var(--dt-radius-pill) |
--dt-button-transition | component | var(--dt-motion-micro) |
--dt-size-control-lg | semantic | var(--dt-dim-12) |
--dt-size-control-md | semantic | var(--dt-dim-10) |
--dt-size-control-sm | semantic | var(--dt-dim-8) |
--dt-size-control-xs | semantic | var(--dt-dim-6) |
Source
React only, no dependencies, every value a token.
import React from "react";
export function IconButton({ label, variant = "ghost", size = "md", disabled = false, children, style, ...rest }) {
const [state, setState] = React.useState("idle");
const solid = variant === "solid";
const bg = disabled
? "var(--dt-button-disabled-bg)"
: solid
? `var(--dt-button-primary-bg${state === "active" ? "-active" : state === "hover" ? "-hover" : ""})`
: `var(--dt-button-ghost-bg${state === "active" ? "-active" : state === "hover" ? "-hover" : ""})`;
return (
<button
type="button"
aria-label={label}
title={label}
disabled={disabled}
onMouseEnter={() => !disabled && setState("hover")}
onMouseLeave={() => setState("idle")}
onMouseDown={() => !disabled && setState("active")}
onMouseUp={() => !disabled && setState("hover")}
style={{
display: "inline-flex", alignItems: "center", justifyContent: "center",
width: `var(--dt-size-control-${size})`, height: `var(--dt-size-control-${size})`,
padding: 0, border: 0, borderRadius: "var(--dt-button-radius)",
background: bg,
color: disabled ? "var(--dt-button-disabled-fg)" : solid ? "var(--dt-button-primary-fg)" : "var(--dt-button-ghost-fg)",
cursor: disabled ? "not-allowed" : "pointer",
transition: "background var(--dt-button-transition)",
...style,
}}
{...rest}
>
{children}
</button>
);
}
In context
IconButton also appears on the actions reference cards, alongside the rest of its family.