Skip to main content

Button

BetaSince 0.2.0

Runs an action, such as Save or Delete, in five variants and two sizes.

import { Button, IconButton } from "@anantya/uix";

Overview

Use a button to run an action, such as saving a form or deleting an invoice. Give each view one primary button for its main action, and make the others secondary, ghost or danger. IconButton is a square button that shows only an icon, for actions whose icon is familiar, such as copy or edit.

To go to another page, use a link instead. When a link must look like a button, such as a call to action, render your router's link with asChild.

Playground

Note

The playground for Button is coming soon.

Examples

Five variants: primary for the main action, secondary for the others, ghost for quiet actions, danger for destructive ones and link to look like a link.

Code
import { Button } from "@anantya/uix";

export default function Variants() {
return (
<>
<Button>Save changes</Button>
<Button variant="secondary">Cancel</Button>
<Button variant="ghost">Edit</Button>
<Button variant="danger">Delete invoice</Button>
<Button variant="link">View details</Button>
</>
);
}

Two sizes: md, 36px high, is the default, and sm, 32px, fits dense screens such as tables and toolbars.

Code
import { Filter } from "@anantya/icons";
import { Button, IconButton } from "@anantya/uix";

export default function Sizes() {
return (
<>
<Button size="sm" variant="secondary">
Export
</Button>
<IconButton size="sm" variant="secondary" aria-label="Filter invoices">
<Filter />
</IconButton>
<Button variant="secondary">Export</Button>
<IconButton variant="secondary" aria-label="Filter invoices">
<Filter />
</IconButton>
</>
);
}

An icon goes before the label, or after it with iconPosition="end".

Code
import { Add, ArrowRight } from "@anantya/icons";
import { Button } from "@anantya/uix";

export default function WithIcon() {
return (
<>
<Button icon={<Add />}>New invoice</Button>
<Button variant="secondary" icon={<ArrowRight />} iconPosition="end">
Next step
</Button>
</>
);
}

While loading, it shows a spinner, ignores clicks and keeps focus. Click it to see.

Code
"use client";

import { Button } from "@anantya/uix";
import { useState } from "react";

export default function Loading() {
const [saving, setSaving] = useState(false);
const save = () => {
setSaving(true);
setTimeout(() => setSaving(false), 2000);
};
return (
<Button loading={saving} loadingText="Saving…" onClick={save}>
Save changes
</Button>
);
}

A disabled button can't be clicked or reached with Tab, so say next to it what turns it on.

Add a customer to send this invoice.

Code
import { Button } from "@anantya/uix";

export default function Disabled() {
return (
<div className="flex flex-col items-center gap-2">
<Button disabled>Send invoice</Button>
<p className="text-sm text-muted-foreground">Add a customer to send this invoice.</p>
</div>
);
}

IconButton takes the same five variants and two sizes, and its aria-label names the action.

Code
import { Add, Copy, Edit, Settings, Trash } from "@anantya/icons";
import { IconButton } from "@anantya/uix";

export default function IconButtons() {
return (
<>
<IconButton aria-label="Add customer">
<Add />
</IconButton>
<IconButton variant="secondary" aria-label="Invoice settings">
<Settings />
</IconButton>
<IconButton variant="ghost" aria-label="Edit invoice">
<Edit />
</IconButton>
<IconButton variant="danger" aria-label="Delete invoice">
<Trash />
</IconButton>
<IconButton variant="link" aria-label="Copy invoice number">
<Copy />
</IconButton>
</>
);
}

Props

Note

The props table for Button is coming soon.

Accessibility

It follows the WAI-ARIA button pattern. It renders a <button type="button">, so it never submits a form by accident; pass type="submit" for a form's submit button.

KeyAction
TabMoves focus to the next button. A disabled button is skipped; a loading one isn't.
Shift + TabMoves focus to the previous button.
Enter or SpaceRuns its action, unless it is loading. A link rendered with asChild runs on Enter only, like any link.

Screen readers

  • A screen reader announces it as a button named by its label, such as "Save changes, button". IconButton is named by its aria-label, and its icon is hidden from screen readers
  • With asChild, it is announced as its child is, so a link stays a link
  • While loading, it is marked busy and unavailable, and keeps focus, so the reader doesn't lose their place
  • A disabled button is announced as unavailable when the reader browses to it

Contrast

The lowest contrast of its text and icons at rest, on hover and while pressed, on the page and in a dialog. Text needs 4.5:1 and the focus outline 3:1.

VariantLightDark
primary5.2:15.2:1
secondary11.9:17.3:1
ghost16.1:114.2:1
danger6.4:16.8:1
link4.7:15.6:1
Focus outline5.2:16.7:1

A disabled button is exempt from contrast minimums, as WCAG allows for inactive controls. With forced colors on, such as Windows high contrast, primary, secondary and danger keep their edge as a border.

WCAG 2.2

  • 1.4.3 Contrast (Minimum) and 1.4.11 Non-text Contrast, in light and dark
  • 2.1.1 Keyboard: every action works from the keyboard, with Enter or Space, or Enter on a link
  • 2.4.7 Focus Visible: a focus outline shows on keyboard focus
  • 2.5.8 Target Size (Minimum): every button and IconButton is at least 32 by 32px
  • 4.1.2 Name, Role, Value: a native button, named by its label or aria-label, with its busy and disabled state

axe checks every example on this page in CI, in light and dark, in Chromium, Firefox and WebKit.

What your app still has to do

  • Write a label that says what the button does, such as Delete invoice, and an IconButton aria-label that names the action, not the icon
  • When the visible label is shortened, start the aria-label with the visible words, so voice control users can say them
  • Tell the reader what happened after a loading button finishes, such as with announce("Invoice sent")
  • Say on the screen why a button is disabled, or leave it enabled and explain what's missing when it is clicked

Do / Don't

Use one primary button per view, for the main action.

Don't make every action primary. Nothing stands out.

Name what the button does, such as Delete invoice.

Don't use a vague label such as OK or Yes. The reader can't tell what it does.

Name an IconButton after its action, such as Copy invoice number.

Don't name it after its icon, such as Clipboard. A screen reader user hears the icon, not the action.

Use a link, or a Button with asChild, to go to another page.

Don't navigate from a button's onClick. The reader can't open it in a new tab, and screen readers announce a button.

Changelog

Every change to Button since 0.2.0 is in the release notes.

© Anantya TechLabs. All rights reserved.