Skip to content

Want to skip the docs? Check out pandamastery.com - the best way to learn Panda CSS

Customization

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 codegen

Spread 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 exist

blocklist 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 exist

blocklist 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.

Edit this page on GitHubView as markdown
Last updated on