Docs
Systhema Design (opens in new tab)
Unreleased

Button

Token-variant buttons as button, link or div, with ButtonTitle and ButtonIcon.

On this page

Button renders an action or a link in one of the button variants defined by your tokens. It is polymorphic: the same styles apply to a <button>, a link or any other element, and ButtonTitle and ButtonIcon lay out the label and an optional icon.

Button
import { Button, ButtonIcon, ButtonTitle } from '@systhemaui/next'

export default function Demo() {
  return (
    <div className="flex flex-wrap items-center gap-4">
      <Button.a href="#examples" variant="primary">
        <ButtonTitle>Get started</ButtonTitle>
        <ButtonIcon />
      </Button.a>
      <Button.a href="#variants-and-tags" variant="secondary">
        <ButtonTitle>See variants</ButtonTitle>
      </Button.a>
    </div>
  )
}

ImportLink to this section

import { Button, ButtonIcon, ButtonTitle } from '@systhemaui/next'

In a React app without Next.js, import them from @systhemaui/react. There Button.a renders a plain anchor with the same hash handling.

Variants and tagsLink to this section

VariantsLink to this section

variant takes one of the button variants in your tokens: primary and secondary with the default tokens. Each variant has its own colors for the normal and hover states, its own padding, radius, border, shadow, font and icon size. Without variant, the first variant in the tokens is used.

Variants
import { Button } from '@systhemaui/next'

export default function Demo() {
  return (
    <div className="flex flex-wrap items-center gap-4">
      <Button.button variant="primary">Primary</Button.button>
      <Button.button variant="secondary">Secondary</Button.button>
    </div>
  )
}

A variant you add in Figma becomes a new button-<name> class and a new member of the ButtonVariant type after the next sync.

TagsLink to this section

ComponentRenders
Buttona <button> by default, or the element in as
Button.buttona <button>
Button.aa link through the LinkHelper (next/link)
Button.diva <div>, for a button look inside another interactive element
Tags
Button.a
Button.div
import { Button } from '@systhemaui/next'

export default function Demo() {
  return (
    <div className="flex flex-wrap items-center gap-4">
      <Button.button type="button">Button.button</Button.button>
      <Button.a href="#tags">Button.a</Button.a>
      <Button.div>Button.div</Button.div>
    </div>
  )
}

Use Button.button (or Button with type) for actions and Button.a for navigation. Button.div is not focusable and has no role; use it only where an outer element, such as a card link, is the real control.

ExamplesLink to this section

With an iconLink to this section

An empty ButtonIcon draws the default arrow, sized by the variant's icon token and colored with the text. Put it before ButtonTitle for a leading icon. ButtonTitle grows to fill the button, which keeps the icon at the edge in a wide button.

Default icon, leading and trailing
import { Button, ButtonIcon, ButtonTitle } from '@systhemaui/next'

export default function Demo() {
  return (
    <div className="flex flex-wrap items-center gap-4">
      <Button.a href="#with-an-icon" variant="primary">
        <ButtonTitle>Continue</ButtonTitle>
        <ButtonIcon />
      </Button.a>
      <Button.a href="#with-an-icon" variant="secondary">
        <ButtonIcon />
        <ButtonTitle>Leading icon</ButtonTitle>
      </Button.a>
      <Button.a href="#with-an-icon" variant="primary" className="w-64">
        <ButtonTitle>Full row</ButtonTitle>
        <ButtonIcon />
      </Button.a>
    </div>
  )
}

Change the default arrow for the whole project with blocks.button.icon (see Component CSS blocks).

Custom iconLink to this section

ButtonIcon.<tag> renders the icon as that element. ButtonIcon.svg makes the SVG itself the icon, so it takes the variant's icon size and color.

Custom icon
import { Button, ButtonIcon, ButtonTitle } from '@systhemaui/next'

export default function Demo() {
  return (
    <div className="flex flex-wrap items-center gap-4">
      <Button.button variant="primary">
        <ButtonIcon.svg viewBox="0 -960 960 960" fill="currentColor">
          <path d="M480-320 280-520l56-58 104 104v-326h80v326l104-104 56 58-200 200ZM240-160q-33 0-56.5-23.5T160-240v-120h80v120h480v-120h80v120q0 33-23.5 56.5T720-160H240Z" />
        </ButtonIcon.svg>
        <ButtonTitle>Download</ButtonTitle>
      </Button.button>
      <Button.button variant="secondary">
        <ButtonTitle>Favorite</ButtonTitle>
        <ButtonIcon.svg viewBox="0 -960 960 960" fill="currentColor">
          <path d="m480-120-58-52q-101-91-167-157T150-447.5Q111-500 95.5-544T80-634q0-94 63-157t157-63q52 0 99 22t81 62q34-40 81-62t99-22q94 0 157 63t63 157q0 46-15.5 90T810-447.5Q771-395 705-329T538-172l-58 52Z" />
        </ButtonIcon.svg>
      </Button.button>
    </div>
  )
}

Icon-onlyLink to this section

An icon-only button needs one accessible name. Keep the ButtonTitle and hide it visually with sr-only!, or set aria-label on the button, never both.

Icon-only
import { Button, ButtonIcon, ButtonTitle } from '@systhemaui/next'

export default function Demo() {
  return (
    <div className="flex flex-wrap items-center gap-4">
      <Button.button variant="primary">
        <ButtonTitle className="sr-only!">Next step</ButtonTitle>
        <ButtonIcon />
      </Button.button>
      <Button.button variant="secondary" aria-label="Next step">
        <ButtonIcon />
      </Button.button>
    </div>
  )
}

DisabledLink to this section

A disabled button keeps its normal colors, ignores hover and shows the default cursor.

Disabled
import { Button, ButtonIcon, ButtonTitle } from '@systhemaui/next'

export default function Demo() {
  return (
    <div className="flex flex-wrap items-center gap-4">
      <Button.button variant="primary" disabled>
        <ButtonTitle>Saving…</ButtonTitle>
      </Button.button>
      <Button.button variant="secondary" disabled>
        <ButtonTitle>Continue</ButtonTitle>
        <ButtonIcon />
      </Button.button>
    </div>
  )
}

The tokens have no separate disabled colors. Add your own, for example disabled:opacity-50, when the state needs to read more clearly.

Form submitLink to this section

Button passes every attribute to the element, so type, form, name and value work as on a native button.

Submit button
import { Button, ButtonTitle } from '@systhemaui/next'

export default function Demo() {
  return (
    <form className="flex flex-wrap items-center gap-4" action="#form-submit">
      <Button.button type="submit" variant="primary">
        <ButtonTitle>Subscribe</ButtonTitle>
      </Button.button>
      <Button.button type="reset" variant="secondary">
        <ButtonTitle>Reset</ButtonTitle>
      </Button.button>
    </form>
  )
}

PropsLink to this section

ButtonLink to this section

Button and its shorthands take the props of the element they render. variant is typed from your tokens.

PropTypeDefaultDescription
asElementType'button'
variantButtonVariant (primary, secondary)-
classNamestring-
childrenReactNode-
…and all <button> attributes

ButtonIconLink to this section

ButtonIcon renders a <span> by default and always sets aria-hidden="true" (pass aria-hidden={false} to override). An empty ButtonIcon shows the configured default icon. ButtonTitle renders a <span> and takes its attributes.

PropTypeDefaultDescription
asElementType'svg'
classNamestring-
…and all <svg> attributes

HTML and CSSLink to this section

Without React, put the variant class on any element. One class per variant is generated (button-primary and button-secondary with the default tokens), and an empty .button-icon element draws the default icon:

<a class="button-primary" href="/signup">
  <span class="button-title">Get started</span>
  <span class="button-icon" aria-hidden="true"></span>
</a>

<button class="button-secondary" type="button">
  <span class="button-title">See variants</span>
</button>

The component also adds the transitionClasses from your config; in plain HTML add transition utilities yourself. A project transition token collection sets the duration and easing per variant (see Motion tokens).

ClassStyles
button-primary& { --tw-shadow-color: var(--color-button-primary-normal-shadow); --tw-shadow: var(--tw-shadow-color); vertical-align: bottom; position: relative; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; border-style: solid; border-radius: var(--button-primary-border-radius); border-width: var(--button-primary-border-width); box-shadow: var(--button-primary-shadow-x) var(--button-primary-shadow-y) var(--button-primary-shadow-blur) var(--button-pri…+ 9 more rules
button-secondary& { --tw-shadow-color: var(--color-button-secondary-normal-shadow); --tw-shadow: var(--tw-shadow-color); vertical-align: bottom; position: relative; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; border-style: solid; border-radius: var(--button-secondary-border-radius); border-width: var(--button-secondary-border-width); box-shadow: var(--button-secondary-shadow-x) var(--button-secondary-shadow-y) var(--button-secondary-shadow-blur) var(…+ 9 more rules

Next.jsLink to this section

The @systhemaui/next Button.a renders through the Next-aware LinkHelper: internal routes navigate on the client and prefetch, and same-page hashes scroll smoothly. Passing as="a" to Button takes the same route, not a bare <a>.

import { Button, ButtonIcon, ButtonTitle } from '@systhemaui/next'

export function Cta() {
  return (
    <Button.a href="/contact" variant="primary">
      <ButtonTitle>Talk to us</ButtonTitle>
      <ButtonIcon />
    </Button.a>
  )
}

Button is not a 'use client' module, so its shorthands also work when you import it in a Server Component.

AccessibilityLink to this section

  • ButtonIcon renders with aria-hidden="true", so assistive technology announces only the title.
  • An icon-only button gets exactly one name: a visually hidden ButtonTitle (sr-only!) or an aria-label. Two names make some screen readers read the label twice.
  • Use a real <button> for actions and a link for navigation, so keyboard and screen-reader behavior match what the control does.

See Icons and icon-only controls.