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

Importing

You can import the Button component via:

import { Button } from '@customerio/pluma-components/react';

Variants

Primary

Use a primary button as a CTA for the most important action on a page. Ideally, there should be at most only one primary button on any given page.

<Button variant="primary">primary</Button>

Secondary

A secondary button can be used for any action less important than "primary". There can be multiple secondary buttons on a page. This is the default, if no variant is provided.

<Button variant="secondary">secondary</Button>

Tertiary

A tertiary button can also be used for "secondary" actions, but with less visual weight attached to it.

<Button variant="tertiary">tertiary</Button>

Subtle

A subtle button is another low-priority button type, but without a border, to make it even less prominent than the "tertiary" or "secondary" styles.

<Button variant="subtle">subtle</Button>

The Button component accepts a href prop, which makes it render an anchor element with the button component's styles. This link will behave like the Link component. This means:

  • Button will use the provider's linkComponent (or an a tag for external links)
  • Button will also accept PlainLink props, like isExternal or replace
<Button
  href="https://customer.io"
  isExternal={true}
  variant="primary"
>
  Sign up!
</Button>

For buttons that render just like a link (i.e. renders inline and doesn't include paddings), see the Link component.

Danger states

All button variations support an danger state via the isDanger argument. This should be used for "dangerous" actions, like deleting a model.

Note: isError is the deprecated name for this argument.

<Button variant="primary" isDanger>
  primary
</Button>

<Button variant="secondary" isDanger>
  secondary
</Button>

<Button variant="tertiary" isDanger>
  tertiary
</Button>

<Button variant="subtle" isDanger>
  subtle
</Button>

Size

A button can be medium (default) or small:

<Button variant="primary">medium</Button>

<Button variant="primary" size="small">
  small
</Button>

Icons

A button will render an icon if an icon name is passed in as the icon argument:

<Button variant="primary" icon="manage">Manage</Button>

Icon position

The icon can be positioned on either side with the iconPosition argument, which can be either 'leading' or 'trailing'. 'leading' is the default, if no argument is provided.

<Button variant="primary" icon="manage" iconPosition="leading">
  Manage
</Button>

<Button variant="primary" icon="chevron-right" iconPosition="trailing">
  Go there
</Button>

Icon only button

A button can be rendered with just an icon by setting the isIconOnly argument to true. This will stop the button from rendering the text content, and adjust the paddings so it looks balanced:

<Button variant="primary" icon="manage" isIconOnly={true} />

Custom icons

If you need to use a custom icon that isn't part of the Pluma Icon set, you can use the iconSrc prop to provide a URL to an image. The image will be automatically sized to match the button's icon size:

<Button variant="primary" iconSrc="https://img.icons8.com/?size=256&id=19978&format=png">
  Connect with Slack
</Button>

Custom image icons support the same positioning options as standard icons:

<Button variant="primary" iconSrc="https://img.icons8.com/?size=256&id=19978&format=png" iconPosition="leading">
  Leading icon
</Button>

<Button variant="primary" iconSrc="https://img.icons8.com/?size=256&id=19978&format=png" iconPosition="trailing">
  Trailing icon
</Button>

<Button variant="primary" iconSrc="https://img.icons8.com/?size=256&id=19978&format=png" isIconOnly={true} />

When both icon and iconSrc are provided, iconSrc takes precedence. If iconSrc is an empty string, the component will fall back to using the icon prop.

Disabled buttons

Whenever possible, we prefer using native HTML attributes over custom component arguments. In this component's case, there is an exception for the disabled attribute.

The disabled attribute isn't valid on all HTML elements, which means we can't rely on the browser to universally render the correct disabled style, and disable click events. This is important for situations when we use the Button component for routing library links, where sometimes we would like to disable some of those links.

To work around this limitation, the isDisabled argument should be used instead. This will render the disabled style, and prevent click events from bubbling up.

<Button variant="primary" isDisabled={true}>
  Disabled button
</Button>

Loading states

Buttons are often tied to some sort of asynchronous task like saving data to the server, polling for updates, etc. In those cases, it's a good idea to signal to the user that the UI is not frozen and something is happening behind the scenes.

To show a spinner during an asynchronous action, pass the isLoading argument to a Button. The original button text will be retained so that the button doesn't change shape. Additionally, the button will be disabled so that the user can't click the button again.

<Button variant="primary" isLoading={true}>
  Loading button
</Button>

Because this scenario is so common, you have the option to pass a function that returns a promise to the onClick argument. This will automatically show the spinner while the promise is running. If you want to pass a promise to onClick, but don't want the spinner to display, you can set the autoLoading argument to false.

import { Flex, Button } from '@customerio/pluma-components/react';

export default function Example() {
  const onClick = () => {
    return new Promise((resolve) => setTimeout(() => resolve(), 2_000));
  };

  return (
    <Flex gap="100">
      <Button variant="primary" onClick={onClick}>
        Auto loading button
      </Button>

      <Button variant="primary" onClick={onClick} autoLoading={false}>
        Auto loading button
      </Button>
    </Flex>
  );
}

Legacy styles

The buttons presented in these docs may look different from the buttons in the app. Components in Pluma are based on new Figma specs, based on a new set of design tokens. In addition to that, those components may include new styles (like a new focus state).

To make it possible to use Pluma Components in our apps now without having to update all component instances at once, the Button component includes an optional style override to make it look like the old version. By default, buttons render in the new style.

Legacy styling on buttons can be enabled with the unsafe_isLegacy argument. Compare the focus states below:

<Button variant="primary" unsafe_isLegacy={true}>
  Legacy styles
</Button>

<Button variant="primary">
  New styles
</Button>

In practice, it would be tedious to have to use that flag on all new buttons. To make setting the legacy styles easier in the whole app, there is also a flag that can be passed into the PlumaProvider component at the root of the app. With this flag, all Button components nested in that provider will render in the legacy style. It's still possible to override individual buttons within the provider's context too, if necessary.

<PlumaProvider componentFlags={{ unsafe_useLegacyButton: true }}>
  <Box display="flex" gap="300">
    <Button variant="primary">
      Legacy styles
    </Button>
    <Button variant="primary" unsafe_isLegacy={false}>
      New styles
    </Button>
  </Box>
</PlumaProvider>

API

If true, the button will automatically show the spinner when the onClick function is called and it returns a promise. If false, the spinner will only show if the isLoading prop is set to true. By default, this is true.

If provided, the component will render as a link instead

The name of an icon (from Pluma Icons) to render in the button

Default:leading

Which side of the text the icon should be rendered on. leading renders the icon before the text, trailing renders it after

The source URL of a custom image icon to render in the button. When provided, this will be rendered using the Image component with icon sizing. Use v2 sizing (icon-v2-sm, icon-v2-md, icon-v2-lg, icon-v2-xl) for new implementations. The image will always have alt="" as it is purely decorative - meaning comes from button text or aria-label.

Whether to make the button in the active state

Whether the button should be rendered in an danger state

Disables the button. Use this instead of the native disabled prop, to make sure non-button elements (when used with as) get the correct styles

Deprecated: Use isDanger instead.

When this flag is true, an a tag will be used instead of the provider's linkComponent, even if it exists. Additionally, target="_blank" rel="noopener noreferrer" will be added automatically

When this is set to true, the button won't render any text, and will reduce its side paddings

Shows a spinner in the button overlaid on top of the button's content. The button will act as if disabled while in the loading state.

If provided, the component will render as a button and call this function when clicked. If the function returns a promise, the button will show a spinner while the promise is running. If autoLoading is false, the spinner will not automatically show.

This is passed into the provider's linkComponent. It can be used by the link component implementation to handle replaceState instead of pushState

Default:md

The size of the button. medium and small are deprecated, use md and sm instead.

Change the color of the icon. By default, the icon inherits the button's text color. Use with care, as it can cause inconsistencies in the UI.

Overrides the icon size used within the button. Should only be used deliberately, as it can cause inconsistencies in the UI. For example, this is used in Pagination to increase the icon size.

Whether to make the button look "active", i.e. apply its hover state.

Legacy styling flag for backwards compatibility

Whether to render the button variants as if the upcoming major release that removes the secondary variant has already shipped.

Intended for use after running the codemod at codemods/button-variant-rename. With this flag set, source that has been migrated by the codemod renders the same as it did before:

  • prop secondary renders with tertiary styles
  • prop tertiary renders with subtle styles

Note: this applies to the default variant too. A <Button> with no variant prop defaults to secondary, so it'll render with tertiary styles when this flag is on. That's intentional: the default-variant look post-major is today's tertiary, so the change happens once (when you enable the flag) and not again when the major lands and the flag is removed.

When the next major Pluma version ships:

  • the current secondary variant is removed
  • the current tertiary is renamed to secondary (now rendered natively)
  • the current subtle is renamed to tertiary (now rendered natively)

Once that lands, drop this flag — rendering doesn't change.

Supersedes unsafe_withSoftDeprecatedSecondaryVariant: when this flag is on, the secondary → tertiary mapping is also applied.

Whether to render the button variants with the secondary variant removed. secondary will render as tertiary. In a later release, button variants will be reduced to only primary, secondary, and tertiary:

  • the current secondary variant will be removed
  • the current tertiary will be renamed to secondary
  • the current subtle will be renamed to tertiary This flag allows us to make the buttons look like the target state for the upcoming layout refresh.

Default:secondary

The visual style variant of the button.

This allows turning off the automatic addition of target="_blank" rel="noopener noreferrer". This can be used for links to other protocols like mailto: