guides
dynamic styling

Dynamic styling

Represent runtime choices without a runtime style engine.

Bamboo style APIs are compile-time syntax. The Vite compiler must be able to recover every declaration a call can produce; there is no runtime CSS fallback.

Choose the representation from the shape of the value:

Runtime inputRepresentation
One value from a finite setA recipe variant
A finite branch between complete stylesA branch between separately compiled calls
A continuous value such as width or offsetThe style attribute, or a CSS variable when the cascade must still reach it
A dynamic token nameA recipe variant that names the allowed token set
An opaque class alongside Bamboo stylescx() — merged semantically if analyzable, a plain string-join otherwise

Finite choices

Declare the possible values as recipe variants. Bamboo compiles every leaf and replaces the runtime choice with a small lookup:

const badge = cva({
  variants: {
    tone: {
      neutral: { color: 'gray.700' },
      danger: { color: 'red.700' },
    },
  },
})
 
export const Badge = ({ tone }: { tone: 'neutral' | 'danger' }) => <span className={badge({ tone })}>Status</span>

An ordinary branch between complete calls is also finite:

const className = compact ? css({ px: '2' }) : css({ px: '4' })

The compiler can rewrite both calls independently. Moving the branch inside a style value is open-ended and rejected:

// Rejected: `color` is a runtime style value.
css({ color: props.color })

Continuous values

A dragged offset, progress percentage, or measured size has no finite rule set. Put an unconditional value in the style attribute:

const track = css({ height: '[4px]', bg: 'blue.500' })
 
export const Progress = ({ value }: { value: number }) => <div className={track} style={{ width: `${value}%` }} />

Use a CSS variable when Bamboo conditions or consumer CSS must still be able to override the property:

const sidebar = css({
  width: 'var(--sidebar-width)',
  _tablet: { width: '100%' },
})
 
export const Sidebar = ({ width }: { width: string }) => (
  <aside className={sidebar} style={{ '--sidebar-width': width } as React.CSSProperties} />
)

The variable value is runtime data; the declaration that reads it remains static and compiles normally.

Token choices

token('colors.red.500') with a literal path is fine where the compiler can evaluate it. A path assembled from runtime data is not:

// Rejected: the build cannot know which token declaration is needed.
token(`colors.${tone}.500`)

Represent that set as recipe variants instead:

const text = cva({
  variants: {
    tone: {
      red: { color: 'red.500' },
      blue: { color: 'blue.500' },
    },
  },
})

Composing with an opaque class

A forwarded className prop has no declarations the compiler can see. cx() is the one call allowed to degrade instead of reject:

import { css, cx } from '../styled-system/css'
 
const styles = css({ borderWidth: '1px', paddingX: '12px' })
 
const Card = ({ className, ...props }) => <div className={cx('group', styles, className)} {...props} />

When every argument is analyzable, cx() merges the underlying styles before allocating classes and later declarations win:

cx(css({ paddingX: '4' }), css({ paddingX: '2' }))
// one atom for padding-inline: 2

An opaque argument like className above removes that guarantee: cx() falls back to a plain string-join, and the opaque class's position in the call gives it no override priority over the compiled styles beside it. This does not fail the build — it is cx()'s documented second contract — but the two are worth telling apart. See classname concatenation for how the resulting cascade race resolves, and source compilation for the opaque-composition diagnostic that flags it.

css.raw()

css.raw() returns a style object for build-time composition; it does not return a class. The outer class-producing call must still compile:

const shared = css.raw({ display: 'flex' })
const className = css(shared, { gap: '4' })

A live css.raw() result passed around at runtime violates the strict compiler contract and fails the build.

See Source compilation for the complete list of accepted and rejected call shapes.