IconButton
A square button that shows only an icon. Its required label is the button's accessible name and, by default, the text of a tooltip shown on hover and keyboard focus.
Import
import { IconButton, IconButtonStatic } from '@nizivo/react/icon-button';
// Once per app (Getting started), after
// @nizivo/tokens/tokens.css and @nizivo/react/css/base.css and @nizivo/react/css/kbd.css and @nizivo/react/css/tooltip.css
import '@nizivo/react/css/icon-button.css';Static or interactive
Split. The default tooltip ships the tooltip leaf and the Tooltip code (+20,802 B gzip under
Turbopack, once per page). tooltip={false} runs no client code, but Turbopack and next dev still
ship that code, because the shell imports the leaf; IconButtonStatic, from the same import path,
imports no client module. How to choose.
In a client app (a Vite or React Router single-page app, or a 'use client' file) it is an ordinary React component: event handlers, refs and state work as usual. In a client app.
import { IconButton, IconButtonStatic } from '@nizivo/react/icon-button';
<IconButton label="Delete product" icon={<Trash />} />; // the tooltip shows "Delete product"
<IconButtonStatic aria-label="Delete product" icon={<Trash />} />; // no tooltip, no client moduleUse IconButtonStatic only where something visible already names the action for sighted users,
such as a column header; otherwise keep the tooltip. It takes no label, tooltip or shortcut
(TypeScript rejects them); pass aria-keyshortcuts yourself to announce a shortcut.
Bundle size with Turbopack
<IconButton tooltip={false}> renders its button on the server and runs no client code, and
production webpack ships none of the tooltip code for it. Turbopack and next dev ship every client
module a page's server code imports, rendered or not, so the IconButton's tooltip leaf and the Tooltip
code still reach the page. Measured on the Next fixture (gzip of the page's client chunks, a page
with one <IconButton tooltip={false}> minus a page with one Button): Turbopack production
+20,802 B, production webpack 0 B, next dev +62,742 B (unminified), the same as a page that shows
the tooltip.
IconButtonStatic avoids it: its module imports no client code, so a page that renders only
IconButtonStatic ships none of the tooltip code in any mode (the fixture's /icon-button-static
page: 0 B over the Button page under Turbopack, webpack and next dev). On a page that
already loads the Tooltip code for something else, it saves nothing. Dialog.Close and
AlertDismiss write the IconButton markup themselves, so they do not import the tooltip.
Pending
pending replaces the glyph with a spinner and keeps the button focusable with aria-disabled,
swallowing clicks. The spinner turns once every 800 ms (--nz-duration-spin). Under Reduced motion
it stops upright. A Button then shows "Loading…" after its label, but an icon-only button has no
room for text, so nothing visible says it is loading: when the wait is more than a moment, say so in
words next to it (a status line such as "Deleting…"), or use a Button with a label. This differs
from Figma's rule ("Spinner and skeleton loops stop; a text label shows") and is listed under
Deviations.
Examples
Basic
Secondary (the default), tertiary and primary. Each shows its label in a tooltip on hover and focus.
import type { JSX } from 'react';
import { Inline } from '@nizivo/react/inline';
import { IconButton } from '@nizivo/react/icon-button';
/** Stand-ins for `@nizivo/icons` glyphs (20 px Lucide strokes). */
function Bell(): JSX.Element {
return (
<svg
viewBox="0 0 24 24"
width="1em"
height="1em"
fill="none"
stroke="currentColor"
strokeWidth={1.92}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M10.3 21a1.94 1.94 0 0 0 3.4 0M3.26 15.33A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.67C19.41 13.96 18 12.5 18 8A6 6 0 0 0 6 8c0 4.5-1.41 5.96-2.74 7.33" />
</svg>
);
}
function Trash(): JSX.Element {
return (
<svg
viewBox="0 0 24 24"
width="1em"
height="1em"
fill="none"
stroke="currentColor"
strokeWidth={1.92}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
</svg>
);
}
function Plus(): JSX.Element {
return (
<svg
viewBox="0 0 24 24"
width="1em"
height="1em"
fill="none"
stroke="currentColor"
strokeWidth={1.92}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M5 12h14M12 5v14" />
</svg>
);
}
export function IconButtonExample(): JSX.Element {
return (
<Inline gap="2">
<IconButton label="Notifications" icon={<Bell />} />
<IconButton label="Delete product" icon={<Trash />} variant="tertiary" />
<IconButton label="Add product" icon={<Plus />} variant="primary" />
</Inline>
);
}
Sizes
32, 40 and 48 px. tooltip={false} renders on the server with no client code.
import type { JSX } from 'react';
import { Inline } from '@nizivo/react/inline';
import { IconButton } from '@nizivo/react/icon-button';
function Plus(): JSX.Element {
return (
<svg
viewBox="0 0 24 24"
width="1em"
height="1em"
fill="none"
stroke="currentColor"
strokeWidth={1.92}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M5 12h14M12 5v14" />
</svg>
);
}
export function IconButtonSizesExample(): JSX.Element {
return (
<Inline gap="2">
<IconButton label="Add product" icon={<Plus />} size="sm" tooltip={false} />
<IconButton label="Add product" icon={<Plus />} tooltip={false} />
<IconButton label="Add product" icon={<Plus />} size="lg" tooltip={false} />
</Inline>
);
}
States
A toggle keeps its label and reports its state with pressed (aria-pressed); pending keeps the button focusable while the action runs; a disabled button has no tooltip.
import type { JSX } from 'react';
import { Inline } from '@nizivo/react/inline';
import { IconButton } from '@nizivo/react/icon-button';
function Trash(): JSX.Element {
return (
<svg
viewBox="0 0 24 24"
width="1em"
height="1em"
fill="none"
stroke="currentColor"
strokeWidth={1.92}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
</svg>
);
}
function Bold(): JSX.Element {
return (
<svg
viewBox="0 0 24 24"
width="1em"
height="1em"
fill="none"
stroke="currentColor"
strokeWidth={1.92}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8" />
</svg>
);
}
export function IconButtonStatesExample(): JSX.Element {
return (
<Inline gap="2">
<IconButton label="Bold" icon={<Bold />} variant="tertiary" pressed={false} />
<IconButton label="Bold" icon={<Bold />} variant="tertiary" pressed />
<IconButton label="Delete product" icon={<Trash />} variant="critical" pending />
<IconButton label="Delete product" icon={<Trash />} disabled />
</Inline>
);
}
Shortcut
The shortcut shows in the tooltip after the label: ⌘B on Apple devices, Ctrl+B elsewhere.
import type { JSX } from 'react';
import { IconButton } from '@nizivo/react/icon-button';
function Bold(): JSX.Element {
return (
<svg
viewBox="0 0 24 24"
width="1em"
height="1em"
fill="none"
stroke="currentColor"
strokeWidth={1.92}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8" />
</svg>
);
}
export function IconButtonShortcutExample(): JSX.Element {
return (
<IconButton
label="Bold"
icon={<Bold />}
variant="tertiary"
shortcut="mod+b"
tooltip={{ side: 'bottom' }}
/>
);
}
Static
IconButtonStatic in a Server Component: no tooltip and no client code on the page. The name is aria-label. The second renders as a link, as a row action in a server-rendered table might.
import type { ComponentPropsWithRef, JSX } from 'react';
import { Inline } from '@nizivo/react/inline';
import { IconButtonStatic } from '@nizivo/react/icon-button';
function Trash(): JSX.Element {
return (
<svg
viewBox="0 0 24 24"
width="1em"
height="1em"
fill="none"
stroke="currentColor"
strokeWidth={1.92}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
</svg>
);
}
function Plus(): JSX.Element {
return (
<svg
viewBox="0 0 24 24"
width="1em"
height="1em"
fill="none"
stroke="currentColor"
strokeWidth={1.92}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M5 12h14M12 5v14" />
</svg>
);
}
/** Stands in for a router link (Next `Link`, React Router `Link`). */
function Link({ children, ...props }: ComponentPropsWithRef<'a'>): JSX.Element {
return <a {...props}>{children}</a>;
}
export function IconButtonStaticExample(): JSX.Element {
return (
<Inline gap="2">
<IconButtonStatic aria-label="Add product" icon={<Plus />} variant="primary" />
<IconButtonStatic
aria-label="Delete product"
icon={<Trash />}
variant="tertiary"
render={<Link href="/products/42/delete" />}
/>
</Inline>
);
}
Props
IconButton
Server component Renders data-slot="icon-button" with the class nz-icon-button. Props: IconButtonProps. Source: src/icon-button/icon-button.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
iconrequired | ReactNode | – | The glyph, decorative ( |
labelrequired | string | – | The action, in words: the button's accessible name ( |
pending | boolean | false | An action is in flight: the spinner replaces the glyph, and the button stays focusable with |
pendingIndicator | ReactNode | – | Replaces the internal spinner glyph. |
pressed | boolean | – | Makes it a toggle button: |
render | RenderProp<IconButtonState> | – | Composition, for example |
shortcut | string | – | A keyboard shortcut shown in the tooltip after the label, with |
size | ControlSize"sm""md""lg" | 'md' | 32 / 40 / 48 px square. |
tooltip | boolean | IconButtonTooltipOptions | true | Pairs the button with a Tooltip that shows |
variant | ButtonVariant"primary""secondary""tertiary""critical""accent" | 'secondary' | Figma "Variant". |
- Also takes every attribute of
<button>exceptaria-label,aria-pressed,children. (Omit<ComponentPropsWithRef<"button">, "children" | "aria-label" | "aria-pressed">)
render takes an element, or, in client code, a function of the props and this state (IconButtonState):
| Name | Type | Default | Description |
|---|---|---|---|
disabledrequired | boolean | – | |
pendingrequired | boolean | – | |
pressedrequired | boolean | undefined | – |
|
sizerequired | ControlSize"sm""md""lg" | – | |
variantrequired | ButtonVariant"primary""secondary""tertiary""critical""accent" | – |
IconButtonStatic
A server-safe IconButton with no tooltip: the same <button>, classes and CSS as <IconButton tooltip={false}>, from a module that imports no client code.
Use it only where visible text already names the action for sighted users (a column header, a labelled row) and the page should ship no JavaScript for the button: a table row action that is a link (render={<a href="/orders/42/edit" />}) under a header that says what it does, a submit button in a Server Action form (type="submit" inside <form action={deleteProduct}>) beside its label. Under Turbopack and next dev, importing IconButton ships its tooltip code even with tooltip={false} (about 20 KB gzip in production); IconButtonStatic ships none.
An icon-only toolbar needs IconButton's tooltip, so a sighted user can read what each glyph does (Figma: "Icon-only buttons have an aria-label and a Tooltip").
aria-label is always required. You may also pass aria-labelledby pointing at visible text; it then names the button, with aria-label as the fallback. There is no tooltip, no shortcut (pass aria-keyshortcuts yourself, for example "Meta+B Control+B") and no label.
Server component Renders data-slot="icon-button" with the class nz-icon-button. Props: IconButtonStaticProps. Source: src/icon-button/icon-button-static.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
aria-labelrequired | string | – | The action, in words: the button's accessible name. Required, because the glyph alone names nothing. |
iconrequired | ReactNode | – | The glyph, decorative ( From |
label | never | – | IconButtonStatic has no tooltip and no |
pending | boolean | false | An action is in flight: the spinner replaces the glyph, and the button stays focusable with From |
pendingIndicator | ReactNode | – | Replaces the internal spinner glyph. From |
pressed | boolean | – | Makes it a toggle button: From |
render | RenderProp<IconButtonState> | – | Composition, for example From |
shortcut | never | – | IconButtonStatic has no tooltip to show a shortcut in: pass |
size | ControlSize"sm""md""lg" | 'md' | 32 / 40 / 48 px square. From |
tooltip | never | – | IconButtonStatic has no tooltip: use |
variant | ButtonVariant"primary""secondary""tertiary""critical""accent" | 'secondary' | Figma "Variant". From |
- Also takes every attribute of
<button>exceptaria-label,aria-pressed,children. (Omit<ComponentPropsWithRef<"button">, "children" | "aria-label" | "aria-pressed">)
render takes an element, or, in client code, a function of the props and this state (IconButtonState):
| Name | Type | Default | Description |
|---|---|---|---|
disabledrequired | boolean | – | |
pendingrequired | boolean | – | |
pressedrequired | boolean | undefined | – |
|
sizerequired | ControlSize"sm""md""lg" | – | |
variantrequired | ButtonVariant"primary""secondary""tertiary""critical""accent" | – |
Other exports
IconButtonPropstypeimport type { IconButtonProps } from '@nizivo/react/icon-button'The props of
IconButton, listed above.IconButtonStatetypeimport type { IconButtonState } from '@nizivo/react/icon-button'The render state of
IconButton, listed above.IconButtonStaticPropstypeimport type { IconButtonStaticProps } from '@nizivo/react/icon-button'IconButtonStatic's props: IconButton's, without the tooltip.
aria-labelis always required; anaria-labelledbypointing at visible text may be added, and then names the button, witharia-labelas the fallback.The props of
IconButtonStatic, listed above.IconButtonTooltipOptionstypeimport type { IconButtonTooltipOptions } from '@nizivo/react/icon-button'Where the tooltip goes and when it opens;
tooltip={{ side: 'bottom' }}.Name Type Default Description alignAlign"start""center""end"'center'delaynumber400Hover delay in ms; keyboard focus opens it at once.
sideTooltipSide"top""bottom""start""end"'top'
Data attributes and slots
Style parts by class (.nz-<slot>) and state by data attribute. Attribute meanings come from contracts/vocabulary.json.
| Slot | Rendered by | Data attributes |
|---|---|---|
icon-button.nz-icon-button | IconButtonroot (takes render); interactive, focus: self; forced colours: highlight on [aria-pressed='true'] |
|
icon-button-icon.nz-icon-button-icon | IconButtoninner element (CSS only) | none |
CSS hooks
Set a hook on the component's class or any ancestor to restyle it without specificity games; leave it unset for the default shown.
| Hook | Default | Read as |
|---|---|---|
--nz-icon-button-bg |
|
|
--nz-icon-button-bg-disabled | --nz-bg-disabled |
|
--nz-icon-button-bg-hover |
|
|
--nz-icon-button-bg-pressed |
|
|
--nz-icon-button-border |
|
|
--nz-icon-button-border-hover |
|
|
--nz-icon-button-fg |
|
|
--nz-icon-button-fg-disabled | --nz-text-disabled |
|
--nz-icon-button-radius |
|
|
--nz-icon-button-size |
|
|
Layers: nz.components. Classes: .nz-glyph-spinner, .nz-icon-button, .nz-icon-button-icon.
Tokens this stylesheet reads (38)
--nz-bg-accent, --nz-bg-accent-hover, --nz-bg-brand, --nz-bg-brand-hover, --nz-bg-brand-pressed, --nz-bg-brand-subtle, --nz-bg-critical, --nz-bg-critical-hover, --nz-bg-disabled, --nz-bg-hover, --nz-bg-pressed, --nz-bg-surface, --nz-border-brand, --nz-border-default, --nz-border-strong, --nz-border-subtle, --nz-border-width-default, --nz-border-width-focus, --nz-duration-fast, --nz-duration-instant, --nz-duration-spin, --nz-easing-standard, --nz-focus-halo, --nz-radius-md, --nz-radius-sm, --nz-scale-press, --nz-size-control-lg, --nz-size-control-md, --nz-size-control-sm, --nz-size-icon-lg, --nz-size-icon-md, --nz-size-icon-sm, --nz-text-brand, --nz-text-disabled, --nz-text-on-accent, --nz-text-on-brand, --nz-text-on-critical, --nz-text-primary
Accessibility
These parts are interactive. Each gets the focus outline and touch-action rule from @nizivo/react/css/base.css (the nz.base layer), so the floor survives a restyle:
icon-button: draws the focus outline itself on keyboard focus.
In forced-colours mode these states keep a visible system colour: icon-button[aria-pressed='true'] (highlight).
Keyboard
| Keys | Action |
|---|---|
| Tab | Moves focus to the button and shows its tooltip at once. The tooltip repeats the name, so the button is not described by it. |
| Enter, Space | Activates the button. A pending button stays focusable and swallows the activation. |
| Escape | Hides the tooltip; focus stays on the button. |
Source: a native <button> (icon-button.tsx); the tooltip is react-aria-components 1.22.1 TooltipTrigger through Nizivo's Tooltip (icon-button.client.tsx). The Next fixture and the browser lane (test/browser/icon-button.spec.tsx) check showing on focus, Escape and the missing aria-describedby.
Figma
Node ids from design/component-map.json, which nz gen writes from component.json. The links open the Nizivo Figma library, which may ask you to sign in.
- Canonical: Figma node
7275:11357 - Ghost masters: none