Lumen Docs

Button

Primary interactive primitive. Located at apps/web/components/ui/button.tsx.

Variants

| Variant | Use | |---|---| | primary | Main CTA. Accent fill, white text. At most 1-2 per screen. | | ghost | Secondary action. No fill, inherits text color, hover surface. | | icon-only | Square icon button. Used in toolbars, topbars. |

Sizes

| Size | Height | Usage | |---|---|---| | sm | 32px | Topbar actions, row inline actions | | default | 40px | Form submit, modal footer |

Examples

<Button variant="primary">Save changes</Button>
<Button variant="ghost">Cancel</Button>
<Button variant="primary" size="sm">
  <IconPlus width={14} height={14} /> Add user
</Button>
<Button variant="icon-only" size="sm" aria-label="Settings">
  <SettingsIcon />
</Button>

Pairing with icons

When a button has text + icon, put the icon first, use width={14} height={14} for sm and width={16} height={16} for default. The button component already handles the gap via flex.

Loading state

Disabled while submitting:

<Button variant="primary" disabled={saving}>
  {saving ? "Saving…" : "Save changes"}
</Button>

No spinner by default — the text change is enough. Add a spinner only if the wait exceeds ~500ms consistently.

Destructive actions

Use variant="primary" + className="bg-error hover:bg-error/90":

<Button
  variant="primary"
  className="bg-error hover:bg-error/90"
  onClick={handleDelete}
>
  Delete project
</Button>

Pair with window.confirm() or a confirmation modal. Never let a single click destroy data.

Don't

  • Don't add rounded-full to a non-primary button — only the primary button gets the pill shape.
  • Don't use variant="ghost" as a primary CTA — it disappears against the surface.
  • Don't change the button height with h-* — use the size prop.
  • Don't mix icon + text in an icon-only button — use default with icon+text or a <Chip> if you need a tag-like element.