dify/packages/dify-ui/src/button/README.md
Joel aee0062cb2
chore: remove redundant aria-label attributes (#41724)
Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
2026-09-03 09:10:16 +00:00

131 lines
4.8 KiB
Markdown

# Button
Use `Button` for an action with a visible text label. Use [`IconButton`] for an icon-only
command, `Toggle` for a persistent pressed state, and a native link when activation navigates to
a URL.
Dify UI `Button` is an opinionated wrapper around [Base UI Button]. It adds Dify variants,
sizes, content spacing, and a `loading` state while preserving the upstream button, focus, and
composition behavior.
## Button semantics
`Button` renders a native `<button type="button">` by default. Set `type="submit"` explicitly
when the button submits a form:
```tsx
<form onSubmit={handleSubmit}>
<Button type="submit">Save</Button>
</form>
```
Do not render a link through `Button`. Base UI applies button semantics, keyboard interaction,
and disabled behavior to the rendered element. Keep navigation on a native anchor or routing
link and reuse only the visual variants:
```tsx
<a className={buttonVariants({ variant: 'secondary' })} href="/settings">
Settings
</a>
```
Use `render` with `nativeButton={false}` only when a non-button element intentionally needs
button semantics. It is not a link mode.
## Loading and disabled states
`disabled` and `loading` describe different facts:
| Prop | Meaning | Default focus behavior |
| ---------- | -------------------------------------------- | ----------------------------------------------------- |
| `disabled` | The action is unavailable. | Native-disabled and removed from the tab order. |
| `loading` | The action was triggered and is now pending. | Activation is blocked while the button retains focus. |
Internally, Dify UI maps these states to Base UI's interaction contract:
```tsx
disabled={disabled || loading}
focusableWhenDisabled={focusableWhenDisabled ?? loading}
```
Base UI recommends disabling a loading button while setting `focusableWhenDisabled` so that an
action does not lose focus after it is triggered. Dify UI's `loading` prop owns that wiring and
adds the visible spinner. The loading button remains in the tab order with `aria-disabled`
instead of the native [`disabled`] attribute. Unlike native disabled, [`aria-disabled`] preserves
focusability but requires the component to suppress activation. Callers should pass the pending
state only to `loading`:
```tsx
<Button loading={isSaving}>Save</Button>
```
Keep independent availability conditions in `disabled`:
```tsx
<Button loading={isSaving} disabled={!canSave}>
Save
</Button>
```
Do not repeat the same pending state in `disabled`:
```tsx
// Incorrect: loading already blocks activation.
<Button loading={isSaving} disabled={isSaving}>
Save
</Button>
// Incorrect: keep only the independent availability condition in disabled.
<Button loading={isSaving} disabled={isSaving || !canSave}>
Save
</Button>
// Correct.
<Button loading={isSaving} disabled={!canSave}>
Save
</Button>
```
It is valid for `loading` and an independent `disabled` condition to both evaluate to `true`.
The loading focus policy applies while the action is pending; when loading ends, the remaining
availability condition still determines whether the button is disabled.
Pass `focusableWhenDisabled={false}` only when a loading button should opt into native disabled
behavior and may leave the tab order.
### Accessible loading feedback
The loading spinner is decorative and does not replace the button's visible label. `Button` does
not add `aria-busy`: [WAI-ARIA `aria-busy`] defines it for an element being modified whose
content changes may be deferred by assistive technology, not as a generic substitute for a
pending action state. When a long-running operation needs an announcement or progress updates,
the feature owns the corresponding status, live region, or progress component.
## Content and spacing
`Button` owns spacing between its direct children. Do not add icon margins or a standard `gap-*`
at call sites:
```tsx
<Button>
<span aria-hidden="true" className="i-ri-rocket-line size-4" />
Launch
</Button>
```
Regular (`medium`) and `large` sizes use 4px and 6px gaps. `small` uses 3px for `primary` and 4px
for the other variants. Use a `className` override only for a documented layout exception.
## Related guides
- Read [`IconButton`] for icon-only actions.
- Read [Accessible names and descriptions] when choosing or changing a naming source.
- Read [Base UI Button] for the upstream interaction and composition contract.
[Accessible names and descriptions]: ../../docs/accessible-names-and-descriptions.md
[Base UI Button]: https://base-ui.com/react/components/button
[WAI-ARIA `aria-busy`]: https://www.w3.org/TR/wai-aria#aria-busy
[`IconButton`]: ../icon-button/README.md
[`aria-disabled`]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-disabled
[`disabled`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/disabled