Skip to content

Button

Edit on GitHub
@volvo-cars/css v2.3.0@volvo-cars/react-forms v2.0.0

Apply button-filled, button-outlined, or button-text to a <button> or <a> element.

<div class="flex flex-wrap gap-16">
<button class="button-filled">Filled</button>
<button class="button-outlined">Outlined</button>
<button class="button-text">Text</button>
</div>

Primary actions. Use at most one per viewport.

Secondary actions. Can appear multiple times on a page.

Tertiary actions. Shows a forward arrow by default. Control direction with data-arrow:

<div class="flex-col items-start gap-8">
<button class="button-text">Default (forward)</button>
<button class="button-text" data-arrow="back">Back</button>
<button class="button-text" data-arrow="down">Down</button>
<button class="button-text" data-arrow="up">Up</button>
<button class="button-text" data-arrow="none">No arrow</button>
</div>

Arrow direction automatically flips in RTL layouts.

Use data-color to change the button’s intent. Available values:

  • accent — extra prominence. Use only on plain backgrounds. Not supported on text buttons (maps back to default).
  • destructive — delete, remove, or cancel actions.
  • subtle — low-emphasis filled buttons with a gray surface and primary text. Filled only.
<div class="flex flex-wrap gap-16 items-center">
<button class="button-filled">Default</button>
<button class="button-filled" data-color="accent">Accent</button>
<button class="button-filled" data-color="destructive">Destructive</button>
<button class="button-filled" data-color="subtle">Subtle</button>
</div>

Buttons default to 3rem (48px). Use data-size="medium" for 2.5rem (40px) or data-size="small" for 2rem (32px). Both are available on filled and outlined buttons. Small buttons maintain a 40px touch target via an invisible pseudo-element.

Text buttons do not support data-size — the attribute is ignored.

<div class="flex flex-wrap gap-16 items-center">
<button class="button-filled">Default</button>
<button class="button-filled" data-size="medium">Medium</button>
<button class="button-filled" data-size="small">Small</button>
</div>

Use <Icon> from @volvo-cars/react-icons with color="currentColor" so the icon color follows the button. Wrap the label text in a <span> to preserve spacing. Set data-arrow="none" on text buttons to suppress the default arrow. Icons get automatic inline margin depending on whether they appear first or last.

import { Icon } from '@volvo-cars/react-icons';
export function ButtonIcons() {
return (
<div className="flex flex-wrap gap-16 items-center">
<button className="button-filled" data-arrow="none" type="button">
<Icon icon="external-link" size={16} color="currentColor" />
<span>Open link</span>
</button>
<button className="button-outlined" data-arrow="none" type="button">
<span>Download</span>
<Icon icon="download" size={16} color="currentColor" />
</button>
<button className="button-text" data-arrow="none" type="button">
<Icon icon="heart" size={16} color="currentColor" />
<span>Favourite</span>
</button>
</div>
);
}

Apply the class directly to an <a> or your framework’s link component:

<div class="flex flex-wrap gap-16 items-center">
<a class="button-filled" href="#">Filled link</a>
<a class="button-outlined" href="#">Outlined link</a>
<a class="button-text" href="#">Text link</a>
</div>

Use the button-group class to align buttons horizontally. The group automatically handles spacing and responsive behavior.

<div class="button-group">
<button type="button" class="button-filled">Primary</button>
<button type="button" class="button-filled" data-color="subtle">
Secondary
</button>
</div>

The group provides:

  • 16px gap between buttons
  • Automatic wrapping when buttons don’t fit
  • Full-width buttons on viewports < 768px or containers < 30rem

Use justify-* utility classes to position buttons on larger viewports.

<div class="flex flex-col gap-24">
<div class="button-group justify-start">
<button type="button" class="button-filled">Left</button>
<button type="button" class="button-filled" data-color="subtle">
Aligned
</button>
</div>
<div class="button-group justify-center">
<button type="button" class="button-filled">Center</button>
<button type="button" class="button-filled" data-color="subtle">
Aligned
</button>
</div>
<div class="button-group justify-end">
<button type="button" class="button-filled">Right</button>
<button type="button" class="button-filled" data-color="subtle">
Aligned
</button>
</div>
</div>

Combine different variants for visual hierarchy.

<div class="button-group">
<button type="button" class="button-filled">Continue</button>
<button type="button" class="button-text">Cancel</button>
</div>
<div class="button-group">
<button type="button" class="button-filled">Save</button>
<button type="button" class="button-filled" data-color="subtle">
Preview
</button>
<button type="button" class="button-text">Cancel</button>
</div>

Filled and outlined buttons support a loading indicator. Hide the label with invisible and add a <progress class="spinner">. The spinner is absolutely positioned and centered — keeping the label in the DOM preserves the button’s width.

<div class="flex flex-wrap gap-16 items-center">
<button class="button-filled" data-loading="true">
<span class="invisible">Loading</span>
<progress class="spinner" aria-label="Loading"></progress>
</button>
<button class="button-outlined" data-loading="true">
<span class="invisible">Loading</span>
<progress class="spinner" aria-label="Loading"></progress>
</button>
</div>

Prefer aria-disabled="true" over the disabled attribute — it keeps the button focusable for screen reader users and keyboard navigation. Disabled buttons get reduced opacity automatically.

<div class="flex flex-wrap gap-16 items-center">
<button class="button-filled" aria-disabled="true">Filled</button>
<button class="button-outlined" aria-disabled="true">Outlined</button>
<button class="button-text" aria-disabled="true">Text</button>
</div>

Avoid disabled buttons when possible — show an explanatory message instead. See Usability Pitfalls of Disabled Buttons.

Key consumer responsibilities from the WCAG audit:

  • Accessible name — every button needs visible text or aria-label. Consistent labeling across the interface is your responsibility.
  • aria-disabled over disabled — ensures the button remains in the tab order and is announced by screen readers.
  • Destructive actions — provide a confirmation step (dialog, etc.) before finalizing. The data-color="destructive" styling alone is not sufficient.
  • Loading status — if you add a loading spinner, ensure the state change is announced to assistive technology (e.g., with aria-label="Loading" on the <progress>).
  • Links styled as buttons<a> with a button class and a valid href is fine. Do not use <a> without href as a button; use <button> instead.
  • Color modes — use light-mode buttons on light backgrounds and dark-mode buttons on dark backgrounds. Never use accent or destructive colors on colored backgrounds or media.

SubmitButton from @volvo-cars/react-forms is a form-aware button with type="submit" by default. It supports the same filled and outlined variants as the CSS button classes, plus built-in loading state management.

import { SubmitButton } from '@volvo-cars/react-forms';
export function SubmitButtonBasic() {
return <SubmitButton type="button">Submit</SubmitButton>;
}

Set loading to show a spinner and prevent resubmission. Always provide loadingLabel for screen readers.

import { SubmitButton } from '@volvo-cars/react-forms';
export function SubmitButtonLoading() {
return (
<div className="flex flex-wrap gap-16">
<SubmitButton type="button" loading loadingLabel="Submitting">
Submit
</SubmitButton>
<SubmitButton
type="button"
variant="outlined"
loading
loadingLabel="Submitting"
>
Submit
</SubmitButton>
</div>
);
}

Prefer handling the form onSubmit event rather than onClick on the submit button.

Prop Type Required Default
hidden boolean - -
id string - -
title string - -
dir string - -
lang string - -
slot string - -
translate "yes" | "no" - -
className string - -
style CSSProperties - -
tabIndex number - -
onPointerDown (PointerEventHandler<Element> & PointerEventHan... - -
onPointerEnter (PointerEventHandler<Element> & PointerEventHan... - -
onPointerLeave (PointerEventHandler<Element> & PointerEventHan... - -
onPointerMove (PointerEventHandler<Element> & PointerEventHan... - -
onPointerUp (PointerEventHandler<Element> & PointerEventHan... - -
variant "filled" | "outlined" - filled
Which design variant to render.
color "accent" | "destructive" | "neutral" | "subtle" - neutral
The `accent` color can be used to add extra prominence to the button. Use the `destructive` color for actions that require caution.
size "medium" | "small" - medium
Use the small button in cards and other constrained containers.
form string - -
Id of a form element that this button should be associated with. Defaults to the containing form element.
formAction string - -
The URL that processes the information submitted by the button, overriding the `action` attribute of the button's form.
formMethod "post" | "get" - -
Specifies the HTTP method used to submit the form, overriding the `method` attribute of the button's form.
name string - -
The name of the button, submitted as a pair with the button's value as part of the form data.
value string - -
The value associated with the button's `name` in the form data when the form is submitted using this button.
children ReactNode -
The button label.
disabled boolean - -
Disables the button. Avoid using disabled buttons whenever possible - instead show error messages explaining the next steps the user should take.
loading boolean - -
Renders a spinner in place of the button label and prevents activating the button again.
loadingLabel string - -
Label for screen readers while showing the loading spinner.
type "submit" | "button" - submit
onClick MouseEventHandler<HTMLButtonElement> - -
Called when the button is clicked. Prefer to place the button within a `<form> and use the `onSubmit` event on the form instead.
onFocus FocusEventHandler<HTMLButtonElement> - -
onBlur FocusEventHandler<HTMLButtonElement> - -
onKeyDown KeyboardEventHandler<HTMLButtonElement> - -
onKeyUp KeyboardEventHandler<HTMLButtonElement> - -