Button
A native button in five variants and three sizes, with a pending state that keeps focus. Compose it into triggers and links through render.
Import
import { Button } from '@nizivo/react/button';
// Once per app (Getting started), after
// @nizivo/tokens/tokens.css and @nizivo/react/css/base.css
import '@nizivo/react/css/button.css';Static or interactive
Server. It ships no client JavaScript when a Server Component renders it, in every Next.js build
mode; its code is bundled only when a client component (a 'use client' file) imports it.
onClick works as on any <button>. In a client app (a Vite or React Router single-page app, or a
'use client' file) pass it, refs and state as usual
(In a client app). Only a Server Component
cannot pass it, which is React's rule for every component: there, use type="submit" in a
<form action={serverAction}>, render={<a href="…" />}, or a client component that owns the
handler. How to choose.
Pending
pending replaces the leading icon with a spinner, keeps the label, and keeps the button focusable
with aria-disabled, swallowing clicks. The spinner turns once every 800 ms (--nz-duration-spin,
Figma's duration/spin). Under Reduced motion (the OS setting, or data-nz-motion="reduce" on an
ancestor) it stops, and the text "Loading…" shows in the label's place, so the wait is still
visible without movement (Figma's Accessibility page: "Spinner and skeleton loops stop; a text label
shows"). The text and the label share one space, so the button keeps its width when pending
starts (it grows only if "Loading…" is wider than the label). Where the text does not fit, it ends
in an ellipsis.
<>
{/* Reduced motion: the spinner stops and "Loading…" replaces "Publishing" */}
<Button pending>Publishing</Button>
<Button pending labels={{ pending: 'Wird geladen…' }}>
Veröffentlichen
</Button>
</>The label is only made transparent, so it stays in the accessible name, after the text on screen:
"Loading… Publishing" (WCAG 2.5.3, the visible words are part of the name). Under default motion the
text is not rendered visibly and the name is the label. Assistive technology also learns the state
from aria-disabled. labels={{ pending: '' }} shows no text and keeps the label visible. The
markup is the same in both motion modes; only CSS reads the preference.
Translate the text with labels.pending. It is set per instance: Button is a server component, so
NizivoConfig's labels, which reach the client parts only, do not reach it. SubmitButton, its
client twin, does read button.pending from them. A localised app wraps Button once and uses the
wrapper everywhere (see Strings and locales):
import { Button, type ButtonProps } from '@nizivo/react/button';
const strings = { loading: 'Wird geladen…' }; // the app's own message catalogue
export function AppButton({ labels, ...props }: ButtonProps) {
return <Button labels={{ pending: strings.loading, ...labels }} {...props} />;
}Examples
Basic
import type { ComponentPropsWithRef, JSX } from 'react';
import { Button } from '@nizivo/react/button';
/** 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 ButtonExample(): JSX.Element {
return (
<div>
<Button variant="primary">Save</Button>
<Button>Cancel</Button>
<Button variant="accent">Upgrade</Button>
<Button variant="critical">Remove</Button>
<Button size="lg">Continue</Button>
<Button variant="critical" size="sm" disabled>
Delete
</Button>
<Button variant="tertiary" pending>
Publishing
</Button>
<Button render={<Link href="/orders" />} fullWidth>
Orders
</Button>
</div>
);
}
Props
Server component Renders data-slot="button" with the class nz-button. Props: ButtonProps. Source: src/button/button.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
children | ReactNode | – | |
fullWidth | boolean | false | Fills the width of its container. |
labels | Partial<ButtonLabels> | – | Overrides Button's strings, for example |
leadingIcon | ReactNode | – | |
pending | boolean | false | An action is in flight: the spinner replaces the leading icon, the label stays, and the button stays focusable with |
pendingIndicator | ReactNode | – | Replaces the internal spinner glyph. |
render | RenderProp<ButtonState> | – | Composition, for example |
size | ControlSize"sm""md""lg" | 'md' | 32 / 40 / 48 px. |
trailingIcon | ReactNode | – | |
variant | ButtonVariant"primary""secondary""tertiary""critical""accent" | 'secondary' | Figma "Variant". One primary per view; secondary for everything else; tertiary inside dense rows. |
- Also takes every attribute of
<button>exceptchildren. (Omit<ComponentPropsWithRef<"button">, "children">)
render takes an element, or, in client code, a function of the props and this state (ButtonState):
| Name | Type | Default | Description |
|---|---|---|---|
disabledrequired | boolean | – | |
pendingrequired | boolean | – | |
sizerequired | ControlSize"sm""md""lg" | – | |
variantrequired | ButtonVariant"primary""secondary""tertiary""critical""accent" | – |
Other exports
ButtonLabelstypeimport type { ButtonLabels } from '@nizivo/react/button'Button's own strings (§16.4): serialisable, English defaults.
Name Type Default Description pendingrequiredstring'Loading…'The text a pending button shows in place of its label under Reduced motion, where the spinner stops (Figma: "Spinner and skeleton loops stop; a text label shows").
ButtonPropstypeimport type { ButtonProps } from '@nizivo/react/button'The props of
Button, listed above.ButtonStatetypeimport type { ButtonState } from '@nizivo/react/button'The render state of
Button, listed above.
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 |
|---|---|---|
button.nz-button | Buttonroot (takes render); interactive, focus: self |
|
button-icon.nz-button-icon | Buttoninner element (CSS only) | none |
button-label.nz-button-label | Buttoninner element (CSS only) | none |
button-pending-label.nz-button-pending-label | Buttoninner 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-button-bg |
|
|
--nz-button-bg-disabled | --nz-bg-disabled |
|
--nz-button-bg-hover |
|
|
--nz-button-bg-pressed |
|
|
--nz-button-border |
|
|
--nz-button-border-hover |
|
|
--nz-button-fg |
|
|
--nz-button-fg-disabled | --nz-text-disabled |
|
--nz-button-font |
|
|
--nz-button-gap |
|
|
--nz-button-height |
|
|
--nz-button-padding-inline |
|
|
--nz-button-radius |
|
|
--nz-button-shadow |
|
|
Layers: nz.components. Classes: .nz-button, .nz-button-icon, .nz-button-label, .nz-button-pending-label, .nz-glyph-spinner.
Tokens this stylesheet reads (42)
--nz-bg-accent, --nz-bg-accent-hover, --nz-bg-brand, --nz-bg-brand-hover, --nz-bg-brand-pressed, --nz-bg-critical, --nz-bg-critical-hover, --nz-bg-disabled, --nz-bg-hover, --nz-bg-pressed, --nz-bg-surface, --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-elevation-button, --nz-focus-halo, --nz-font-label-l, --nz-font-label-m, --nz-radius-md, --nz-radius-sm, --nz-scale-press, --nz-size-control-lg, --nz-size-control-md, --nz-size-control-sm, --nz-size-icon-md, --nz-size-icon-sm, --nz-space-1-5, --nz-space-2, --nz-space-3, --nz-space-4, --nz-space-5, --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:
button: draws the focus outline itself on keyboard focus.
Keyboard
| Keys | Action |
|---|---|
| Tab | Moves focus to the button. A pending button stays focusable (aria-disabled); a disabled one does not. |
| Enter, Space | Activates the button. A pending button swallows the activation. |
Source: a native <button> (button.tsx); with render={<a />} it is a link and only Enter activates it.
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:11244 - Ghost masters: Figma node
744:263
BulkActionBar
The bar over a selectable table, with how many rows are selected and the actions for them. A server part; TableSelection's island writes the count, and the actions work before hydration.
Card
A surface with optional header, title, description, action, body and footer parts. The inverse tone flips every token inside it; brand and accent are the tinted surfaces of the dashboard widgets (KpiTile's tone), with no stroke.