Dynamic styling
How to manage dynamic styling in Panda
While Panda is mainly focused on the statically analyzable styles, you might need to handle dynamic styles in your project.
We recommend that you avoid relying on runtime values for your styles . Consider using recipes, css variables or
data-* attributes instead.
Here are some ways you can handle dynamic styles in Panda:
Runtime values
Using a value that is not statically analyzable at build-time will not work in Panda due to the inability to determine the style values.
App.tsx
import { useState } from 'react'
import { css } from '../styled-system/css'
const App = () => {
const [color, setColor] = useState('red.300')
return (
<div
className={css({
// ā Avoid: Panda can't determine the value of color at build-time
color
})}
/>
)
}The example above will not work because Panda can't determine the value of color at build-time. Here are some ways to
fix this:
Using Static CSS
Panda supports a staticCss option in the config you can use to pre-generate some styles ahead
of time.
panda.config.ts
import { defineConfig } from '@pandacss/dev'
export default defineConfig({
staticCss: {
css: [
{
properties: {
// ā
Good: Pre-generate the styles for the color
color: ['red.300']
}
}
]
}
})Button.tsx
import { useState } from 'react'
import { styled } from '../styled-system/jsx'
export const Button = () => {
const [color, setColor] = useState('red.300')
// ā
Good: This will work because `red.300` is pre-generated using `staticCss` config
return <styled.button color={color} />
}Using token()
The token() function is generated by Panda and contains an object of all tokens by dot-path, allowing you to query for
token's raw value at runtime.
App.tsx
import { useState } from 'react'
import { css } from '../styled-system/css'
import { token } from '../styled-system/tokens'
const Component = props => {
return (
<div
className={css({
// ā
Good: Store the value in a CSS custom property
color: 'var(--color)'
})}
style={{
// ā
Good: Handle the runtime value in the style attribute
'--color': token(`colors.${props.color}`)
}}
>
Dynamic color with runtime value
</div>
)
}
// App.tsx
const App = () => {
const [runtimeColor, setRuntimeColor] = useState('pink.300')
return <Component color={runtimeColor} />
}Using token.var()
You could also directly use the token.var() function to get a reference to the underlying CSS custom property for a
given token:
App.tsx
import { useState } from 'react'
import { token } from '../styled-system/tokens'
const Component = props => {
return (
<div
style={{
// ā
Good: Dynamically generate CSS custom property from the token
color: token.var(`colors.${props.color}`)
}}
>
Dynamic color with runtime value
</div>
)
}
const App = () => {
const [runtimeColor, setRuntimeColor] = useState('yellow.300')
return <Component color={runtimeColor} />
}JSX Style Props
Panda supports forwarding JSX style properties to any element in your codebase.
For example, let's say we create a Card component that accepts a color prop:
Card.tsx
import { styled } from '../styled-system/jsx'
const Card = props => {
return <styled.div px="4" py="3" {...props} />
}Then you add more style props to the Card component in a different file:
App.tsx
const App = () => {
return (
<Card color="blue.300">
<p>Some content</p>
</Card>
)
}As long as all prop-value pairs are statically extractable, Panda will automatically generate the CSS, so avoid using runtime values:
App.tsx
import { useState } from 'react'
const App = () => {
const [color, setColor] = useState('blue.300')
// ā Avoid: Panda can't determine the value of color at build-time
return (
<Card color={color}>
<p>Some content</p>
</Card>
)
}Property names
Extract reads the prop as written. A rename like circleSize to size does not emit CSS. Same for a custom name like
elementWidth. See Extraction rules.
Marking objects with raw()
{fn}.raw() returns a plain style object and marks it for the compiler to extract.
It can be useful, for example, in Storybook args or custom react props.
App.tsx
// mark the object as valid css for the extractor
<Button rootProps={css.raw({ bg: 'red.400' })} />export const Funky: Story = {
// mark this as a button recipe usage
args: button.raw({
visual: 'funky',
shape: 'circle',
size: 'sm'
})
}Enhanced css.raw spreading
Added in v1.6.1
You can also spread css.raw objects within nested selectors and conditions for better style composition:
App.tsx
import { css } from '../styled-system/css'
const baseStyles = css.raw({ margin: 0, padding: 0 })
const interactive = css.raw({ cursor: 'pointer', transition: 'all 0.2s' })
const component = css({
// Spreading in child selectors
'& p': { ...baseStyles, fontSize: '1rem' },
// Spreading in nested conditions
_hover: {
...interactive,
_dark: { ...interactive, color: 'white' }
}
})Static expressions
Panda supports static expressions in your styles, as long as they are statically analyzable.
Static Composition
You can compose different style objects together using the css.raw() function.
App.tsx
import { css } from 'styled-system/css'
const paragraphSpacingStyle = css.raw({
'& p': { marginBlockEnd: '1em' }
})
export const proseCss = css.raw({
'& h1': paragraphSpacingStyle
})This will result in the following CSS:
/* ... */
@layer utilities {
.\[\&_p\]\:mb_1em p,
.\[\&_h1\]\:\[\&_p\]\:mb_1em h1 p {
margin-block-end: 1em;
}
}Supported expressions
Panda resolves an expression when every value it depends on is known at build time.
Literals and arithmetic
import { css } from '../styled-system/css'
const unit = 4
css({ padding: `${unit * 2}px`, opacity: unit > 2 ? 1 : 0.5 })Constants and destructuring
import { css } from '../styled-system/css'
const colors = { text: 'gray.900', muted: 'gray.500' } as const
const { muted } = colors
css({ color: colors.text, borderColor: muted })Spreads and computed keys
import { css } from '../styled-system/css'
const base = { display: 'flex', gap: '2' }
const state = 'hover'
css({ ...base, [`&:${state}`]: { gap: '3' } })Enums and satisfies
import { css } from '../styled-system/css'
enum Size {
Small = '8px',
Large = '16px'
}
const card = { padding: Size.Large } satisfies { padding: string }
css({ ...card, margin: Size.Small })Imports from other files
theme.ts
export const brand = { primary: 'red.500' }
export const spacing = (step: number) => `${step * 4}px`button.ts
import { css } from '../styled-system/css'
import { brand, spacing } from './theme'
css({ bg: brand?.primary, paddingInline: spacing(3) })Tokens and raw styles
import { css } from '../styled-system/css'
import { token } from '../styled-system/tokens'
const focus = css.raw({ outlineWidth: '2px' })
css({ ...focus, boxShadow: `0 0 0 1px ${token('colors.blue.500')}` })Default values
Panda generates the CSS for a parameter's default value, including destructured props:
import { css } from '../styled-system/css'
function Button({ size = 'md' }: { size?: string }) {
return <button className={css({ fontSize: size })} />
}A value passed by the caller isn't known at build time. Use recipes or
staticCss to generate CSS for the other values.
Helper functions
A helper can destructure its parameters, declare local const or let values before its return, spread objects, and
call other helpers:
import { css } from '../styled-system/css'
const rule = (color: string) => ({ color })
function card({ tone, size = 'md' }: { tone: string; size?: string }) {
const border = `1px solid ${tone}`
return { border, fontSize: size, ...rule(tone), '& .badge': rule(`${tone}.700`) }
}
css(card({ tone: 'red' }))Unsupported expressions
Panda does not run a JavaScript interpreter, so it only evaluates helpers made of const / let declarations and one
return. When a helper can't be evaluated, its styles aren't extracted and Panda warns with pure_helper_unevaluated.
The rest of the style object still works.
Control flow
if statements, loops, and more than one return aren't evaluated. Use a ternary instead:
// ā
function card(tone: string) {
if (!tone) return {}
return { color: tone }
}
// ā
const card = (tone: string) => (tone ? { color: tone } : {})Rest and array patterns
Destructuring with ...rest or an array pattern isn't evaluated. Name each value instead:
// ā
const badge = ({ tone, ...rest }: { tone: string; size: string }) => ({ color: tone, fontSize: rest.size })
// ā
const badge = ({ tone, size }: { tone: string; size: string }) => ({ color: tone, fontSize: size })Built-in functions
Calls to built-ins such as Object.entries, Object.fromEntries, Array.prototype.map, and Math.max aren't
evaluated. Write the styles out instead:
import { cva } from '../styled-system/css'
// ā
const sizes = (keys: string[]) => Object.fromEntries(keys.map(key => [key, { paddingX: key }]))
const recipe = cva({ variants: { size: sizes(['1', '2']) } })
// ā
const recipe = cva({
variants: {
size: {
sm: { paddingX: '1' },
md: { paddingX: '2' }
}
}
})This limit applies to styles in your source files. Your panda.config.ts runs in Node, so you can still use built-ins
there to construct configuration values.
Runtime conditions
Even though we recommend that you first look for better alternatives (such as using recipe variants), you may still occasionally need runtime conditions.
When encountering a runtime condition, Panda will first try to resolve it statically. If it can't, it will fallback to the generating the corresponding CSS for each possible branches.
import { useState } from 'react'
import { css } from '../styled-system/css'
import { Stack } from '../styled-system/jsx'
const App = () => {
const [isHovered, setIsHovered] = useState(false)
return (
<Stack
color={isHovered ? { _hover: 'red.100' } : 'red.200'}
_hover={{
color: { base: 'red.300', md: isHovered ? 'red.400' : undefined }
}}
>
<div className={css({ color: isHovered ? 'red.500' : 'red.600' })} />
</Stack>
)
}Since none of the conditions above are statically extractable, Panda will generate css for all possible code path, resulting in a css that looks like this:
/* ... */
@layer utilities {
.hover\:text_red\.100:where(:hover, [data-hover]) {
color: var(--colors-red-100);
}
.text_red\.200 {
color: var(--colors-red-200);
}
.hover\:text_red\.300:where(:hover, [data-hover]) {
color: var(--colors-red-300);
}
@media (width >= 48rem) {
.hover\:md\:text_red\.400:where(:hover, [data-hover]) {
color: var(--colors-red-400);
}
}
.text_red\.500 {
color: var(--colors-red-500);
}
.text_red\.600 {
color: var(--colors-red-600);
}
}
/* ... */Referenced values
Although you should have your styles inlined most of the time, maybe you want to store a value in a variable and re-use in multiple places. This should be fine as long as you keep it statically analyzable.
Panda v2 can resolve named imports of static values from other files, including values passed through re-exports. The imported value must still be statically analyzable:
theme.ts
export const brand = 'red.300'
export const colorName = 'primary'App.tsx
import { css } from '../styled-system/css'
import { brand, colorName } from './theme'
const colors = { primary: 'red.300', secondary: 'blue.300' }
const className = css({
borderColor: brand,
color: colors[colorName]
})Avoid values supplied by React state, component props, or functions that need runtime data. Being imported from another file does not make a runtime value extractable.
import { css } from '../styled-system/css'
// ā
Good: All values are statically extractable
const mainColor = 'red.300'
const sizes = { sm: '12px', md: '16px', '2xl': '42px' }
const App = () => {
return (
<div
className={css({
color: mainColor,
fontSize: sizes.md,
width: sizes['2xl']
})}
/>
)
}Runtime reference on known objects
Using a more complex but still common example :
import { useState } from 'react'
import { css } from '../styled-system/css'
const colorByType = {
primary: 'red.300',
secondary: 'blue.300',
tertiary: 'green.300'
}
const Section = () => {
const [type, setType] = useState('primary')
// ā Avoid: since only "gray.100" is statically extractable here
// This will not work as expected, the color CSS won't be generated
return <section className={css({ color: colorByType[type] ?? 'gray.100' })}>ā Will not be extracted</section>
}Even though colorByType is statically analyzable, Panda does not yet support this kind of automatic extraction
fallback. This is the perfect opportunity to use the recipes.
import { useState } from 'react'
import { cva } from '../styled-system/css'
const sectionRecipe = cva({
base: { color: 'gray.100' },
variants: {
type: {
primary: { color: 'red.300' },
secondary: { color: 'blue.300' },
tertiary: { color: 'green.300' }
}
}
})
const Section = () => {
const [type, setType] = useState('primary')
// ā
Good: This will work as expected
return <section className={sectionRecipe({ type })}>ā
With a recipe</section>
}Not only did you get the same end result, but you also got a more readable and maintainable code !
You can now :
- add more variants to your recipe
- add more properties
- use a shorthand or a condition
All of this with complete type-safety and without having to make drastic changes to the component.
Note that you can also integrate this recipe directly into your theme if you want to only generate the CSS that you use, among other things
Config recipes
Config recipes only emit CSS for variants Panda can statically analyze. button({ size }) works at runtime, but you'll
only get defaultVariants in the stylesheet unless you add staticCss or use string
literals in the call.
See Dynamic variants.
Summary
What you can do
// ā
Good: Conditional styles
<styled.div color={{ base: "red.100", md: "red.200" }} />
// ā
Good: Arbitrary value
<styled.div color="#121qsd" />
// ā
Good: Arbitrary selector
<styled.div css={{ "&[data-thing] > span": { color: "red.100" } }} />
// ā
Good: Runtime value (with config.`staticCss`)
const Button = () => {
const [color, setColor] = useState('red.300')
return <styled.button color={color} />
}
// ā
Good: Runtime condition
<styled.div color={{ base: "red.100", md: isHovered ? "red.200" : "red.300" }} />
// ā
Good: Referenced value
<styled.div color={mainColor} />
// ā
Good: Imported static value (brand is a named constant from another file)
<styled.div color={brand} />
What you can't do
// ā Avoid: Runtime value (without config.`staticCss`)
const Button = () => {
const [color, setColor] = useState('red.300')
return <styled.button color={color} />
}
// ā Avoid: Referenced value that depends on runtime data
<styled.div color={getColor()} />
<styled.div color={colors[getColorName()]} />
<styled.div color={colors[runtimeColorName]} />
// ā renamed prop. See Extraction rules
<CustomCircle circleSize="4" />