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.
As a link
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 atabindex, and a form does not submit when it is pressed. isDisabledsetsaria-disabledand 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.isLoadingsetsaria-busyand 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
hreforlinkis 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.