|
| 1 | +# Button |
| 2 | + |
| 3 | +A button with the disabled behaviour a native `<button>` cannot express: staying focusable while inert, and behaving like a button on elements that aren't one. |
| 4 | + |
| 5 | +## When to Use |
| 6 | + |
| 7 | +- A button that disables itself mid-interaction — while a form submits, while a request is in flight. Use `focusableWhenDisabled` so focus is not dropped to the body and the user keeps their place on the page. |
| 8 | +- Rendering a button as a link or a `<span>`. Pass `nativeButton={false}` so the role, tab order, and Enter/Space activation are applied. |
| 9 | +- A plain always-enabled button needs neither prop, and a bare `<button>` is fine. |
| 10 | + |
| 11 | +## Usage |
| 12 | + |
| 13 | +```tsx |
| 14 | +import { Button } from '@/primitives/button'; |
| 15 | + |
| 16 | +<Button onClick={save}>Save</Button>; |
| 17 | +``` |
| 18 | + |
| 19 | +### Focusable while disabled |
| 20 | + |
| 21 | +```tsx |
| 22 | +<Button |
| 23 | + type='submit' |
| 24 | + disabled={submitting} |
| 25 | + focusableWhenDisabled |
| 26 | +> |
| 27 | + Save |
| 28 | +</Button> |
| 29 | +``` |
| 30 | + |
| 31 | +The button keeps its place in the tab order, is marked `aria-disabled`, and ignores clicks and keyboard activation. Focus is not pulled off its current element by a pointer press either. |
| 32 | + |
| 33 | +Suppression is the consumer's handler plus the event's default action, not propagation. Events still bubble, so an enclosing dialog or menu keeps seeing them. Every key but `Tab` has its default prevented — `Tab` is exempt so focus can still move off the button, which is the point of keeping it focusable. |
| 34 | + |
| 35 | +### Non-native element |
| 36 | + |
| 37 | +```tsx |
| 38 | +<Button |
| 39 | + nativeButton={false} |
| 40 | + render={<a href='/settings' />} |
| 41 | +> |
| 42 | + Settings |
| 43 | +</Button> |
| 44 | +``` |
| 45 | + |
| 46 | +## Props |
| 47 | + |
| 48 | +| Prop | Type | Default | Description | |
| 49 | +| ----------------------- | --------------------- | ------- | ------------------------------------------------------------------ | |
| 50 | +| `disabled` | `boolean` | `false` | Makes the button inert | |
| 51 | +| `focusableWhenDisabled` | `boolean` | `false` | Keeps a disabled button in the tab order | |
| 52 | +| `nativeButton` | `boolean` | `true` | Whether the rendered element is a real `<button>` | |
| 53 | +| `render` | `RenderPropOrElement` | — | Renders a different element (see the shared `render` escape hatch) | |
| 54 | + |
| 55 | +Standard `<button>` attributes pass through. `type` defaults to `"button"` on a native button and is overridable. |
| 56 | + |
| 57 | +## Data Attributes |
| 58 | + |
| 59 | +| Attribute | Description | |
| 60 | +| --------------- | --------------------- | |
| 61 | +| `data-disabled` | Present when disabled | |
| 62 | + |
| 63 | +## ARIA |
| 64 | + |
| 65 | +- Native + disabled: the `disabled` attribute. |
| 66 | +- Native + `disabled` + `focusableWhenDisabled`: `aria-disabled="true"`, no `disabled` attribute. |
| 67 | +- `nativeButton={false}`: `role="button"`, `tabIndex={0}` (`-1` when disabled without `focusableWhenDisabled`, since an `<a href>` is tabbable on its own and dropping the attribute would leave it in the tab order), `aria-disabled` when disabled. `Enter` and `Space` activate it; `Enter` on a link is left to the browser so it does not fire twice. |
0 commit comments