Patterns
Change what the built-in layout patterns do, or turn the layouts your app repeats into patterns of its own.
The built-in layout patterns cover what every app needs. Your app has layouts of its own that they don't cover: a scrollable region, a two-pane view with a fixed sidebar, a sticky header that clears the nav. Today those live as copied style objects, and they drift.
A custom pattern gives such a layout a name, a small set of typed props, and one place to change it. After codegen it behaves exactly like a built-in: a function, a JSX component, responsive props, and build-time extraction.
Everything on this page goes under patterns.extend in the config, and takes effect after panda codegen.
Change a default
Start small. Built-ins carry defaults, and defaultValues overrides them. The base preset gives stack a gap of
eight pixels; most design systems want a spacing token there instead.
panda.config.ts
import { defineConfig } from '@pandacss/dev'
export default defineConfig({
patterns: {
extend: {
stack: {
defaultValues: { gap: '4' }
}
}
}
})A gap at the call site still wins. hstack and vstack are separate patterns with their own defaults, so change
each one you use.
Narrow a prop
properties describes what a pattern accepts. Overriding one entry under extend replaces that prop's definition
and leaves the rest alone. Here flex is limited to two directions:
panda.config.ts
export default defineConfig({
patterns: {
extend: {
flex: {
properties: {
direction: { type: 'enum', value: ['row', 'column'] }
}
}
}
}
})Write a pattern
A pattern is properties for its props and a transform that turns them into a style object. Keep each one in its
own file with definePattern, which types the props for you, then register it.
patterns/scroll-area.ts
import { definePattern } from '@pandacss/dev'
export const scrollArea = definePattern({
properties: {
axis: { type: 'enum', value: ['x', 'y'] },
hideScrollbar: { type: 'boolean' }
},
defaultValues: {
axis: 'y',
hideScrollbar: false
},
transform(props, { map }) {
const { axis, hideScrollbar, ...rest } = props
return {
overflowX: map(axis, (v) => (v === 'x' ? 'auto' : 'hidden')),
overflowY: map(axis, (v) => (v === 'y' ? 'auto' : 'hidden')),
scrollbarWidth: hideScrollbar ? 'none' : undefined,
'&::-webkit-scrollbar': { display: hideScrollbar ? 'none' : undefined },
...rest
}
}
})panda.config.ts
import { defineConfig } from '@pandacss/dev'
import { scrollArea } from './patterns/scroll-area'
export default defineConfig({
patterns: {
extend: { scrollArea }
}
})panda codegenSpread rest last so callers can still pass any style property. map is explained under
Responsive props. The generated pattern then has the same three forms as a built-in one:
import { scrollArea } from '../styled-system/patterns'
import { ScrollArea } from '../styled-system/jsx'
scrollArea({ axis: 'x' })
scrollArea.raw({ axis: 'x' })
<ScrollArea axis="x" />The function returns a class name. raw returns the style object, for composing into css() or a recipe. The
component renders a div with the props applied.
Property types
The type of each property decides how the prop is typed and how its value resolves:
properties: {
// Typed and resolved like that CSS property. Most props are this.
align: { type: 'property', value: 'alignItems' },
// A fixed set of strings.
size: { type: 'enum', value: ['sm', 'md', 'lg'] },
// A token from that category, resolved through the given CSS property,
// so `thickness: '1'` becomes the `sizes.1` variable.
thickness: { type: 'token', value: 'sizes', property: 'borderWidth' },
// Plain values, handed to `transform` as they are.
columns: { type: 'number' },
inline: { type: 'boolean' },
label: { type: 'string' },
// `description` becomes the JSDoc on the generated prop.
ratio: { type: 'number', description: 'Width divided by height' }
}Responsive props
Callers can pass any pattern prop as a breakpoint object, the same way they would a style property. A list that scrolls sideways on phones and down on wider screens:
<ScrollArea axis={{ base: 'x', md: 'y' }}>{items}</ScrollArea>Inside transform, that prop arrives as the object, not a string, so a direct comparison like axis === 'x' is
never true. The map helper runs your function once per breakpoint value and builds the responsive result for you:
transform(props, { map }) {
const { axis, ...rest } = props
return {
overflowX: map(axis, (v) => (v === 'x' ? 'auto' : 'hidden')),
overflowY: map(axis, (v) => (v === 'y' ? 'auto' : 'hidden')),
...rest
}
}For the call above, overflowX becomes { base: 'auto', md: 'hidden' }. Use map whenever the output depends on
the value. A prop passed straight through, like gap in stack, needs no helper.
Dynamic defaults
defaultValues can be a function of the incoming props. The base preset's grid sets a default gap only when the
caller hasn't set a more specific one:
defaultValues(props) {
return { gap: props.columnGap || props.rowGap ? undefined : '8px' }
}Value helpers
Alongside map, transform receives three checks for props that accept both tokens and raw CSS:
transform(props, { map, isCssUnit, isCssVar, isCssFunction }) {
const { inline, ...rest } = props
// '12px', '2rem', '50%' → isCssUnit
// 'var(--gutter)' → isCssVar
// 'calc(…)', 'clamp(…)' → isCssFunction
const resolve = (v: string) => (isCssUnit(v) || isCssVar(v) ? v : `token(spacing.${v}, ${v})`)
return {
'--bleed-x': map(inline, resolve),
marginInline: 'calc(var(--bleed-x, 0) * -1)',
...rest
}
}This is how the base preset's bleed accepts inline: '6' and inline: '2rem' alike.
Restrict props
By default a pattern accepts its own props plus every style property, so callers can add padding or a background to
a scrollArea without a wrapper. Two options tighten that at the type level.
strict keeps only the props in properties. Use it for a pattern that shouldn't double as a styling escape hatch:
patterns/scroll-area.ts
export const scrollArea = definePattern({
strict: true,
// ...
})scrollArea({ axis: 'x', p: '4' })
// ^ Object literal may only specify known properties, and 'p' does not existblocklist allows everything except the listed properties. Use it when the pattern owns a property and a caller
overriding it would break the layout:
patterns/scroll-area.ts
export const scrollArea = definePattern({
blocklist: ['overflow', 'overflowX', 'overflowY'],
// ...
})scrollArea({ axis: 'x', p: '4', overflow: 'visible' })
// ^ Object literal may only specify known properties, and 'overflow' does not existblocklist is marked experimental in the types.
JSX hints
With jsxFramework set, each pattern also becomes a component. Its name is the pattern name in PascalCase, so
scrollArea gives ScrollArea. Three options shape it:
patterns/scroll-area.ts
export const scrollArea = definePattern({
// Component name. Defaults to `ScrollArea` here.
jsxName: 'ScrollArea',
// Element it renders. Defaults to `div`.
jsxElement: 'section',
// Extra component names, or regexes, that count as usages of this pattern during extraction.
jsx: ['ScrollArea', /ScrollArea$/],
// ...
})jsxName is about what codegen emits. jsx is about what extraction recognizes, and matters when you wrap the
generated component in one of your own, like a PageScrollArea. See
Wrapping a pattern component.
Common mistakes
Missing extend. Without it, your patterns replace every built-in one.
// ❌ stack, grid, center… are gone
patterns: { scrollArea }
// ✅
patterns: { extend: { scrollArea } }Skipping codegen. The function, the component, and their types exist only after panda codegen. The CLI and
the bundler plugins run it for you when the config changes.
Branching on a responsive prop without map. axis === 'x' is never true when axis is
{ base: 'x', md: 'y' }. See Responsive props.
Reaching for a pattern where a recipe fits. Patterns are layout helpers with a handful of props. A component with named visual variants belongs in a recipe.
Starting from zero
Everything above builds on the base preset's patterns. To ship none of them and define only your own, see Minimal setup.