---
title: Source compilation
description: How Bamboo compiles style APIs to shared atoms and rejects runtime styling.
---

The Vite integration is a compiler, not an optional optimization. It runs in development and production, and Bamboo
style APIs in application source are compile-time syntax. There is no switch that restores runtime styling.

```tsx
// source
export const title = css({ display: 'flex', color: 'red.300' })

// transformed module
export const title = 'fs_lg fw_bold'
```

The virtual stylesheet contains the two shared declaration atoms. A second `css()` call or any recipe selecting either
declaration reuses the same class and rule.

## Why compilation is mandatory

A fallback model has two expensive properties:

- one unresolved call retains the generated style engine and its lookup tables;
- recipe-specific classes prevent declarations such as `display: flex` from being shared with other recipes or `css()`
  calls.

Bamboo instead gives the build one contract: every style-producing call must be compiled, or the build fails. This makes
JavaScript removal, global CSS deduplication, and graph-based CSS pruning reliable rather than best-effort.

## The compiled representation

Recipe identity is not style identity. The compiler resolves a recipe selection to authored declarations, merges them in
recipe order, and only then allocates global atoms.

```tsx
const row = cva({ base: { display: 'flex' } })

export const fromRecipe = row()
export const fromCss = css({ display: 'flex' })
```

Both values contain the same class for `display: flex`, and the stylesheet contains that rule once. The source filename,
recipe variable, configured recipe name, slot name, and call site do not enter the declaration identity.

The production CSS emitter:

1. converts observed recipe declarations to utility atoms;
2. omits the recipe layer and all named recipe rules;
3. keeps explicit `staticCss` atoms as a safelist;
4. removes source-graph atoms no transformed module can emit;
5. compacts the remaining class names; and
6. hashes the final CSS bytes after pruning.

Development uses the same atoms and compact naming. It keeps the full extracted atom set because Vite loads modules
lazily, and hot-replaces the virtual stylesheet when source changes.

## What compiles

### `css()` and patterns

All arguments and style values must be statically recoverable. Multiple arguments are merged before atoms are allocated,
so later values preserve normal `css(a, b)` precedence.

```tsx
css({ color: 'red.300' }, { color: 'blue.500', display: 'flex' })
// emits only the blue color atom and the display atom
```

An open runtime style value has no finite CSS rule set and is rejected:

```tsx
// rejected: `tone` can be any CSS value
const className = css({ color: tone })
```

Express runtime choices as declared recipe variants, or make a finite choice explicit in source. Use `staticCss` when
classes are constructed outside Bamboo's analyzable source graph. A value with no finite set of possibilities — a
progress width, a dragged offset — has no rule a build could emit for it, so it belongs in a CSS variable the compiled
rule reads, or in the `style` attribute: see
[dynamic styling](/docs/guides/dynamic-styling#values-that-are-not-a-finite-set).

`css.raw()` deliberately does not compile to a class: it returns a style object. A live use of it fails the strict
compiler contract because it retains the style runtime.

### Inline `cva()` and `sva()` recipes

The recipe declaration is extracted and erased from the transformed module. Static selections resolve directly to shared
atoms:

```tsx
const badge = cva({
  base: { display: 'flex' },
  variants: {
    tone: {
      quiet: { color: 'gray.500' },
      loud: { color: 'red.500' },
    },
  },
})

badge({ tone: 'quiet' })
```

When a declared variant axis is chosen at runtime, Bamboo enumerates its finite state space at build time. Each leaf is
a complete, precedence-correct StyleSet, and a small `cvaMap` decision table selects among those precompiled class
strings. The style config and recipe engine do not ship.

```tsx
export const className = (tone) => badge({ tone })
// becomes a cvaMap lookup whose leaves are complete shared class strings
```

`undefined` remains distinct from `null` and unknown values so defaults behave correctly. Compound variants are resolved
in every leaf. Slot recipes can return either one selected slot or a precompiled slot object.

The Cartesian product is bounded by `maxRecipeStates` (65,536 by default). Make axes static, split an unusually large
recipe, or raise the bound deliberately when a recipe exceeds it.

Spreads, computed selection keys, unresolved recipe configs, and reflective reads such as `recipe.config` are rejected.
`recipe.splitVariantProps(props)` is compiled to the generated `splitProps` helper and does not retain the recipe
object.

#### What actually decides whether an inline recipe compiles

Not where it is declared, and not whether its variants are chosen at runtime — both of those compile, in the declaring
module and across module boundaries alike. What decides it is whether every **reference to the binding** is a call the
compiler rewrote.

The declaration is erased, so the binding's value becomes `undefined`. A reference that is not a compiled call therefore
has nothing behind it, and the build fails with `runtime-binding` rather than shipping a value that would be `undefined`
at runtime:

```tsx
export const badge = cva({ ... })

badge({ tone }) // compiles — a call, rewritten to a cvaMap lookup
export const alias = badge //   runtime-binding — the value itself is read
badge.raw({ tone: 'loud' }) //  runtime-binding — returns a style object, not a class
```

Exporting is fine, and so is calling the recipe from another module — that call compiles like any other. Each module
answers only for its own text, so a module that reads the binding reports itself, and one that only calls it reports
nothing. The diagnostic names the file and line of the read, which is what has to change, not the declaration.

### Config recipes

Config recipes use the same lowering as inline recipes. Static selections become literal atom strings; scalar variant
props become a finite `cvaMap` decision table; config slot recipes become precompiled slot objects. Responsive or
conditional values belong inside the recipe's style declarations, not in a runtime variant selection.

### `cx()`

When every Bamboo style argument and external class literal is statically analyzable, `cx()` is resolved semantically:
the compiler merges the StyleSets before allocating classes, removes overridden declarations, and preserves literal
third-party classes.

```tsx
cx(css({ color: 'red.300' }), css({ color: 'blue.500' }))
// contains only the blue declaration atom
```

For a finite dynamic recipe plus static Bamboo styles, the static styles are composed into every decision-table leaf. A
`cx()` whose arbitrary class inputs are runtime values remains the tiny string-joining helper; Bamboo makes no
conflict-resolution guarantee for that shape.

### Tokens

`token()` and `token.value()` calls with a statically known path compile to string literals. Unknown paths and dynamic
paths fail rather than retaining the complete runtime token dictionary.

<Callout type="warning">
  Not inside a recipe config. A `token()` **call** in a `cva`/`sva` config is not resolved, which leaves the recipe
  declaration standing and reports the `cva` import as `runtime-binding` at the declaration — a diagnostic that names
  neither the config nor the call. Use the string form, which the utility resolves and the compiler accepts:

```tsx
cva({ base: { outline: '2px solid token(colors.accent)' } }) // ✅
cva({ base: { outline: `2px solid ${token('colors.accent')}` } }) // ❌ runtime-binding
```

A `token()` call inside `css()` is unaffected.

</Callout>

### View transitions

A static `viewTransition()` bag compiles to its compact class literal. The CSS optimizer treats its carrier selector,
`::view-transition-*` selectors, and `view-transition-class` declaration as one renamable, prunable unit. A dynamic bag
is rejected because its runtime hash would name CSS the build could not emit.

## Compiler errors

The compiler scans the output plan as well as recognized calls. A Bamboo import passed around, re-exported, used
reflectively, or missed because a transform threw is still a survivor and fails the build.

```text
bamboocss: 2 call(s) could not be compiled.

  src/card.tsx
    18: css() — dynamic
  src/styles.ts
    7: badge — runtime-binding
```

Common reasons:

| reason             | meaning                                                                    |
| ------------------ | -------------------------------------------------------------------------- |
| `dynamic`          | The call contains an open value or source shape the compiler cannot prove. |
| `recipe-call`      | An inline recipe selection is not a finite analyzable shape.               |
| `raw-call`         | The API returns a style object rather than a class.                        |
| `unsupported-kind` | The result shape cannot be represented as the requested class value.       |
| `runtime-binding`  | A Bamboo runtime binding remains referenced after planned rewrites.        |
| `compile-failed`   | Compilation of the module threw; no clean result can be claimed.           |

`not-imported` is not an error: it means a local or third-party function happened to use the same name. An `overlapping`
inner candidate is owned by the enclosing compiled expression.

Use `reportSkipped: true` for per-file diagnostics. The summary is enabled by default.

## Required graph agreement

Import `virtual:bamboo.css` exactly once. A build fails if compiled classes exist without that virtual stylesheet, or if
a transformed source module is outside Bamboo's configured `include` graph. Both cases would otherwise produce class
values with no backing rules.

The cascade-layer polyfill is incompatible with compiled atom pruning and is rejected. The compiler emits the layer
order itself; because named recipe rules are absent, the order is `reset, base, tokens, utilities`.

See [Using Vite](/docs/installation/vite#plugin-options) for the remaining tuning and diagnostic options.
