Skip to contentAsheeUIAsheeUI

Button

One action, stated once, with the states a real interface needs.

A button asks for one thing. Its label says what it does, its variant says how important it is, its colour says what it is about, and its state says whether it can be pressed at all. Nothing about it is bespoke: the radius comes from the application's configuration unless the instance states one, and a busy button keeps its width, so the row it sits in does not jump while a request is in flight.

It renders a real <button>, or an <a> when it navigates, the variant decides the look, not the element.

Usage

import { Button } from "@asheeui/web";

<Button onClick={() => save(invoice)}>Save invoice</Button>

An action that navigates takes href instead of onClick:

<Button href="/invoices">All invoices</Button>

Variants

Five treatments. solid is the primary action on a screen; bordered and faded are secondary; ghost belongs in a toolbar or a footer where the boundary would be noise; and underlined is for an action that reads as a link inside prose.

underlined is the one variant that overrides radius: an underlined control with rounded corners is two vocabularies at once, so the radius resolves to none.

Colours

Colour is a role, not a hex value: primary, secondary, success, warning and danger. A custom theme changes what a role looks like, not which roles exist, the set is closed, and a colour name outside it is a configuration warning rather than a new role. The same five on the secondary treatment read as a lighter set, which is what a page uses for a row of equivalent actions.

Sizes and width

Density is the same scale the fields use, so a button and an input in one row line up. An icon-only button keeps the square footprint of its size, and fullWidth makes the action fill its column, useful at the foot of a form or inside a card.

Radius

Radius resolves from the application's defaultRadius unless the instance states one, so a product that rounds everything to full states it once.

States

A button is available, unavailable or busy. isDisabled marks it unavailable; isLoading marks it busy, and a busy button is also unavailable, pressing it twice is the mistake the state exists to prevent.

Both states keep the label: a control that changes its text while it works moves everything around it. The state is announced instead, through aria-disabled and aria-busy.

Content

startContent and endContent take a node either side of the label, an icon, a glyph, a count. icon switches to the compact layout, which is square and has no visible label, so it needs one for assistive technology.

href renders an anchor with the button's classes, so it can be opened in a new tab, copied as a URL and announced as a link. The link prop does the same for a framework's own router component, pass its component and props and AsheeUI substitutes it:

import { Link } from "@tanstack/react-router";

<Button
  link={{
    component: Link,
    props: { to: "/invoices" },
  }}
>
  All invoices
</Button>

Props

The defaults are worth reading as a cascade rather than a list. variant and colour are intentionally not registered by the component: they come from the application's defaultVariant and defaultColor. size is registered as md. radius resolves from the application's defaultRadius. So a page with no configuration gets a faded, primary, md button with an xs radius, the framework's deliberate boxy default, and the same page with two lines of configuration gets the product's button everywhere, without restating it.

Accessibility

  • The element is a real <button type="button">, so Enter and Space activate it, it sits in the tab order without a tabindex, and a form does not submit when it is pressed.
  • isDisabled sets aria-disabled and cancels the press in the handler. The control stays focusable and findable: a disabled action that disappears from the tab order is a control the reader cannot discover.
  • isLoading sets aria-busy and disables interaction the same way. The label does not change, so the announcement says what is happening rather than what replaced it.
  • The focus ring is drawn with focus-visible, so it appears for a keyboard and not for a mouse.
  • An icon-only button has no visible text, so it must be given aria-label.
  • When href or link is used the element is an anchor and announces as a link: an action that navigates should never look like one that does not.

Native

@asheeui/native ships the same Button with the same props and the same cascade. The platform's differences are the platform's: the control is the native pressable, the press state is the device's, and href follows the platform's URL handler instead of rendering an anchor. See React Native.