Responsive layout

Breakpoints, media queries, responsive style utilities and hooks for layouts that adapt to the viewport.


Pluma has one set of breakpoints, shared by components and apps. Use them instead of writing pixel widths into your own media queries, so that every part of the product changes layout at the same widths.

Breakpoints

Each breakpoint is a minimum width. A style for md applies from 768px up, so write the narrow layout as the default and add changes for wider screens ("mobile first"). Below sm, no breakpoint applies; the hooks call this base.

Breakpoints
NameApplies fromMedia query
sm576pxscreen and (min-width: 576px)
md768pxscreen and (min-width: 768px)
lg1024pxscreen and (min-width: 1024px)
xl1280pxscreen and (min-width: 1280px)

Prefer CSS over JavaScript for responsive layout. CSS changes with the viewport before the page paints, and it works during server rendering. Use the hooks only when the content itself has to change, not just its layout. The AppLayout responsive header uses both in an app header.

Media queries in your styles

@customerio/pluma-components/css exports mediaQueries, ready to use as a vanilla-extract @media key, and breakpoints, the same widths as numbers.

import { style } from '@vanilla-extract/css';
import { mediaQueries } from '@customerio/pluma-components/css';

export const sidebar = style({
	display: 'none',

	'@media': {
		[mediaQueries.lg]: {
			display: 'block',
		},
	},
});

Media and container queries can't read CSS variables, which is why breakpoints are plain values rather than tokens. For a container query, build the condition from breakpoints:

import { style, createContainer } from '@vanilla-extract/css';
import { breakpoints } from '@customerio/pluma-components/css';

const card = createContainer('card');

export const cardActions = style({
	'@container': {
		[`${card} (max-width: ${breakpoints.sm}px)`]: {
			flexDirection: 'column',
		},
	},
});

Responsive props

Layout props accept an object keyed by breakpoint as well as a single value. Each key applies from that width up, and base applies below sm. This works on Box and every component that takes Box props, and on the layout props of Flex, Stack, InlineStack and Grid.

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

<Stack gap={{ base: '200', md: '400' }}>
	<Flex direction={{ base: 'column', md: 'row' }} justifyContent={{ base: 'start', lg: 'end' }}>
		<Box>One</Box>
		<Box>Two</Box>
	</Flex>
	<Box display={{ base: 'none', lg: 'block' }}>Shown from 1024px up</Box>
</Stack>

You don't need to set base. Without it, the component's own value applies below the first breakpoint you set, so you only describe what changes on wider screens. <Flex display={{ md: 'none' }}> is a flex container below md and hidden from md up, and <Spinner gap={{ md: '400' }}> keeps Spinner's own gap below md.

These component props accept per-breakpoint values:

  • Flex: direction, wrap, justifyContent, alignItems, gap, rowGap, columnGap, display.
  • Stack: gap, alignBlock, alignInline, display.
  • InlineStack: gap, alignBlock, alignInline, wrap, display.
  • Grid: columns, rows, areas, autoFlow, gap, rowGap, columnGap, justifyContent, alignItems.
  • SkeletonText: textAlign.
  • List and ListItem: gap.
  • CardSection and CardHeader: mt, mb and my. Below the first breakpoint you set, the section keeps its gap to the sections around it.

In your own class names

sprinkles(), the function behind the style utilities, takes the same values. Use it for elements that aren't Pluma components.

import { sprinkles } from '@customerio/pluma-components/css';

const layout = sprinkles({
	display: 'flex',
	flexDirection: { base: 'column', md: 'row' },
	gap: { base: '100', md: '400' },
});

Style utility props and sprinkles() accept per-breakpoint values for these properties:

p, py, px, m, my, mx, display, flexDirection, justifyContent, alignItems, flexWrap, textAlign, pt, pr, pb, pl, mt, mr, mb, ml, gap, rowGap, columnGap, w, minW, maxW, h, minH, maxH

Responsive utility props cover layout, flexbox alignment, spacing and sizing. If you need another property to change by breakpoint, write a vanilla-extract style with mediaQueries.

Props also accept raw CSS values in square brackets at each breakpoint. For example, React can use px={{ md: '[2rem]' }} and Ember can use @px={{hash md="[2rem]"}}. In Vite apps, register the style utility plugin for these values.

Hooks

When the content has to change with the viewport, read the breakpoint from JavaScript. Both frameworks share one listener per query, however many components use it.

useMediaQuery(query) returns whether a media query matches. useBreakpoint() returns the largest breakpoint the viewport matches: 'base', 'sm', 'md', 'lg' or 'xl'. Both update when the viewport crosses a breakpoint.

import { useBreakpoint, useMediaQuery } from '@customerio/pluma-components/react';
import { mediaQueries } from '@customerio/pluma-components/css';

function Navigation() {
	const isLargeUp = useMediaQuery(mediaQueries.lg);
	const breakpoint = useBreakpoint();

	return isLargeUp ? <FullNav /> : <CompactNav compact={breakpoint === 'base'} />;
}

During server rendering, useMediaQuery returns false and useBreakpoint returns 'base', then both update on the client.