Skip to content

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

Styled System

css/

The css/ folder — css(), cx(), cva(), sva(), viewTransition(), positionTry(), and keyframes().

Folder: styled-system/css/
Import: import { css, cx, cva, sva, viewTransition, positionTry, keyframes } from '../styled-system/css'

Codegen writes one module per helper (css/css.ts, css/cva.ts, …) plus css/index.ts as the barrel. Every helper maps a style object to a class name string. Panda extracts those styles at build time and emits atomic CSS. At runtime, the helpers only resolve class names: no <style> tags, no theme context, no work on render. See How it works.

css()

Pass a style object, or several to merge, and get back a class name string.

import { css } from '../styled-system/css'
 
const className = css({
  bg: 'blue.500',
  color: 'white',
  px: '4',
  py: '2',
  rounded: 'md',
  _hover: { bg: 'blue.600' },
  md: { px: '5' }
})
// => "bg_blue.500 text_white px_4 py_2 rounded_md ..."
<button className={className}>Save</button>

Multiple arguments, or arrays, merge in order. Later styles win for the same property:

css({ color: 'red.500' }, { color: 'blue.500' }) // blue wins
css([{ color: 'red.500' }, condition && { fontWeight: 'bold' }])

css.raw() returns the merged style object instead of a class name. Use it to pass styles into another helper, a pattern, or a component without generating a class yet:

const base = css.raw({ display: 'flex', gap: '2' })
const styles = css(base, { p: '4' })

Guides: Writing Styles, Merging Styles, Conditional Styles.

cx()

Joins class name strings into one, skipping false, null, and undefined:

import { css, cx } from '../styled-system/css'
 
const base = css({ display: 'flex', gap: '2' })
const active = css({ bg: 'blue.500', color: 'white' })
 
cx(base, isActive && active, className)

cx() does not merge style objects or resolve conflicts between utilities. Merge with css() first, then join the result with other class strings:

// ❌ cx does not merge style objects
cx({ color: 'red' }, { color: 'blue' })
 
// ✅ merge with css, then join with cx
cx(css({ color: 'red' }, { color: 'blue' }), otherClassName)

cva()

An atomic recipe: base styles plus named variants, colocated in the component file. Call the result with a variant map and get a class name.

import { cva } from '../styled-system/css'
 
const button = cva({
  base: {
    display: 'inline-flex',
    alignItems: 'center',
    rounded: 'md',
    fontWeight: 'semibold'
  },
  variants: {
    visual: {
      solid: { bg: 'blue.500', color: 'white' },
      outline: { borderWidth: '1px', borderColor: 'blue.500' }
    },
    size: {
      sm: { px: '2', py: '1', fontSize: 'sm' },
      md: { px: '4', py: '2', fontSize: 'md' }
    }
  },
  defaultVariants: {
    visual: 'solid',
    size: 'md'
  }
})
 
button({ visual: 'outline', size: 'sm' }) // => class name string

The recipe object takes:

  • base: styles that always apply.
  • variants: named axes (size, visual, …) and the styles for each value.
  • compoundVariants: styles for specific combinations of variant values.
  • defaultVariants: values used when a variant is omitted.

Variant props are typed from the definition:

import { cva, type RecipeVariantProps } from '../styled-system/css'
 
type ButtonProps = RecipeVariantProps<typeof button>

Use cva for variants local to one component. For a recipe shared across a design system, define it in config and import it from recipes/ instead. Guides: Atomic recipes, Config recipes, Thinking in Panda.

sva()

cva for components with several parts. One variant map drives every slot, and the call returns a class name per slot.

import { sva } from '../styled-system/css'
 
const card = sva({
  slots: ['root', 'title', 'body'],
  base: {
    root: { rounded: 'lg', borderWidth: '1px', p: '4' },
    title: { fontWeight: 'semibold', mb: '2' },
    body: { color: 'fg.muted' }
  },
  variants: {
    size: {
      sm: {
        root: { p: '3' },
        title: { fontSize: 'sm' }
      },
      md: {
        root: { p: '4' },
        title: { fontSize: 'md' }
      }
    }
  },
  defaultVariants: { size: 'md' }
})
 
const classes = card({ size: 'sm' })
// classes.root, classes.title, classes.body
<div className={classes.root}>
  <h2 className={classes.title}>Title</h2>
  <p className={classes.body}>Body</p>
</div>

The recipe object takes the same keys as cva, plus slots, the list of part names. base, variants, and compoundVariants are then written per slot.

A slot recipe shared across packages belongs in config as defineSlotRecipe, imported from recipes/. Guides: Slot recipes, Config slot recipe, Slot recipe context.

viewTransition()

Returns a shared class built on view-transition-class, so Panda can extract and dedupe view transition styles like any other atomic style. You still set a unique view-transition-name on each element yourself.

import { viewTransition } from '../styled-system/css'
 
const slide = viewTransition({
  group: {
    animationDuration: '0.4s',
    animationTimingFunction: 'ease-in-out'
  },
  imagePair: { isolation: 'isolate' },
  old: { opacity: 0 },
  new: { opacity: 1 }
})
// => "vt_xxx"

The keys group, imagePair, old, and new map to the matching ::view-transition-* pseudo-elements.

View transition names must be unique per element, so keying styles on the name would leave nothing to share. view-transition-class is the CSS feature built for shared animation styles, and this helper emits that class. Guide: View Transitions.

positionTry()

Returns the dashed-ident for a CSS anchor-positioning fallback and emits its @position-try block. Unlike viewTransition(), which returns a class, this returns a value you put in position-try-fallbacks.

import { css, positionTry } from '../styled-system/css'
 
const bottom = positionTry({ top: 'anchor(bottom)', insetInlineStart: 'anchor(start)' })
// => "--pt_xxx"
 
css({ positionTryFallbacks: positionTry('bottom') })

A string argument resolves a named theme.positionTry bag (positionTry('bottom') => "--pt_bottom"); an object hashes to --pt_<hash>. The call folds inside css(), so a fallback list composes in your own style object. Guide: Position Try.

keyframes()

Names an inline @keyframes block and returns the animation name for animationName or the animation shorthand. Object form only — a bare animationName: 'spin' already resolves a theme.keyframes entry.

import { css, keyframes } from '../styled-system/css'
 
const spin = keyframes({ from: { transform: 'rotate(0deg)' }, to: { transform: 'rotate(360deg)' } })
// => "kf_xxx"
 
css({ animation: `${spin} 1s linear infinite` })

The block emits only when its name is referenced, tree-shaken like theme.keyframes. Guide: Keyframes.

See also

Edit this page on GitHubView as markdown
Last updated on