Style utilities

All Pluma components accept a set of utility props to aid with layout and styling.


In Pluma, most of our components inherit from Box. This means that any properties that would work on Box, will also work on the given component.

Utility styles ship with @customerio/pluma-components/css, so your app must import that entry. It already does if it imports lightTheme or darkTheme from there for PlumaProvider. Importing components alone doesn't load utility styles.

Sometimes, components will extend another component that extends from Box. For instance, here is an example of the Paragraph component utilizing the underlying Box properties. Paragraph itself doesn't have a margin-left (ml) attribute, but because it extends Text, which in turn extends Box, ml becomes available to use on Paragraph.

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

<Paragraph ml='200'>I can have Box properties on me!</Paragraph>

Please see PlumaBox documentation for more details.

Layout props such as display, flexDirection, gap, padding and margin also accept an object keyed by breakpoint, like flexDirection={{ base: 'column', md: 'row' }}; see Responsive layout.

Vite plugin

Add plumaSprinkles to your Vite config so the utility values your app uses get CSS:

import { defineConfig } from 'vite';
import { plumaSprinkles } from '@customerio/pluma-components/vite';

export default defineConfig({
	plugins: [plumaSprinkles()],
});

The plugin reads src/**/*.{js,jsx,ts,tsx,gjs,gts,hbs,mdx} under the Vite root. To read other files, set sources to a list of globs; a glob that starts with ! excludes files.

The stylesheet has CSS only for the values the plugin finds in those files and in Pluma's components, token values without a breakpoint included. Write values as literals, and safelist the ones you compute at runtime. A literal is a value like px="300", a choice between literals like px={isWide ? '400' : '200'}, or a breakpoint object of literals like px={{ base: '100', md: '[2rem]' }}.

The plugin also reads literals held in the same file: a constant like const PANEL_PADDING = '250' used as p={PANEL_PADDING}, a lookup table like GAPS[size] where const GAPS = { sm: '050', md: '100' }, and a function or class getter that only returns literals, like @px={{this.contentPaddingX}}. A value that comes from another file, such as an imported constant, is set at runtime.

Values set at runtime

A value the plugin can't read, like px={padding} or @px={{this.padding}}, is set at runtime. The dynamicValues option decides what a value set at runtime gets.

'keep-all', the default, keeps every token value of the prop where it is set: px={padding} keeps every px token without a breakpoint, and px={{ md: padding }} keeps every px token at md. If padding can itself be a breakpoint object, like { base: '100', md: '400' }, add a safelist entry for each breakpoint it uses, like px=*@md.

plumaSprinkles({ dynamicValues: 'keep-all' });

'safelist' keeps only the values safelist lists.

plumaSprinkles({ dynamicValues: 'safelist', safelist: ['px=*', 'gap=300@md'] });

'error' fails the production build, and the dev server at startup, when a value is set at runtime and no safelist entry covers it. A value set at runtime that you add while the dev server runs is logged as an error. An entry like px=* covers px={padding} without a breakpoint only, so when padding can be a breakpoint object, add px=*@md and the other breakpoints it uses as well.

plumaSprinkles({ dynamicValues: 'error', safelist: ['px=*'] });

Pluma's components set some props at runtime too. Every token value of those props is kept where they are set, whatever the policy, and the report below names them.

Values the plugin can't see

These values need safelist entries whatever the policy:

  • Props passed through a spread of a variable, like <Box {...boxProps} /> or @unsafe_props={{this.boxProps}}. An object written inline in the spread, like <Box {...(isCompact && { px: padding })} />, is read.
  • Props in an object built elsewhere and passed on later. The plugin reads a plain object only when its keys are all style utility props, like { px: paddingX, py: paddingY }.
  • Values in files outside sources.

To safelist the values of a constant from another file, import it in the Vite config and build the entries from it. The constant's module must not import app or framework code, since the Vite config loads it:

import { SECTION_GAPS } from './src/layout/section-gaps';

plumaSprinkles({
	dynamicValues: 'safelist',
	safelist: Object.values(SECTION_GAPS).map((value) => `gap=${value}`),
});

In development, Pluma logs a console warning for a value that has no CSS, with the safelist entry to add.

Safelist

List the values that are set at runtime, or used only in a file the plugin doesn't read, in safelist:

plumaSprinkles({
	safelist: ['px=[2rem]@md', 'display=*@lg'],
});

Each entry is prop=value, optionally followed by @ and a breakpoint name. A value of * stands for every token value of the prop, so display=*@lg covers every display value from the lg breakpoint up. Set emitAllTokens: true to include every token value at every breakpoint, for example in a preview build.

Build report

When a production build or the dev server starts, the plugin prints the values set at runtime, grouped by prop and breakpoint, with each one's file, line and the safelist entry that covers it:

[pluma-sprinkles] 1 sprinkle value set at runtime (dynamicValues: 'keep-all'):
  px: safelist 'px=*'
    src/Card.tsx:12  px={padding}
  Every token value of these props is kept, for values without a breakpoint.
  A breakpoint object held in a variable, like padding = { base: '100', md: '400' }, also needs an entry per breakpoint it uses, like 'px=*@md'.

A group that the safelist already covers is marked (covered by safelist). The report is empty when every value is a literal. With emitAllTokens: true, the plugin prints no report.

The plugin is optional. Without it, every token value works at every breakpoint, and bracketed values have no CSS.

Arbitrary values

Square brackets pass a raw CSS value to a style utility prop instead of a token. Every prop that sets a single CSS property accepts them, including spacing and sizing props, display, position, cursor, zIndex, flexbox alignment and text props; truncate, visuallyHidden, centerBaseline and centerBaselineBefore don't. Spacing and sizing props take CSS lengths, percentages, calc() and var(). Shorthand props such as px and m apply the value to each of their sides, and a breakpoint object changes the value at a breakpoint:

<Box px="[21px]" display="[contents]" />
<Box px={{ md: '[2rem]' }} />

A value without brackets must be one of the prop's tokens.

When you write your own vanilla-extract styles, see Interaction states for the @customerio/pluma-components/css/utils helpers that paint hover, pressed, disabled and subtle-fill styles the way Pluma components do.