Buttons are interactive elements that allow the user to perform an action on the page or serve as a prominent hyperlink.

Button

Usage

  • Use Button to perform an action on the current page — open a modal or drawer, submit a form. Buttons and hyperlinks are not functionally interchangeable.
  • Use a link/anchor to navigate to a new location, download a file, or open an email or phone URL.
  • Give a Button href to render it as a hyperlink — reserve this for CTAs and prominent standalone links.
  • Limit one primary Button per page or workflow.
  • Order actions by importance, most important on the right through least important on the left.
  • Never pick a variant or the danger state to match a color theme — use them for their intended meaning.

Types

  • Regular — a label, optionally with an icon. The default and most common type.
  • Icon-only (isIconOnly) — the icon alone with reduced side padding. Use in condensed layouts and to open menus or dropdowns. Requires icon or iconSrc.
  • Link (href) — renders as an anchor. Use when the Button is a navigating CTA.

Variants

  • variant="primary" — the single most important action on the page, including CTAs and form submission.
  • variant="secondary" — lesser actions. Multiple per page are fine.
  • variant="subtle" — the least important action. Called "Tertiary" in design language.

Appearance

  • size="md" — the default; use in the majority of cases.
  • size="sm" — for smaller screens and less prominent actions.
  • icon — a Pluma icon by name, placed before the label by default. Adds visual context to the action.
  • iconPosition="trailing" — moves that icon after the label.
  • iconSrc — a custom image icon by URL, for icons absent from the Pluma library. Sizing matches Pluma icons; check how its color and style read against each variant's background.
  • iconLeading / iconTrailing — icons pinned to one side. Use them together when a Button needs an icon on both sides. They render in addition to icon, never in place of it.
  • elementBefore / elementAfter — arbitrary content such as a count badge. elementBefore sits between any leading icon and the label; elementAfter between the label and any trailing icon.

Behaviors

  • Loading (isLoading) — overlays a spinner on the content and blocks interaction. Signals a background task in progress.
  • Auto-loading (autoLoading, default true) — shows the spinner automatically while an onClick handler's returned promise is pending. Set false to drive the spinner from isLoading alone.
  • Disabled (isDisabled) — blocks interaction and lightens the colors in every variant. Use it instead of the native disabled attribute.
  • Danger (isDanger) — applies the critical color treatment for destructive actions and errors. Combines with any variant.
  • Active (isActive) — applies the active visual state programmatically.
  • Hover, focus, and the 2px focus outline are built in — no props control them.

Content

  • Start the label with a verb describing the action.
  • Keep labels to 1–3 words.
  • Use sentence case with no period.
  • Prefer a specific label over a generic one; generic is acceptable when context makes the action clear.
  • Keep verbiage consistent across the application.
  • Follow the label patterns: Verb + object for primary actions ("Create campaign"), Delete + object for destructive, Permanently delete + object for irreversible, Cancel for cancel, Save / Save changes / Save and close for save, Upload / Upload file for uploads, and a forward-motion verb ("Next", "Continue") for progressive actions.
  • Add an ellipsis () when the Button opens a dialog or needs more input before the action completes. Never add one for an immediate action.
  • Use a present participle plus ellipsis for loading-state labels ("Saving…").
  • For AI-agent entry points, label the Button with a common product question ("How to import people?").

Implementation Notes

  • Content — React passes the label as children. Ember yields the default block: <PlumaButton>Label</PlumaButton>.
  • Tertiary naming mismatch — the design docs' "Tertiary" is variant="subtle". A literal variant="tertiary" also exists with different styling, so do not substitute it.
  • isIconOnly drops the label only when an icon is actually present (icon, iconSrc, iconLeading, or iconTrailing). Give icon-only Buttons an aria-label — the custom-icon image always renders alt="", so the accessible name has to come from the label or aria-label.
  • href is ignored when isDisabled is set: the component renders a <button> rather than a disabled link. Otherwise href renders through PlainLink as an <a>.
  • Default element is <button type="button">. Override with as.
  • unsafe_withSoftDeprecatedSecondaryVariant — renders secondary with tertiary styling, for the layout-refresh transition. Also settable globally through PlumaProvider's componentConfig.PlumaButton.
  • unsafe_withMigratedButtonVariants — shifts variants down one level (secondarytertiary styling, tertiarysubtle styling), including the default variant. Intended for after the button-variant-rename codemod (pnx @customerio/pluma-cli upgrade --codemod button-variant-rename). Supersedes unsafe_withSoftDeprecatedSecondaryVariant. Also settable through PlumaProvider.
  • unsafe_iconColor / unsafe_iconSize override the icon's inherited color and its size-derived sizing. Use deliberately — they cause UI inconsistency.
  • Wrapping in ButtonGroup (or ToggleButton) supplies defaults for variant, size, isDisabled, isLoading, unsafe_iconColor, and unsafe_iconSize; explicit props on the Button win for variant, and the group wins for size.
  • Deprecated props — use isDanger instead of isError, isActive instead of unsafe_isActive, size="md"/size="sm" instead of "medium"/"small". unsafe_isLegacy is deprecated with no replacement; components already render in the target style.
  • Extends Box — sprinkle props (spacing, layout) are supported.
  • Ember invocation: <PlumaButton @variant="primary" @icon="manage">Label</PlumaButton>. elementBefore and elementAfter are available both as args (taking a component) and as the :elementBefore / :elementAfter named blocks; the blocks take precedence.

On this page