Layout Patterns
Stack, grid, center, container and friends. The layouts every app writes by hand, as typed functions and components.
Most layout code is the same handful of ideas: put these in a column with a gap, line those up in a row and center them, cap the page width and center it, lay cards out in a grid. Written by hand, each one is four or five flex or grid properties, and every screen writes them again.
<div className={css({ display: 'flex', flexDirection: 'column', gap: '6' })}>
<div className={css({ display: 'flex', alignItems: 'center', justifyContent: 'space-between' })}>
<h2>Orders</h2>
<button>Export</button>
</div>
<div className={css({ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: '4' })}>
{cards}
</div>
</div>Patterns name those ideas. Each one is a function that takes a few layout props and returns a class name, so the same screen reads as what it is:
<div className={stack({ gap: '6' })}>
<div className={hstack({ justify: 'space-between' })}>
<h2>Orders</h2>
<button>Export</button>
</div>
<div className={grid({ columns: 3, gap: '4' })}>{cards}</div>
</div>Nothing changes underneath. A pattern call is extracted at build time and produces the same atomic CSS as the css()
call it replaces. There is no runtime, no wrapper element, and no new class of thing to learn: any style property
still works alongside the pattern props.
What you get
- Typed props.
gaptakes spacing tokens,aligntakes thealign-itemskeywords,columnstakes a number. Autocomplete shows what a pattern accepts. - Responsive values. Every prop accepts breakpoint objects, so
direction={{ base: 'column', md: 'row' }}just works. - Two forms. A function under
styled-system/patternsfor class names, and withjsxFrameworkset, a component understyled-system/jsx. - Zero cost. Extracted like
css(), emitting the same atomic classes. - Yours to shape. Change a default, narrow a prop, or write your own. See Patterns.
import { stack } from '../styled-system/patterns'
import { Stack } from '../styled-system/jsx'
<div className={stack({ gap: '4' })} />
<Stack gap="4" />The rest of this page uses the function form. Every example has a JSX twin with the same props.
Pick a pattern
- Stacking:
stackfor a column or row with a gap,hstackandvstackfor the two common cases with items centered,spacerto push siblings apart. - Wrapping and grids:
wrapfor items that flow onto new lines,gridandgridItemfor columns and spans. - Sizing and centering:
containerfor a capped, centered page,centerto center children,squareandcirclefor fixed boxes,aspectRatiofor media. - Positioning:
floatto pin a badge to a corner,bleedto break out of a parent's padding,linkOverlayto make a whole card clickable. - Utilities:
boxfor style props with no opinion,visuallyHiddenfor screen-reader only content,dividerfor a rule,cqto declare a container query.
Stacking
Stack
A flex container with a gap. Defaults to a column.
direction:columnby default, orrow.gap: spacing between children. Defaults to8px.align,justify:align-itemsandjustify-content.
import { stack } from '../styled-system/patterns'
<form className={stack({ gap: '4' })}>
<input />
<input />
<button>Save</button>
</form>HStack
A row with children vertically centered. The everyday toolbar.
gap: defaults to8px.justify:justify-content.
import { hstack } from '../styled-system/patterns'
<header className={hstack({ justify: 'space-between' })}>
<Logo />
<nav className={hstack({ gap: '6' })}>{links}</nav>
</header>VStack
A column with children horizontally centered.
gap: defaults to8px.justify:justify-content.
import { vstack } from '../styled-system/patterns'
<div className={vstack({ gap: '2' })}>
<Avatar />
<span>Segun</span>
</div>Spacer
Fills the free space in a stack so what comes after it sits at the far end.
size: a fixed spacing token instead of filling. Without it the spacer grows.
import { hstack, spacer } from '../styled-system/patterns'
<div className={hstack()}>
<Logo />
<div className={spacer()} />
<button>Sign in</button>
</div>Wrapping and grids
Wrap
A row that flows onto new lines when it runs out of room. Tags, chips, filter pills.
gap,rowGap,columnGap:gapdefaults to8pxunless one of the axis gaps is set.align,justify:align-itemsandjustify-content.
import { wrap } from '../styled-system/patterns'
<ul className={wrap({ gap: '2' })}>{tags}</ul>Grid
Equal columns, or as many columns as fit.
columns: a fixed count.minChildWidth: a size token or length. The grid fits as many columns of at least that width as it can. Use it instead ofcolumns, not with it.gap,rowGap,columnGap:gapdefaults to8pxunless an axis gap is set.
import { grid } from '../styled-system/patterns'
<div className={grid({ columns: { base: 1, md: 3 }, gap: '6' })}>{cards}</div>
<div className={grid({ minChildWidth: '64', gap: '6' })}>{cards}</div>Grid Item
Spans and placement for a child of grid.
colSpan,rowSpan: how many tracks to cover.colStart,colEnd,rowStart,rowEnd: explicit lines.
import { grid, gridItem } from '../styled-system/patterns'
<div className={grid({ columns: 3, gap: '6' })}>
<article className={gridItem({ colSpan: 2 })}>Feature</article>
<aside>Sidebar</aside>
</div>Sizing and centering
Container
Caps the width, centers the block, and adds responsive side padding. One per page.
It sets maxWidth: '8xl', marginX: 'auto', and paddingX: { base: '4', md: '6', lg: '8' }. Override any of them
as a style prop.
import { container } from '../styled-system/patterns'
<main className={container({ maxWidth: '4xl' })}>{children}</main>Center
Centers children on both axes.
inline: useinline-flexso the box sizes to its content.
import { center } from '../styled-system/patterns'
<button className={center({ w: '10', h: '10', rounded: 'full' })}>
<Icon />
</button>Square
A box with equal width and height, children centered.
size: a size token or length.
import { square } from '../styled-system/patterns'
<div className={square({ size: '12', bg: 'gray.100' })}>
<Icon />
</div>Circle
The same as square, rounded. Avatars, status dots, icon buttons.
size: a size token or length.
import { circle } from '../styled-system/patterns'
<div className={circle({ size: '10', bg: 'green.500' })} />Aspect Ratio
Holds a box at a ratio and fits its child to it. Images, video, maps.
ratio: a number. Defaults to4 / 3.
For a plain box, the aspectRatio style property is enough. Reach for the pattern when the child is an iframe or
an image that must fill and crop.
import { aspectRatio } from '../styled-system/patterns'
<div className={aspectRatio({ ratio: 16 / 9 })}>
<iframe src={mapUrl} title="Map" />
</div>Positioning
Float
Pins an element to a corner or edge of its nearest positioned ancestor. Badges, close buttons, counters.
placement:top-start,top-center,top-end,middle-start,middle-center,middle-end,bottom-start,bottom-center, orbottom-end. Defaults totop-end.offset,offsetX,offsetY: distance from the edge, as a spacing token.
The parent needs position: relative.
import { css } from '../styled-system/css'
import { float } from '../styled-system/patterns'
<button className={css({ position: 'relative' })}>
<BellIcon />
<span className={float({ placement: 'top-end', offset: '1' })}>3</span>
</button>Bleed
Pulls an element out past its parent's padding so it runs edge to edge.
inline,block: how far to bleed on each axis. Match the parent's padding.
import { css } from '../styled-system/css'
import { bleed } from '../styled-system/patterns'
<article className={css({ px: '6' })}>
<img className={bleed({ inline: '6' })} src={hero} alt="" />
<p>Body copy stays inside the padding.</p>
</article>Link Overlay
Stretches a link's clickable area over its nearest positioned ancestor, so a whole card is clickable while the markup keeps one real link.
import { css } from '../styled-system/css'
import { linkOverlay } from '../styled-system/patterns'
<article className={css({ position: 'relative' })}>
<img src={cover} alt="" />
<h3>
<a href="/post/1" className={linkOverlay()}>
Read the post
</a>
</h3>
</article>Keep one link per card. A second clickable element inside the card would sit under the overlay.
Utilities
Box
No styles of its own. As a function it is css(). As a component it is a div that takes style props, which is
the reason it exists.
import { Box } from '../styled-system/jsx'
<Box p="4" bg="bg.subtle" rounded="md">
{children}
</Box>Visually Hidden
Removes an element from view but not from the accessibility tree. Labels for icon buttons, skip links, the input behind a custom checkbox.
import { visuallyHidden } from '../styled-system/patterns'
<button>
<TrashIcon />
<span className={visuallyHidden()}>Delete</span>
</button>Divider
A horizontal or vertical rule that fills its axis.
orientation:horizontalby default, orvertical.thickness: a size token or length. Defaults to1px.color: a color token.
import { divider, hstack } from '../styled-system/patterns'
<div className={hstack({ gap: '4', h: '6' })}>
<a href="/docs">Docs</a>
<div className={divider({ orientation: 'vertical', color: 'border' })} />
<a href="/blog">Blog</a>
</div>Container Query
Declares the element as a container, so children can respond to its width instead of the viewport's.
type:containerType. Defaults toinline-size.name: a name fromtheme.containerNames, for@name/sizeconditions.
import { css } from '../styled-system/css'
import { cq } from '../styled-system/patterns'
<aside className={cq({ name: 'sidebar' })}>
<p className={css({ fontSize: { base: 'sm', '@sidebar/md': 'md' } })}>{text}</p>
</aside>The size conditions come from theme.containers. See
container queries.
Responsive props
A pattern prop takes the same breakpoint object as any style property. Put the breakpoints on the prop itself:
<Grid columns={{ base: 1, md: 2, lg: 3 }} gap={{ base: '4', md: '6' }} />
<Stack direction={{ base: 'column', md: 'row' }} />Nesting a pattern prop inside a breakpoint key skips the pattern, so nothing changes at that breakpoint:
// ❌ `columns` inside `md` is never transformed
<Grid columns={1} md={{ columns: 2 }} />
// ✅
<Grid columns={{ base: 1, md: 2 }} />Style props on the same element still accept breakpoint keys the usual way.
Wrapping a pattern component
Patterns are found by name when Panda scans your files. Wrap Stack in your own Section component and the props
passed to Section are invisible to extraction, because nothing tells Panda that Section is a stack.
The jsx option on the pattern's config adds names for Panda to treat as that pattern:
panda.config.ts
patterns: {
extend: {
stack: { jsx: ['Stack', 'Section'] }
}
}Now <Section gap="8" /> is extracted like <Stack gap="8" />. Regexes work too, for a family of wrappers. See
Patterns for the full option.
Make your own
The built-ins cover the layouts every app has. The ones your app repeats that these don't cover, a scrollable region, a two-pane sidebar, a sticky header that clears the nav, are the case for writing a pattern.