dify/packages/dify-ui/src/button/README.md

4.1 KiB

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:

<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.

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.

loading owns Base UI's disabled interaction, retained focus, and the decorative spinner. The button remains in the tab order with aria-disabled, and Dify UI suppresses activation. Pass the pending state only to loading; keep independent availability conditions in disabled:

<Button loading={isSaving} disabled={!canManageSettings}>
  Save
</Button>

Pass focusableWhenDisabled={false} only when a loading button should opt into native disabled behavior and may leave the tab order.

Accessible loading feedback

The spinner is decorative. Keep a non-empty visible label throughout loading. If the visible label stays the same, its text continues to name the button.

When the label changes while the focused button enters loading, give the changing text a stable ID and reference it explicitly. Some browser and screen-reader combinations do not reliably announce changes to a focused button's descendant text:

const labelId = useId()

<Button loading={isSaving} aria-labelledby={labelId}>
  <span id={labelId}>{isSaving ? 'Saving' : 'Save'}</span>
</Button>

The consumer owns this relationship because only it knows whether the label changes and whether other visible context must also be referenced. Do not replace the changing text with aria-label.

Button does not add aria-busy: WAI-ARIA aria-busy describes an element whose own updates may be deferred by assistive technology, not a generic pending action. Long-running announcements and progress remain with the feature's status, live-region, or progress owner.

Content and spacing

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.