Button component for interface actions. Renders as a regular link when href is provided. Buttons should primarily be used to trigger events or actions.

Open in new tab
import { Button, Stack } from "@kesko/components";
import { IconBasketAddTo, IconArrowRight, IconExternalLink } from "@kesko/iconography/react";

export default () => (
  <Stack direction="row" gap="2xs" align="center">
    <Button>
      <IconBasketAddTo />
      Primary
    </Button>
    <Button square>
      <IconBasketAddTo label="Shopping basket" />
    </Button>
    <Button variant="secondary">
      Secondary
      <IconArrowRight />
    </Button>
    <Button variant="secondary" appearance="outline">Outline</Button>
    <Button variant="secondary" appearance="plain" href="https://kesko.design" target="_blank">
      Plain <IconExternalLink label="(Opens in a new window)" />
    </Button>
    <Button variant="secondary" appearance="text">Text</Button>
  </Stack>
);
import { Button, Stack } from "@kesko/components";
import { IconClear, IconTick } from "@kesko/iconography/react";

export default () => (
  <Stack direction="row" gap="2xs" align="center">
    <Button variant="success">
      <IconTick /> Accept
    </Button>
    <Button variant="error">
      <IconClear /> Decline
    </Button>
  </Stack>
);
import type { CSSProperties } from "react";
import { Button, Stack } from "@kesko/components";

const info = {
  "--k-button-bg": "var(--k-color-status-info)",
  "--k-button-text": "var(--k-color-text-on-info)",
  "--k-button-bg-hover": "var(--k-color-highlight-info)",
} as CSSProperties;

export default () => (
  <Stack direction="row" gap="2xs" align="center">
    <Button style={info}>Info</Button>
    <Button appearance="outline" style={{ "--k-button-radius": "var(--k-radius-md)" } as CSSProperties}>
      Rounded corners
    </Button>
  </Stack>
);
import { Button, Stack } from "@kesko/components";

export default () => (
  <Stack direction="row" gap="2xs" align="center">
    <Button disabled>Filled</Button>
    <Button appearance="outline" disabled>Outline</Button>
    <Button appearance="plain" disabled>Plain</Button>
    <Button appearance="text" disabled>Text</Button>
  </Stack>
);
import { Button } from "@kesko/components";

export default () => (
  <Button onClick={() => alert("Clicked!")}>Click me</Button>
);
import { Button, Stack } from "@kesko/components";
import { IconArrowUp } from "@kesko/iconography/react";

export default () => (
  <Stack direction="row" gap="2xs" align="center">
    <Button expand>Full width</Button>
    <Button expand>
      Back to top <IconArrowUp />
    </Button>
  </Stack>
);
import { Button, Stack } from "@kesko/components";

export default () => (
  <form onSubmit={event => event.preventDefault()}>
    <Stack direction="row" gap="2xs" align="center">
      <Button type="submit">Save</Button>
      <Button type="reset" variant="secondary" appearance="outline">
        Reset
      </Button>
      <Button type="submit" loading>
        Saving
      </Button>
    </Stack>
  </form>
);
import { Button, Stack } from "@kesko/components";

export default () => (
  <Stack direction="row" gap="2xs" align="center">
    <Button size="lg" loading>Loading</Button>
    <Button size="md" loading>Loading</Button>
    <Button size="sm" loading>Loading</Button>
  </Stack>
);
import { Button } from "@kesko/components";
import { IconBasketAddTo } from "@kesko/iconography/react";

export default () => (
  <div
    style={{ display: "grid",
      gridTemplateColumns: "max-content repeat(4, max-content)",
      gap: "var(--k-space-2xs) var(--k-space-sm)",
      alignItems: "center",
      justifyItems: "start",
    }}
  >
    <span className="k-text-sm">Large</span>
    <Button appearance="filled" size="lg">
      <IconBasketAddTo /> Filled
    </Button>
    <Button appearance="outline" size="lg">
      <IconBasketAddTo /> Outline
    </Button>
    <Button appearance="plain" size="lg">
      <IconBasketAddTo /> Plain
    </Button>
    <Button size="lg" square>
      <IconBasketAddTo label="Add to basket" />
    </Button>
    <span className="k-text-sm">Medium</span>
    <Button appearance="filled">
      <IconBasketAddTo /> Filled
    </Button>
    <Button appearance="outline">
      <IconBasketAddTo /> Outline
    </Button>
    <Button appearance="plain">
      <IconBasketAddTo /> Plain
    </Button>
    <Button square>
      <IconBasketAddTo label="Add to basket" />
    </Button>
    <span className="k-text-sm">Small</span>
    <Button appearance="filled" size="sm">
      <IconBasketAddTo /> Filled
    </Button>
    <Button appearance="outline" size="sm">
      <IconBasketAddTo /> Outline
    </Button>
    <Button appearance="plain" size="sm">
      <IconBasketAddTo /> Plain
    </Button>
    <Button size="sm" square>
      <IconBasketAddTo label="Add to basket" />
    </Button>
  </div>
);
import { Button } from "@kesko/components";
import { IconBasketAddTo } from "@kesko/iconography/react";

export default () => (
  <div style={{
      display: "grid",
      gridTemplateColumns: "max-content repeat(4, max-content)",
      gap: "var(--k-space-2xs) var(--k-space-sm)",
      alignItems: "center",
      justifyItems: "start",
    }}
  >
    <span className="k-text-sm">Primary</span>
    <Button>
      <IconBasketAddTo /> Filled
    </Button>
    <Button appearance="outline">
      <IconBasketAddTo /> Outline
    </Button>
    <Button appearance="plain">
      <IconBasketAddTo /> Plain
    </Button>
    <Button appearance="text">
      <IconBasketAddTo /> Text
    </Button>
    <span className="k-text-sm">Secondary</span>
    <Button variant="secondary">
      <IconBasketAddTo /> Filled
    </Button>
    <Button variant="secondary" appearance="outline">
      <IconBasketAddTo /> Outline
    </Button>
    <Button variant="secondary" appearance="plain">
      <IconBasketAddTo /> Plain
    </Button>
    <Button variant="secondary" appearance="text">
      <IconBasketAddTo /> Text
    </Button>
    <div
      style={{
        gridColumn: "1 / -1",
        display: "grid",
        gridTemplateColumns: "subgrid",
        alignItems: "center",
        padding: "var(--k-space-sm)",
        marginInline: "calc(-1 * var(--k-space-sm))",
        borderRadius: "var(--k-radius-md)",
        background: "var(--k-color-bg-inverted)",
        color: "var(--k-color-text-inverted)",
      }}
    >
      <span className="k-text-sm">Neutral</span>
      <Button variant="neutral">
        <IconBasketAddTo /> Filled
      </Button>
      <Button variant="neutral" appearance="outline">
        <IconBasketAddTo /> Outline
      </Button>
      <Button variant="neutral" appearance="plain">
        <IconBasketAddTo /> Plain
      </Button>
      <Button variant="neutral" appearance="text">
        <IconBasketAddTo /> Text
      </Button>
    </div>
  </div>
);

Properties

NameDescriptionTypeDefault
variant

The style variant of the button.

"primary" | "secondary" | "neutral" | "success" | "error""primary"
appearance

The appearance of the button. Defaults to filled, or outline when variant is error. Plain HTML has no such default: write appearance="outline" on an error button yourself.

"filled" | "outline" | "plain" | "text"—
size

The size of the button.

"sm" | "md" | "lg""md"
expand

Makes the button fit its container.

booleanfalse
disabled

Disables the component. Native disabled on buttons, aria-disabled on links.

booleanfalse
loading

Shows a spinner and blocks activation with aria-disabled. The button stays focusable so keyboard and screen reader users keep their place. Pass a boolean whenever the button can load, loading={isSaving}: the status region that announces the wait is rendered as soon as the prop is set, so it exists before loading starts.

boolean—
loadingLabel

Text announced by screen readers when loading turns on. Localize it or name what is loading, for example "Saving order". Pass an empty string to keep a button silent, for rows that load together while one message is announced elsewhere.

string—
square

Forces a 1:1 aspect ratio at the size's height, for icon-only buttons and single characters such as page numbers. An icon-only button needs an accessible name: set aria-label on the button, or give the icon a label.

booleanfalse
aria-label

Accessible name. Icon-only (square) buttons need one: set it here, or give the icon a label instead.

string—
aria-labelledby

id of an element that labels this button. Use instead of aria-label when the label is already visible elsewhere on the page.

string—
aria-describedby

id of an element that describes this button (typically helper text).

string—
type

The type of the button.

"submit" | "reset" | "button""button"
aria-controls

id of the element(s) controlled by this button.

string—
aria-expanded

Whether a grouping controlled by this button is expanded.

boolean | "true" | "false"—
aria-haspopup

Availability and type of popup this button opens.

boolean | "menu" | "listbox" | "tree" | "grid" | "dialog" | "true" | "false"—
href

Renders as an anchor element.

string—
target

Where to open the linked URL.

"_self" | "_blank" | "_parent" | "_top"—
download

Triggers a file download. Pass true to use the server filename or a string to override it.

boolean | string—

Events

NameDescriptionType
onClick

Handler called on click (mouse, touch, or keyboard activation).

(event: MouseEvent<ButtonTarget>) => void
onFocus

Handler called when the element receives focus.

(event: FocusEvent<ButtonTarget>) => void
onBlur

Handler called when the element loses focus.

(event: FocusEvent<ButtonTarget>) => void

Styles

CSS custom properties provide finer-grained control than props. Prefer the props API when it suits your need.

NameDescriptionDefault
--k-button-bg

Overrides the background color.

var(--k-color-theme-primary)
--k-button-text

Overrides the text color.

var(--k-color-text-on-primary)
--k-button-border

Overrides the border color.

transparent
--k-button-bg-hover

Overrides the hover and pressed background color.

var(--k-color-highlight-primary)
--k-button-text-hover

Overrides the hover and pressed text color.

var(--k-button-text)
--k-button-border-hover

Overrides the hover and pressed border color.

transparent
--k-button-radius

Overrides the border radius.

var(--k-radius-button)
--k-button-focus

Overrides the focus ring color.

var(--k-color-border-focus)
--k-button-height

Overrides the button height.

calc(line-height + 2 * padding)

Accessibility #

Under the hood this is a native <button>, or an <a> when you pass a href, so it behaves as people and assistive technologies expect.

  • Use href for navigation. Screen readers announce a link as going somewhere and a button as doing something. Do not navigate from onClick. In Next.js or React Router, wrap your app once in KeskoProvider with the router's navigate function, and links change pages without a full reload.
  • Give icon-only buttons a label. Pass aria-label, or set label on the icon. That text is what a screen reader reads out.
  • Keep the meaning in the label. The error variant marks a risky action for people who see red. The label has to mark it for everyone, for example “Poista tilaus” rather than “Vahvista”.
  • Say what happens. Screen reader users often jump from button to button and hear only the labels, so “Lue lisää” or “Klikkaa tästä” mean nothing on their own. Name the outcome, for example “Näytä tilaushistoria”.
  • Avoid disabled. A disabled button leaves the tab order and cannot explain itself. Keep it enabled and validate on click. If you must disable it, use the provided prop, not CSS: it sets disabled or aria-disabled for you.
  • Use loading for short waits. It blocks the button, keeps its size and renders a status spinner that screen readers announce. Show longer progress elsewhere.
  • Announce bulk waits once. When many buttons load together, for example one per table row, every region would speak. Pass loadingLabel="" to those buttons and announce the wait once in a status region of your own.
  • Check contrast when you override colors. Text needs 4.5:1 against --k-button-bg, and --k-button-focus needs 3:1 against the surface the button sits on. Do not override the sizes: the small size is already the minimum target.

See the accessibility guidelines and the A11Y checklist for more details.


Usage #

There are two necessary parts to using the Button component. The @kesko/components package and the CSS Framework are required. Everything else is optional:

npm install @kesko/components @kesko/css

Once installed, load the CSS Framework in your app’s entry point or root layout:

import "@kesko/css";

Then, you can import the Button:

import { Button } from "@kesko/components";

Projects without React can get the same styles from @kesko/css by importing the components stylesheet as well:

@import "@kesko/css";
@import "@kesko/css/components";

Resources #