Button

A button in solid, outline and ghost variants.

Example

Installation

pnpm dlx shadcn@latest add superzero11/uiset/button

It lands in components/ui/button.tsx, with UISet’s root (uiset). Wrap your app in <UISet> once, and every component takes its skin.

What UISet adds

The component is UI SDK’s own, with its parts, props and names, so everything below works as UI SDK documents it. UISet adds the look and checks it:

  • Styles in five skins, each in light and dark, from CSS variables you can override.
  • Text at 4.5:1 contrast and control edges and focus rings at 3:1, in every skin and theme, checked with axe in tests.
  • Haptic cues on phones, and sound cues when you set sound on <UISet>.
  • Examples that name every control, so agents can find it (see Agents below).

The API reference and accessibility notes below are UI SDK’s, from its docs for @ui-sdk/react (Apache-2.0). Button on ui-sdk.dev has more usage examples. Your coding agent can read the same page at node_modules/@ui-sdk/react/docs/components/button.md.

API Reference

Button accepts native button props and the options below. render, children, className and style can read the button's state; see Parts and Composition.

Button

  • disabled? (boolean): Stops presses. The button is natively disabled, so it leaves the tab order and its form doesn't submit through it, unless focusableWhenDisabled is set.
  • focusableWhenDisabled? (boolean): Keeps a disabled button in the tab order with aria-disabled instead of the native attribute, so it can hold focus while it becomes disabled and host a tooltip that says why. Presses still do nothing.
  • type? ("button" | "reset" | "submit"): What a native button does in a form: "submit" submits it and "reset" resets it. Ignored when the button renders as something else.
  • pendingAnnouncement? (string): What screen readers hear once the button has been pending for a second, so a slow action doesn't pass in silence. Set it in the page's language. An empty string says nothing.
  • debug? (boolean): Logs every state change with its reason to the console, in development builds.

State

  • disabled (boolean): Whether it's disabled, by the disabled option or a disabled fieldset around it. A running press shows in pending instead.
  • pending (boolean): Whether the Promise the last press returned is still running. Presses do nothing meanwhile.
  • error (unknown): Why the last press failed: what its handler threw, or its Promise rejected with. Cleared by the next press.

Events

PropReceives
onClickReact's click event and { source }, how the button was pressed. Return a Promise to keep the button pending until it settles.

Accessibility

Give the button a clear text label. If it contains only an icon, add an aria-label. Native buttons respond to Enter and Space; links respond to Enter.

Pending Buttons

While its action runs, the button tells assistive technology it's busy, so you don't need to add anything:

  • It sets aria-busy="true" and aria-disabled="true", and ignores presses until the action settles. It keeps focus the whole time.
  • After a second, it announces pendingAnnouncement ("Loading" by default) through a polite live region. Quick actions stay silent. This is a status message: people hear it without focus moving.

To show progress, keep the label and change only the visual, like a spinner. Changing the text renames the button while it has focus: screen readers don't reliably announce a new name on the focused element, so the change mostly helps sighted users, and the button's width shifts as the text changes length. The announcement already tells everyone it's working.

tsx
<Button onClick={save} pendingAnnouncement="Saving">
  <ArrowDownToLineIcon data-show="idle" aria-hidden="true" />
  <LoaderCircleIcon data-show="pending" aria-hidden="true" />
  Save
</Button>
tsx
<Button onClick={save} aria-label="Save">
  {saving ? "Saving…" : "Save"}
  <span aria-live="polite">{saving ? "Saving" : ""}</span>
</Button>

The second button tracks saving itself, shows text its accessible name doesn't contain, and announces the same state twice. If the text has to change anyway, swap it with data-show and leave out aria-label, so the button's name always contains the text on screen. See Pending Content for the markup.

References:

Tested Behavior

The following behaviors are covered by automated tests:

  • Clicking a button runs its handler.
  • Space presses a focused button.
  • Enter presses a focused button.
  • A submit button submits its form.
  • A reset button resets its form.
  • A disabled button can't be focused or pressed.
  • A button rendered as a link stays a link.

Focusable when disabled

  • It stays focusable and says it's disabled.
  • Tab reaches it.
  • Clicking it runs nothing and submits nothing.
  • Space on it runs nothing and submits nothing.
  • Enter on it runs nothing and submits nothing.

Pending

  • A returned Promise makes it busy, and it keeps focus until the Promise settles.
  • While pending, it says it's unavailable with aria-disabled and keeps focus.
  • Still pending after a second, it tells screen readers it's loading.
  • A rejected Promise ends the pending state.

Runs in React, Vue and web components.

Agents

UI SDK’s agent bridge finds this component by its accessible name: give it a visible label or an aria-label, and an agent can read its state and call its actions without screenshots. Building with AI agents shows how to connect one.

This is agent.snapshot() for the example above, read live from this page. Change something in the example, then read it again: the values follow.

agent.snapshot()
[
  {
    "ref": "c10",
    "component": "button",
    "actions": [
      "press"
    ],
    "label": "Create project"
  },
  {
    "ref": "c11",
    "component": "button",
    "actions": [
      "press"
    ],
    "label": "Invite people"
  },
  {
    "ref": "c12",
    "component": "button",
    "actions": [
      "press"
    ],
    "label": "Cancel invite"
  },
  {
    "ref": "c13",
    "component": "button",
    "actions": [],
    "label": "Archived",
    "disabled": true
  }
]