packages/dify-ui/src/button/README.md
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 renders a native <button type="button"> by default. Set type="submit" explicitly
when the button submits a form:
<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:
<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.
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:
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:
<Button loading={isSaving}>Save</Button>
Keep independent availability conditions in disabled:
<Button loading={isSaving} disabled={!canSave}>
Save
</Button>
Do not repeat the same pending state in disabled:
// 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.
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.
Button owns spacing between its direct children. Do not add icon margins or a standard gap-*
at call sites:
<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.
IconButton for icon-only actions.