Skip to content

Button

Use a Button for anything that happens in the page — submitting a form, opening a dialog, starting a request. Its appearance is set by two independent props: variant is how loud the button is, tone is which colour set it draws from. Everything else the component adds on top of Base UI is about states you would otherwise have to wire yourself: a loading state that blocks double submits without losing focus, and square icon-only sizing that holds a 24px hit target.

button/variants.tsx

Import

import { Button } from "@forte-ui/react";

Examples

Tones

variant and tone combine freely, and all sixteen pairs are defined. That means emphasis and meaning stay separate decisions: tone="danger" variant="solid" is the destructive action you want people to find, while tone="danger" variant="outline" is the same meaning at low emphasis — the right shape for a delete sitting among other row actions.

button/tones.tsx

Sizes

size sets the height, horizontal padding and font size. The values come from the control tokens, so the ambient data-forte-density setting moves them too: size="sm" is 28px tall by default and exactly 24px under data-forte-density="compact", which is the SC 2.5.8 floor — the density scale deliberately stops there. fullWidth is separate from size — it stretches the button to its container, for stacked forms and mobile action bars.

button/sizes.tsx

Loading

loading shows a spinner, hides the label from paint while keeping its width so the button cannot resize under the pointer, sets aria-busy, and blocks activation. Use it for the span of one request, and pair it with a loadingLabel that names the operation.

The indicator is Spinner's ring, rendered decorative and at tone="current" — it takes the button's own text colour, so it stays legible through every variant and tone, and it leaves the announcement to aria-busy and loadingLabel rather than opening a second live region for the same wait. Size it with --forte-button-spinner-size.

button/loading.tsx

No changes saved yet

Icon-only

iconOnly makes the button square, guarantees the icon --forte-button-icon-padding of space on every side, and floors the square at the control height and at --forte-target-min (24px) so a small icon button still meets WCAG SC 2.5.8. When icon plus padding outgrows that floor the button grows with it, so the icon never crowds the corners — which matters most under data-forte-radius="pill", where the corner is the nearest point of the curve. It is a shape, not a fifth variant: every variant, tone and size works exactly as it does with a label. The icon itself should be aria-hidden — the button's name comes from aria-label.

You do not have to size the glyph. Button sizes a direct svg child of its content from --forte-button-icon-size, which tracks size one step above the label on the same scale — 14px, 16px, 18px. That is there because an icon set's own default is whatever that set picked, and here it decides geometry rather than just weight: lucide draws at 24px, which turns a 36px md button into a 42px one that no longer lines up with the controls beside it. Set the knob, or put a size class on the icon, when you want something else — only a direct child is touched, so a composed forte-ui component keeps sizing its own glyphs.

button/icon-only.tsx
solid
soft
outline
ghost

Rendering as another element

render replaces the rendered tag, and nativeButton={false} tells Base UI the replacement is not a <button> so it supplies button semantics itself — role="button" plus Enter and Space activation. For an anchor that genuinely navigates, pass role="link" back so the element keeps the semantics its behaviour actually matches.

button/as-link.tsx

Accessibility

Keyboard interactions
KeyBehaviour
TabMoves focus to the button. A loading button stays in the tab order, so focus is never dropped mid-request.
EnterActivates the button. On a nativeButton={false} element, Base UI calls onClick on key down.
SpaceActivates the button on release, matching native button behaviour.

The focus ring is an outline, not a box-shadow, so it follows the button's border radius and survives forced-colors mode, where box shadows are stripped.

Two disabled states behave differently, and the difference matters. A plain disabled button is removed from the tab order: a keyboard or screen reader user tabbing through the page never lands on it, so any explanation of why it is unavailable has to live somewhere they will actually reach. A loading button keeps focusableWhenDisabled on for the duration, which is what stops the browser from blurring it — focus stays put, and aria-busy and loadingLabel stay on the element the user is still on. You can pass focusableWhenDisabled explicitly to get the same reachability for an ordinary disabled button.

Theming

Every property below is declared on the button's own root element. That means setting one on :root, on a theme class or on a wrapper has no effect — the element's own declaration wins over an inherited value. Override them on the button itself, through the className prop (an unlayered rule beats the library's @layer forte.components, whatever its specificity) or an inline style. To move every button at once, re-point the global tokens in the Default column instead — --forte-color-*, --forte-radius-*, --forte-control-* and --forte-font-* are only read here, never re-declared, so those do inherit from :root or a theme scope. State is exposed on data attributes — data-variant, data-tone, data-size, data-loading, data-icon-only, data-full-width, data-disabled — which is also what Tailwind arbitrary variants like data-[variant=solid]:… target.

Theming tokens for Button
PropertyControlsDefault
--forte-button-radiusCorner radiusvar(--forte-radius-control)
--forte-button-font-weightLabel weightvar(--forte-font-weight-medium)
--forte-button-gapSpace between icon and labelvar(--forte-control-gap)
--forte-button-border-widthBorder thickness (only outline gives it a colour)1px
--forte-button-heightButton height; reassigned by sizevar(--forte-control-h-md)
--forte-button-padding-xHorizontal padding; reassigned by size. iconOnly replaces it with --forte-button-icon-padding on all four sides.var(--forte-control-px-md)
--forte-button-font-sizeLabel size; reassigned by sizevar(--forte-font-size-2)
--forte-button-icon-sizeSize of an svg a consumer drops in; reassigned by size. Applies to direct children of the content span only, so a composed forte-ui component keeps sizing its own glyphs.var(--forte-font-size-3)
--forte-button-spinner-sizeDiameter of the loading spinner1em
--forte-button-hover-liftHow far the button rises on hover. Multiplied by --forte-motion-ok, so a literal value still collapses under reduced motion.var(--forte-control-hover-lift, 0px)
--forte-button-hover-scaleHow far the button grows on hover; also multiplied by --forte-motion-ok.var(--forte-control-hover-scale, 1)
--forte-button-press-scaleHow far the button squashes while held.var(--forte-scale-press)
--forte-button-bgResting background; filled in by varianttransparent
--forte-button-bg-hoverHover backgroundtransparent
--forte-button-bg-activePressed backgroundtransparent
--forte-button-fgLabel and icon colourvar(--forte-color-foreground)
--forte-button-border-colorBorder colourtransparent
--forte-button-tone-solidTone's solid fillvar(--forte-color-primary)
--forte-button-tone-solid-hoverTone's solid fill on hovervar(--forte-color-primary-hover)
--forte-button-tone-solid-activeTone's solid fill when pressedvar(--forte-color-primary-active)
--forte-button-tone-on-solidText colour on the solid fillvar(--forte-color-on-primary)
--forte-button-tone-softTone's tinted fill, and the hover wash for outline and ghostvar(--forte-color-primary-soft)
--forte-button-tone-soft-hoverTinted fill on hovervar(--forte-color-primary-soft-hover)
--forte-button-tone-soft-activeTinted fill when pressedvar(--forte-color-primary-soft-active)
--forte-button-tone-textLabel colour for soft, outline and ghostvar(--forte-color-primary-text)
--forte-button-tone-borderBorder colour for outlinevar(--forte-color-primary-border)
--forte-button-icon-paddingMinimum space between the icon and the button's edge (iconOnly only)var(--forte-space-2)

The two groups are layered: variant decides which of the --forte-button-tone-* values become the background, text and border slots, and tone decides what those values are. To restyle a whole tone, override the tone properties; to restyle one button, override the slots directly.

.checkout-cta {
  --forte-button-radius: var(--forte-radius-full);
  --forte-button-tone-solid: var(--forte-color-secondary);
  --forte-button-tone-on-solid: var(--forte-color-on-secondary);
}

API reference

Props for Button
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
focusableWhenDisabledbooleantrue while `loading`, otherwise falseKeep the button focusable while it is disabled. A native `disabled` button is blurred by the browser, which drops focus to `<body>` and loses the user's place in the tab order — and takes `aria-busy` and `loadingLabel` out of earshot with it. Left unset, this turns itself on for the duration of `loading` so the busy state is actually announced; pass it explicitly to override.
fullWidthbooleanfalseStretch the button to fill the width of its container.
iconOnlybooleanfalseRender as a square button sized for a single icon. Enforces the 24px minimum hit target from WCAG SC 2.5.8. Always pair with `aria-label`.
loadingbooleanfalseShow a busy indicator and block interaction. The label keeps its space so the button cannot resize mid-interaction.
loadingLabelstringLoadingAnnounced to assistive technology while `loading` is true. Without it, a screen reader user gets no signal that anything is happening.
sizeButtonSizemdSize of the button. Actual dimensions also follow the ambient `data-forte-density` setting.
toneButtonToneprimaryWhich semantic colour set the button draws from. Combines freely with `variant` — `tone="danger" variant="outline"` is a low-emphasis destructive action.
variantButtonVariantsolidHow much visual weight the button carries.

The table above is generated from the component's TypeScript source. Button also forwards everything Base UI's Button accepts — render, nativeButton, disabled — along with the usual <button> attributes such as type, form, onClick and any aria-* prop.