---
title: Configuring Bamboo
description: Customize how Bamboo works via the `bamboo.config.ts` file in your project.
---

Customize how Bamboo works via the `bamboo.config.ts` file in your project.

```js
import { defineConfig } from '@bamboocss/dev'

export default defineConfig({
  // your configuration options here...
})
```

## Output css options

### presets

**Type**: `(string | Preset | Promise<Preset>)[]`

**Default**: `['@bamboocss/preset-base', '@bamboocss/preset-bamboo']`

The set of reusable and shareable configuration presets.

By default, any preset you add will be smartly merged with the default configuration, with your own configuration acting
as a set of overrides and extensions.

```json
{
  "presets": ["@bamboocss/preset-base", "@bamboocss/preset-bamboo"]
}
```

`presets` is the complete list: what you write is what is loaded. Leaving it unset loads `defaultPresets` (the two
above); setting it to `[]` loads nothing, which is what the removed `eject: true` meant. To add a preset without
restating the defaults:

```ts
import { defaultPresets } from '@bamboocss/dev/presets'

export default defineConfig({
  presets: [...defaultPresets, myPreset],
})
```

### preflight

**Type**: `boolean` | `{ scope?: string; level?: 'element' | 'parent'; prune?: boolean }`

**Default**: `false`

Whether to enable css reset styles. See also [Global styles](/docs/concepts/global-styles) for how reset interacts with
global variables and layering, and [`preflight.prune`](#preflightprune) for dropping the parts of the reset that style
elements your source never renders.

Enable preflight:

```json
{
  "preflight": true
}
```

You can also scope the preflight; Especially useful for being able to scope the CSS reset to only a part of the app for
some reason.

Enable preflight and customize the scope:

```json
{
  "preflight": { "scope": ".extension" }
}
```

The resulting `reset` css would look like this:

```css
.extension button,
.extension select {
  text-transform: none;
}

.extension table {
  text-indent: 0;
  border-color: inherit;
  border-collapse: collapse;
}
```

Setting `level: 'element'` (defaults to `parent`) puts the scope on the element instead – `button.extension` rather than
`.extension button` – so only elements carrying the scope class are reset.

#### preflight.prune

Whether to drop the parts of the reset that style elements your source never renders.

Two thirds of the reset is bound to specific elements – 41 of them, covering `table`, `pre`, `kbd`, `optgroup` and the
rest of the long tail. The reset is a fixed size, so it dominates a small stylesheet rather than amortising like the
utilities layer does.

```json
{
  "preflight": { "prune": true }
}
```

Measured on the example apps in this repository:

| app     |    raw |   gzip | brotli |
| ------- | -----: | -----: | -----: |
| vite-ts | -13.2% | -14.8% | -14.2% |
| svelte  | -33.9% | -29.1% | -29.3% |

Unlike most CSS size work, this holds up under compression, because it emits less rather than spelling the same thing
differently.

A selector list loses only the parts naming unrendered elements, so a rule shared between `button` and
`::file-selector-button` keeps the half that still applies. A rule goes only when every part does. `html` and `body` are
never removed, and a selector naming no element at all – `*`, `::backdrop`, `[hidden]`, a class – is always kept, since
nothing about it says which element it reaches.

<Callout type="warning">
  This one cannot be made safe by default, and it is the only pruning option here that cannot. The token and keyframe
  passes have something to prove reachability against; this has a textual scan of your own source and nothing else. An
  element rendered by a dependency's component, by `dangerouslySetInnerHTML`, or by markdown is invisible to it. What
  you get wrong is an element quietly losing its reset – no error, no warning, just a `<table>` that has its browser
  default border spacing back. Reach for it when you control the markup, and check the result.

Bamboo's own documentation site is the example: its `include` covers `./src` and `./app`, its prose lives in `.mdx`
under `content/`, and the scan therefore sees 49 elements. Turning this on there removes the reset for twenty it never
sees — including `h5`, which the docs render today, and `input`, which a search dialog renders from a dependency. It
saves 450 bytes of a 2,973-byte reset, or 165 compressed.

</Callout>

Because being wrong is silent, the build names what it removed — all of it, once per project — whenever this is on:

```
🎋 info [prune:preflight] Reset rules removed for 20 element(s) your source never renders: abbr, audio, b, canvas,
dialog, embed, h5, h6, iframe, input, menu, object, optgroup, progress, samp, select, small, sub, sup, textarea.
Anything rendered by a dependency, by markdown, or through `dangerouslySetInnerHTML` is invisible to this scan —
check the list, or set `preflight: { prune: false }`.
```

The list says a rule for that element went, not that nothing styles it any more.

**Check your entry template first.** The scan reads the files [`include`](#include) covers, and `include` conventionally
covers components rather than markup: `./src/**/*.tsx` does not match `index.html`, where `<noscript>`, a static
`<table>` and the rest of a page's hand-written markup usually live. Name the template – or any other markup outside
`include`, such as an email template or a server-rendered layout – alongside your components; the scan reads any file
`include` covers, not only ones the parser understands:

```ts
include: ['./src/**/*.{ts,tsx}', './index.html']
```

Each file is read from disk rather than from the parsed copy the build holds, so a `.svelte` or `.vue` file keeps the
markup its transform to TSX would have dropped.

A [scoped reset](#preflight) is handled – `preflight: { scope: '.app' }` writes `.app table`, and the scope is stripped
before the element is read out. `bamboo cssgen preflight` prunes as well, so the `reset.css` it writes matches the one a
full build produces.

### emitTokensOnly

**Type**: `boolean`

**Default**: `false`

Whether to only emit the `tokens` directory

### prefix

**Type**: `string | { cssVar?: string; className?: string }`

**Default**: `''`

The namespace prefix for the generated css classes and css variables. A string prefixes both; the object form sets them
separately.

The examples in this section all use the same call, referred to below as the shared example:

```tsx
import { css } from '../styled-system/css'

const App = () => {
  return <div className={css({ color: 'blue.500' })} />
}
```

Ex: when using a prefix of `bamboo`

```json
{
  "prefix": "bamboo"
}
```

would result in:

```css
.bamboo-text_blue\.500 {
  color: var(--bamboo-colors-blue-500);
}
```

### layers

**Type**: `Partial<CascadeLayers>`

**Default**: `{ reset: 'reset', base: 'base', tokens: 'tokens', recipes: 'recipes', utilities: 'utilities' }`

Cascade layers used in generated css.

Ex: when customizing the utilities layer

```json
{
  "layers": {
    "utilities": "bamboo_utilities"
  }
}
```

the [shared example](#prefix) would result in:

```css
@layer bamboo_utilities {
  .text_blue\.500 {
    color: var(--colors-blue-500);
  }
}
```

You should update the layer in your root css also.

### separator

**Type**: `'_' | '=' | '-'`

**Default**: `'_'`

The separator for the generated css classes.

Using a `=`, the [shared example](#prefix) would result in:

```css
.text\=blue\.500 {
  color: var(--colors-blue-500);
}
```

### minify

**Type**: `boolean`

**Default**: `false`

Whether to minify the generated css.

```json
{
  "minify": true
}
```

Measured on the example apps in this repository:

| app     |    raw |  gzip | brotli |
| ------- | -----: | ----: | -----: |
| vite-ts | -21.6% | -5.6% |  -4.8% |
| svelte  | -20.4% | -6.6% |  -7.3% |

If you import the stylesheet through a bundler you probably do not need this – Vite, Next.js and any PostCSS setup with
`cssnano` already minify CSS in their production build. It matters when you ship `styled-system/styles.css` as-is:
linked straight from an HTML file, served from a CDN, or published inside a component library.

Pass `--minify` to [`bamboo cssgen`](/docs/references/cli#cssgen) to enable it for a single build without changing the
config. [`bamboo debug`](/docs/references/cli#debug) always writes unminified css, whatever this is set to.

### hash

**Type**: `boolean | { cssVar: boolean; className: boolean }`

**Default**: `false`

Whether to hash the generated class names / css variables. This is useful if want to shorten the class names or css
variables. `true` hashes both; the object form hashes each independently.

For the [shared example](#prefix), the emitted class and variable are:

| setting                              | class             | variable                 |
| ------------------------------------ | ----------------- | ------------------------ |
| `false`                              | `.text_blue\.500` | `var(--colors-blue-500)` |
| `true`                               | `.dOFUTE`         | `var(--cgpxvS)`          |
| `{ cssVar: true, className: false }` | `.text_blue\.500` | `var(--cgpxvS)`          |
| `{ cssVar: false, className: true }` | `.dOFUTE`         | `var(--colors-blue-500)` |

This option applies to every build path, Vite included — it is the only thing that shortens class names. Vite's compiler
composes StyleSets before naming and then emits the names the stylesheet is written under, so hashing changes what those
names look like without changing `cx` semantics.

## File system options

### gitignore

**Type**: `boolean`

**Default**: `true`

Whether to update the .gitignore file.

Will add your [`outdir`](#outdir) to your `.gitignore` file:

```txt
## Bamboo
styled-system
```

### cwd

**Type**: `string`

**Default**: `process.cwd()`

The current working directory.

```json
{
  "cwd": "src"
}
```

### clean

**Type**: `boolean`

**Default**: `false`

Whether to clean the output directory before generating the css.

### outdir

**Type**: `string`

**Default**: `styled-system`

The output directory for the generated css.

### importMap

**Type**: `string | ImportMapInput | Array<string | ImportMapInput>`

**Default**:
`{ "css": "styled-system/css", "recipes": "styled-system/recipes", "patterns": "styled-system/patterns", "tokens": "styled-system/tokens" }`

Allows you to customize the import paths for the generated outdir.

```json
{
  "importMap": {
    "css": "@acme/styled-system",
    "recipes": "@acme/styled-system",
    "patterns": "@acme/styled-system"
  }
}
```

A string is the base import path with the default entrypoints appended – `"@scope/styled-system"` resolves to
`@scope/styled-system/css`, `/recipes`, `/patterns` and `/tokens`:

```json
{
  "importMap": "@scope/styled-system"
}
```

An array of either form is also accepted, and matches an entrypoint imported from any of them.

Check out the [Component Library](/docs/guides/component-library) guide for more information on how to use the
`importMap` option.

### include

**Type**: `string[]`

**Default**: `[]`

List of files glob to watch for changes.

```json
{
  "include": ["./src/**/*.{js,jsx,ts,tsx}", "./pages/**/*.{js,jsx,ts,tsx}"]
}
```

### exclude

**Type**: `string[]`

**Default**: `[]`

List of files glob to ignore.

### dependencies

**Type**: `string[]`

**Default**: `[]`

Explicit list of config related files that should trigger a context reload on change.

> We automatically track the config file and (transitive) files imported by the config file as much as possible, but
> sometimes we might miss some. You can use this option as a workaround for those edge cases.

```json
{
  "dependencies": ["path/to/files/**.ts"]
}
```

### watch

**Type**: `boolean`

**Default**: `false`

Whether to watch for changes and regenerate the css.

This is the CLI's own watcher — `bamboo --watch`. A bundler integration does not read it and does not need it:
`@bamboocss/vite` re-extracts as you edit source, and declares your config to Vite as a config file, so editing it — or
a preset it imports — restarts the dev server.

### poll

**Type**: `boolean`

**Default**: `false`

Whether to use polling instead of filesystem events when watching.

### outExtension

**Type**: `'mjs' | 'js'`

**Default**: `mjs`

File extension for generated javascript files.

### forceConsistentTypeExtension

**Type**: `boolean`

**Default**: `false`

Whether to force consistent type extensions for generated typescript .d.ts files.

If set to `true` and `outExtension` is set to `mjs`, the generated typescript `.d.ts` files will have the extension
`.d.mts`.

```json
{
  "forceConsistentTypeExtension": true
}
```

### LightningCSS

Use LightningCSS by installing `@bamboocss/plugin-lightningcss` and listing it:

```ts
import { pluginLightningcss } from '@bamboocss/plugin-lightningcss'

export default defineConfig({
  plugins: [pluginLightningcss()],
})
```

There was a `lightningcss: boolean` option whose only job was to push that plugin into this list. Naming the plugin from
inside `@bamboocss/node` made it a static import, so every project installed a native binary whether or not the flag was
ever set — the package is separate precisely so that cost is opt-in.

### browserslist

**Type**: `string[]`

**Default**: unset – `@bamboocss/plugin-lightningcss` fills it in from the browserslist config found in your project
when you do not set it.

Browserslist query to target specific browsers. Only used by the LightningCSS optimizer.

```json
{
  "browserslist": ["last 2 versions", "not dead", "not < 2%"]
}
```

### polyfill

**Type**: `boolean`

**Default**: `false`

Polyfill CSS @layers at-rules for older browsers.

```json
{
  "polyfill": true
}
```

## Design token options

### shorthands

**Type**: `boolean`

**Default**: `true`

Whether to allow shorthand properties.

| setting | accepted                                        |
| ------- | ----------------------------------------------- |
| `true`  | `css({ bgColor: 'gainsboro', p: '10px 15px' })` |
| `false` | longhand only – `backgroundColor`, `padding`    |

### prune

**Type**:
`{ tokens?: boolean; keepTokens?: string[]; unresolvedPath?: 'off' | 'warn' | 'error'; propertyRegistrations?: boolean; keyframes?: boolean }`

**Default**: `{ tokens: true, unresolvedPath: 'off', propertyRegistrations: true, keyframes: true }`

What to drop from the generated stylesheet. Each key is an independent switch, and setting one keeps the defaults for
the rest. The reset is not here — it is pruned by [`preflight.prune`](#preflightprune), alongside the option that emits
it and the `scope` that pruning has to strip.

```json
{
  "prune": { "tokens": true, "keyframes": true }
}
```

#### prune.tokens

Whether to drop token css variables nothing asks for. On by default.

The token layer declares every token in your theme, and an app typically uses a small fraction of them, so this is
usually the largest single saving in render-blocking css. It scales with the size of your design system rather than the
size of your app. Across the example apps in this repository it is worth 10–67% of the gzipped stylesheet.

```json
{
  "prune": { "tokens": false }
}
```

A variable is kept when the generated css references it, when a kept variable's own value references it, or when
javascript under [`include`](#include) names it — a `token()` or `token.value()` call, or a literal `var(--x)` written
by hand. Paths are read through a constant or a template literal the extractor can follow, not only from a literal
spelled at the call. Anything a theme refers to is kept as well, since themes ship as separate artifacts injected at
runtime.

A path the build **cannot** follow keeps every declaration rather than dropping one something still asks for: `token()`
hands back a `var()` for every token, so an unreadable path could name any of them. That fallback is what
[`unresolvedPath`](#pruneunresolvedpath) reports and [`keepTokens`](#prunekeeptokens) replaces with a bound you declare.

Turning it off does **not** bring back the `@property` rules your utilities register: those are not tokens, so
reachability never applied to them, and a preset ships the set it composes from regardless of what you draw.

<Callout>
  This was three strategies — `'off' | 'reachable' | 'accounted'` — which conflated how hard to try with what to say
  when it fails. `'reachable'` answered one cheap question, *does any javascript reach for a token*, and threw away
  everything else it had read, so a single `token()` call anywhere kept all 468 declarations of the default preset.
  `'accounted'` did the work properly but was framed as an assertion, so it reported by default and had to be asked for.
  Doing the work is now the default, saying so is [`unresolvedPath`](#pruneunresolvedpath), and a file that never spells
  `token` is skipped entirely — so the accounting costs nothing where there is nothing to account for.
</Callout>

#### prune.unresolvedPath

What to do about a token path the build cannot follow. `'off'` (the default) falls back and says nothing; `'warn'` falls
back and reports; `'error'` fails the build. The keeps are identical across all three — this decides how loudly, and
nothing else. It is inert under `tokens: false`, which keeps everything and runs no accounting pass.

It defaults to `'off'` because pruning is an inference the build makes on its own rather than a claim you asked it to
check. Reach for `'warn'` when the token layer is larger than you expect — it names what is holding the keep set open:

```
⚠ tokens:unresolved  2 token reference(s) could not be resolved, so every token declaration is kept.

  src/chart.tsx
    14: unresolved-reference
  src/theme.ts
    3: unclassified-import
```

Set `prune: { unresolvedPath: 'error' }` to assert that every token path in your project is spelled out at the call, so
the fallback can never ship unnoticed. Reach for `'warn'` first: it runs the same accounting and prints the same
references, so you can see what `'error'` would reject before a build depends on it.

A reference it cannot read is never pruned — the declaration it might have named is kept. So the assertion is never less
safe than leaving it off, and the message tells you why when it cannot prune.

##### A dynamic path is bounded, not declined

``token(`colors.${shade}`)`` cannot say which token it wants, but it says which it **cannot**. The static head is a
bound on everything the expression can produce, so the `colors` category is kept and nothing else — no decline, no
fallback. That covers the commonest dynamic read outright, and it is worth knowing before you conclude that your project
cannot be pruned:

```ts
// bounded — keeps `colors.*`, prunes the rest
token(`colors.${shade}` as Token)

// unfollowable — nothing bounds it
token(key)
token('colors.' + shade)
```

##### A local `token` is not a token reference

`token` is the obvious name for a token _object_, and binding one shadows nothing:

```ts
items.map((token) => token.value) // a parameter, not the artifact
```

Bamboo resolves the binding rather than the spelling, so a parameter (destructuring included), a catch variable, a
function or class declaration, a named function or class expression, and a variable destructured off one of those are
all skipped. A type or class member named `token` names a property and reads nothing, so it is skipped too.

What still declines is anything whose value the build cannot see:

```ts
const { token } = useTheme() // an initializer that could be the artifact, through a barrel
const { token } = ui // a namespace import, likewise
theme.token(key) // a member on an object bamboo never bound
```

#### prune.keepTokens

Token paths to keep whatever the build can see, as exact names or `*` patterns.

```json
{
  "prune": { "keepTokens": ["colors.*"] }
}
```

This is the bound the build could not infer, written by hand. It exists because the fallback is otherwise total: **one**
reference the accounting cannot follow keeps every declaration in the project, so a codebase with a single `token(key)`
in it ships the same stylesheet as one that never prunes.

It does two things — it keeps what it matches, and it stands in for what could not be followed, in place of the blanket
keep. Measured on `sandbox/vite-ts` with one unfollowable `token(key)` in it:

| setting                        | declarations | stylesheet |
| ------------------------------ | -----------: | ---------: |
| no `keepTokens`                |          426 |   23,412 B |
| `keepTokens: ['colors.*']`     |          270 |   17,867 B |
| `keepTokens: ['colors.red.*']` |           51 |   10,649 B |

Patterns match the dotted token path, anchored and case-sensitively, with `*` standing for any run of characters and a
leading `!` excluding. `colors.*` keeps every colour, `colors.brand.*` one palette, `colors.red.300` one token, and
`['colors.*', '!colors.legacy.*']` every colour but one palette.

<Callout type="warning">
  Patterns match the token **path**, not the css variable. A token is `fontSizes.3xl` and its declaration is
  `--font-sizes-3xl`, so `font-sizes.*` — the natural thing to write after reading `styles.css` — matches nothing at
  all. The same goes for `lineHeights`, `letterSpacings`, `aspectRatios` and every other multi-word category. A category
  name on its own (`colors`) is not a path either.
</Callout>

A pattern matching no token is reported rather than ignored, and names the spelling that would have worked, because it
is nearly always a typo and keeping nothing is otherwise silent. A list holding **only** exclusions is reported too: `!`
subtracts from a selection, so `['!colors.legacy.*']` on its own selects everything else rather than nothing.

<Callout type="warning">
  Saying `keepTokens: ['colors.*']` is saying **the reads you cannot follow land in colours** — an assertion about your
  own code, which is why nothing infers it for you. Nothing verifies it: a read landing outside the patterns loses its
  declaration and resolves to a `var()` with nothing behind it, which inherits rather than falling back. The references
  being covered are still printed under `'warn'`, so check them.
</Callout>

`'error'` and `keepTokens` do not combine, and the build says so: one asserts every path resolves, the other declares
where the ones that do not will land. A project that cannot make the first assertion wants `'warn'`.

With nothing to stand in for, this is additive only — naming a token nothing in the stylesheet references and no
javascript here reads, such as one consumed by a sibling package or by css outside `include`. It is inert under
`tokens: false`, which keeps everything already.

This replaces [`staticCss`](#staticcss) as the way to keep a token category alive. `staticCss` emits utility _classes_,
so keeping the colours meant shipping a rule per colour purely to hold the declarations up — usually a larger stylesheet
than the pruning saved.

These resolve, and cost nothing:

```ts
token('colors.red.300') // a string literal
token.value('spacing.4') // either half
t('colors.red.300') // `import { token as t }`
ds.token('colors.red.300') // `import * as ds`
```

Two things keep `strict` inert rather than wrong, and both are reported. A file whose parsed tree carries syntax errors
declines — a `.js` file using TypeScript-only syntax, or one a `parser:before` hook rewrote. And a `.vue` or `.svelte`
file mentioning `token` anywhere declines, because a single-file component is stored post-transform and the tree is not
the code that ships.

A path built from a template literal is _bounded_ rather than reported: Bamboo cannot tell which token
``token(`colors.${shade}`)`` wants, but it knows the answer starts with `colors.`, so it keeps that category and prunes
the rest. An empty head — ``token(`${path}`)`` — bounds nothing and is reported.

These are reported and keep the whole layer:

```ts
token(`${name}`) // built at runtime, with nothing to bound it
token(KEY) // a constant — resolvable for extraction, not here
const t = token // the binding escapes
export { token } from 'styled-system/tokens' // re-exported
const { token } = require('styled-system/tokens') // not statically bound
import { token } from '@acme/ui' // a barrel Bamboo cannot classify
```

<Callout type="warning">
  `strict` is an assertion, and one thing it cannot check for you: a caller **outside** `include`. That scopes style
  extraction, not everything that may import — a build script, a config file, or a sibling workspace package calling
  `token()` is invisible, and pruning against it produces a `var()` with no declaration, which inherits rather than
  falling back. Check that `include` covers every file that reaches for a token before turning this on. `bamboo init`
  scaffolds `./src/**` and `./pages/**`; a Next App Router `./app/**`, a Remix `./app/root.tsx`, or plain `.ts` files
  under a `.tsx`-only glob are the common gaps.
</Callout>

#### prune.keyframes

Whether to drop `@keyframes` rules nothing in the generated css can reach.

A preset declares every animation it offers and an app uses a handful; the rest sit in the same render-blocking
stylesheet. Like [`prune.tokens`](#prune), the saving scales with the size of your design system rather than the size of
your app. The two are independent – either can be switched off without the other.

```json
{
  "prune": { "keyframes": false }
}
```

Only keyframes your theme declares are ever removed, so one emitted by [`global.css`](#globalcss) is left alone. A name
is kept when an animation property in the generated css names it, when a theme names it, when it is named by a custom
property that is itself reachable, or when it appears anywhere under [`include`](#include).

That last clause is a textual scan, and it is deliberately over-inclusive: it covers an animation name assembled at
runtime or handed to an inline `style` rather than to Bamboo, neither of which the css can show. A name matching a word
in unrelated prose keeps its keyframe alive, which is the cheap failure.

"A custom property that is itself reachable" means reachable to [`prune.tokens`](#prune), not merely referenced
somewhere in the stylesheet. A token can be reached from outside the css entirely — a `token()` call, a
[`keepTokens`](#prunekeeptokens) pattern, a theme injected at runtime, a `global.css` export — and those are exactly the
declarations `prune.tokens` keeps. So a keyframe is dropped only when the declarations naming it were dropped too, and
under `prune: { tokens: false }`, where nothing is removable, every keyframe a declaration names is kept.

This is why you do not have to keep your own `outdir` inside `include` to protect your animations. The textual scan
reads whatever `include` covers, and the generated token artifact spells each animation value out in full — so a project
whose `include` reached its own output was keeping its keyframes by accident, and excluding `outdir` took the accident
away.

Names are recovered by testing each token in a value against the set your theme declares, rather than by parsing the
`animation` shorthand – whose parts come in any order. A keyframe named after a keyword such as `none` or `ease`
therefore always looks referenced. The bias is intentional throughout: keeping an unused keyframe costs bytes, dropping
a used one silently stops an animation.

### cssVarRoot

**Type**: `string`

**Default**: `:where(:root, :host)`

The root selector for the css variables.

### conditions

**Type**: `Extendable<Conditions>`

**Default**: `{}`

The css selectors or media queries shortcuts.

```json
{
  "conditions": { "hover": "&:hover" }
}
```

### global.css

**Type**: `Extendable<GlobalStyleObject>`

**Default**: `{}`

The global styles for your project.

```json
{
  "global": {
    "css": {
      "html, body": {
        "margin": 0,
        "padding": 0
      }
    }
  }
}
```

At-rules work here too, so `@font-face` written directly in `global.css` does emit. Prefer
[`global.fontface`](#globalfontface) for fonts anyway: it emits the same rule **and** registers the family name as a
value `fontFamily` accepts. Declared through `global.css`, the name is a string Bamboo has never heard of — no
autocomplete, and under [`strictValues`](#strictvalues) a build error.

```ts
// fontFamily accepts Tokens["fonts"] | "Roboto"
global: {
  fontface:   { Roboto: { src: "url(/fonts/roboto.woff2) format('woff2')" } },
}

// fontFamily accepts Tokens["fonts"] only — the @font-face still ships
global: {
  css:   { '@font-face': { fontFamily: 'Roboto', src: "url(/fonts/roboto.woff2) format('woff2')" } },
}
```

Same for [`global.positionTry`](#globalpositiontry), which registers its names as values `positionTryFallbacks` and
`positionTry` accept, and adds the leading `--` those require. Written here you have to remember the `--`, and
`@position-try flip` is invalid css that ships silently and never fires.

### global.vars

**Type**: `Extendable<GlobalVarsDefinition>`

**Default**: `{}`

Additional global css variables, emitted at [`cssVarRoot`](#cssvarroot). A string value declares the variable directly;
an object value registers it with `@property`. See
[Global styles](/docs/concepts/global-styles#custom-global-variables-globalvars) for a worked example.

### theme

**Type**: `Extendable<Theme>`

**Default**: `{}`

The theme configuration for your project.

```json
{
  "theme": {
    "tokens": {
      "colors": {
        "red": { "value": "#EE0F0F" },
        "green": { "value": "#0FEE0F" }
      }
    },
    "semanticTokens": {
      "colors": {
        "danger": { "value": "token(colors.red)" },
        "success": { "value": "token(colors.green)" }
      }
    }
  }
}
```

### theme.variants

**Type**: `Extendable<ThemeVariantsMap>`

**Default**: `{}`

The theme variants configuration for your project.

```json
{
  "theme": {
    "variants": {
      "primary": {
        "tokens": {
          "colors": {
            "text": { "value": "red" }
          }
        },
        "semanticTokens": {
          "colors": {
            "muted": { "value": "token(colors.red.200)" },
            "body": {
              "value": {
                "base": "token(colors.red.600)",
                "_osDark": "token(colors.red.400)"
              }
            }
          }
        }
      }
    }
  }
}
```

### utilities

**Type**: `Extendable<UtilityConfig>`

**Default**: `{}`

The css utility definitions.

```js
{
  "utilities": {
    extend: {
      borderX: {
        values: ['1px', '2px', '4px'],
        shorthand: 'bx', // `bx` or `borderX` can be used
        transform(value, { token }) {
          return {
            borderInlineWidth: value,
            borderColor: token('colors.red.200'), // read the css variable for red.200
          }
        },
      },
    },
  }
}
```

### patterns

**Type**: `Extendable<Record<string, AnyPatternConfig>>`

**Default**: `{}`

Common styling or layout patterns for your project.

```js
{
  "patterns": {
    extend: {
      // Extend the default `flex` pattern
      flex: {
        properties: {
          // only allow row and column
          direction: { type: 'enum', value: ['row', 'column'] },
        },
      },
    },
  },
}
```

### staticCss

**Type**: `StaticCssOptions`

**Default**: `{}`

Used to generate css utility classes for your project.

```js
{
  "staticCss": {
    css: [
      {
        properties: {
          margin: ['*'],
          padding: ['*', '50px', '80px'],
        },
        responsive: true,
      },
      {
        properties: {
          color: ['*'],
          backgroundColor: ['green.200', 'red.400'],
        },
        conditions: ['light', 'dark'],
      },
    ],
  },
}
```

<Callout type="info">
  This is for classes your source never writes — a class name assembled at runtime, or one applied from outside the
  files [`include`](#include) covers.

It is **not** the way to stop a token being pruned, which is what it used to be reached for. `staticCss` emits a utility
_rule_ per value, so keeping a colour palette alive meant shipping a rule per colour purely to hold its declaration up —
usually a larger stylesheet than the pruning saved. Name the tokens with [`prune.keepTokens`](#prunekeeptokens) instead,
which keeps the declarations without emitting anything that uses them.

</Callout>

### strictValues

**Type**: `boolean`

**Default**: `false`

Require every style value to be a token, so a raw CSS value has to be written `[14px]`.

```ts
css({ color: 'red.300' }) // ✅ a token
css({ display: 'flex' }) // ✅ a keyword — `flex` is the only way to say it
css({ animationName: 'fadeIn' }) // ✅ an identifier you invented
css({ transitionProperty: 'color' }) // ✅ a css property name, where the grammar asks for one

css({ fontSize: '14px' }) // ❌ write `[14px]`
css({ color: '#fff' }) // ❌
css({ border: '1px solid red' }) // ❌
```

This is a design-system policy — "everything goes through the theme" — and the brackets are what make reaching outside
it visible in the source, where `fontSize: '14px'` reads exactly like using the scale. On one otherwise-correct
five-page app it reports 468 values, which is what makes it a day-one decision.

It is reported by the build and graded by [`validation`](#validation), against the styles your **source** produced — a
preset's reset and your own config recipes are not held to a policy about your source. It was a set of TypeScript
narrowings until 1.x; the grammar is what lets it tell a keyword from a raw value, which the types could not, and it is
why `transitionProperty: 'color'` is no longer rejected in favour of `'colors'`.

It is **not** what catches a misspelled token — see [`unresolvedToken`](#unresolvedtoken), which is on by default.

### strictPropertyValues

**Type**: `boolean`

**Default**: `false`

Only use valid CSS values for properties that do have a predefined list of values. Will throw for properties that do not
have config tokens, such as `display`, `content`, `willChange`, etc.
[Learn more.](/docs/concepts/writing-styles#type-safety)

### global.fontface

**Type**: `GlobalFontfaceDefinition`

**Default**: `{}`

Global font face definitions.

```json
{
  "global": {
    "fontface": {
      "Inter": {
        "src": "url(/fonts/inter.woff2) format('woff2')",
        "fontWeight": "400",
        "fontStyle": "normal"
      },
      "Roboto": {
        "src": "url(/fonts/roboto.woff2) format('woff2')",
        "fontWeight": "400",
        "fontStyle": "normal"
      }
    }
  }
}
```

Each key becomes a value `fontFamily` accepts, alongside your `fonts` tokens — so `css({ fontFamily: 'Inter' })`
autocompletes and typechecks. That is the reason to declare a face here rather than as a raw `@font-face` in
[`global.css`](#globalcss), which emits the same rule but leaves the family name unregistered.

Check out the [Custom Fonts](/docs/guides/fonts#global-font-face) guide for more information on how to use the
`global.fontface` option.

### global.positionTry

**Type**: `Extendable<GlobalPositionTry>`

**Default**: `{}`

Named [`@position-try`](https://developer.mozilla.org/en-US/docs/Web/CSS/@position-try) fallbacks for anchor
positioning. Each key emits one `@position-try` rule, and a key without a leading `--` gets one added, so
`bottom-scrollable` below becomes `--bottom-scrollable`.

Each name is also registered as a value `positionTryFallbacks` and `positionTry` accept, so
`css({ positionTryFallbacks: '--bottom-scrollable' })` autocompletes and — under [`strictValues`](#strictvalues) —
typechecks. That is the reason to declare one here rather than as a raw `@position-try` in [`global.css`](#globalcss),
which emits the same rule but leaves the name unknown.

```json
{
  "global": {
    "positionTry": {
      "bottom-scrollable": {
        "positionArea": "block-start span-inline-end",
        "alignSelf": "stretch"
      }
    }
  }
}
```

## Diagnostics options

### logLevel

**Type**: `'debug' | 'info' | 'warn' | 'error' | 'silent'`

**Default**: `info`

The log level for the built-in logger.

### validation

**Type**: `'off' | 'warn' | 'error'`

**Default**: `warn`

What to do when the config does not validate.

- When set to 'off', no validation will be performed.
- When set to 'warn', warnings will be logged when validation fails.
- When set to 'error', errors will be thrown when validation fails.

```json
{
  "validation": "error"
}
```

This grades opinions about a config that still builds. Two checks are not that, and are not governed by this option —
they throw at every setting, including `off`:

- A token value written in a **retired reference syntax**, which is output that is already broken.
- An option that has been **removed or renamed**, which is proof the config predates the version reading it. The error
  names each key and what to write instead.

A removed option throws rather than warns because nothing else notices it. There is no schema walk, so a key that no
longer exists is otherwise ignored in silence: the build reverts to the default, and any assertion the option asked for
stops being enforced. Removals ship in minor versions, so a warning is what an automated dependency upgrade merges
without a person reading it. An _unknown_ key is a different case and is still tolerated — it may be a setting for a
version you have not installed yet. A removed key can only point backwards.

### unresolvedToken

**Type**: `'off' | 'warn' | 'error' | { token?: …; grammar?: … }`

**Default**: `{ token: 'error', grammar: 'warn' }`

What to do about a style value that names neither a token nor anything the CSS property accepts.

- `off` says nothing.
- `warn` logs each one as it is transformed.
- `error` fails the build, listing every one it found.

**The two halves are graded apart, because they are not equally certain.**

| half      | when                                                                    | decided by               | default |
| --------- | ----------------------------------------------------------------------- | ------------------------ | ------- |
| `token`   | the property draws from a token category, or the value is a dotted path | bamboo's own bookkeeping | `error` |
| `grammar` | a bare name on a property with no token category                        | the CSS grammar          | `warn`  |

`color: 'mutedd'` and `top: 'navH'` are the first: `color` reads `colors` and `top` reads `spacing`, so a name that is
not one of those is bamboo's own answer and there is nothing else to be wrong about it.

`display: 'flexx'` is the second, and so is `containerType: 'scroll-state'` — which is valid CSS the grammar's data has
not caught up with. Sweeping every keyword csstype enumerates through the grammar found 8 such disagreements in 10,128
pairs, 0.08%, and every one of them was on a property with no token category. Keeping that half at `warn` is what stops
a build failing over how fresh a grammar is.

Pass a single severity to apply it to both:

```json
{
  "unresolvedToken": "error"
}
```

Token resolution falls back to the value it was given, so an unknown path is emitted as written —
`css({ background: 'accent.default' })` ships as `background: accent.default`. That parses, so the stylesheet is valid
and no build step objects; the browser drops the declaration at compute time and the style is simply absent. It surfaces
as "this colour never applied", a long way from the typo that caused it.

The default is `warn` because the test is a **shape**: a dotted value, against the set of values the property
enumerates. That is right about a mistyped token and cannot be certain about a literal, so escalating it is a choice a
project makes once it knows its own source is clean. A property that enumerates nothing — `gridTemplateAreas` — is never
reported, and `[accent.default]` marks a value as literal.

Every style the build transforms is graded, wherever it was written — a `css()` call, `staticCss`, and the styles your
config supplies: `globalCss`, the preflight scope, config recipes and mixins. The three settings differ in what they do
about a finding, never in which styles they look at.

The modifiers a path can carry are stripped before it is resolved, so `accent.default!`, `accent.default !important` and
`accent.default/50` are all judged as `accent.default` and reported once between them. The opacity modifier is only
stripped for a property drawing on `colors`, since that is the only place it means anything — a slash stays part of the
value in `font: 12px/1.5 serif`.

<Callout type="info">
  Not to be confused with [`prune.unresolvedPath`](#prune), which is about a `token()` **call** whose path the prune
  scan cannot follow statically — a question about pruning coverage, asked of a token that usually exists. This one is
  about a token that does not.
</Callout>

Under `error` the check is asked of the stylesheet being emitted rather than of the calls that built it, so it reports
exactly what the file about to be written contains. Extraction is additive within a watch process — the rule for a style
deleted from source stays in the sheet until the process restarts — so a dev server reports a value you have already
fixed for the same reason it still serves its rule. A production build is a fresh process and sees only what its source
asked for.

A **binding that does not exist** is not graded here and always fails the build; see below.

### Calls to a binding that does not exist

Not an option — there is no setting under which it is what someone meant.

`styled-system/patterns` and `styled-system/recipes` are generated from your config, so what they export moves when it
does: a pattern dropped from a preset, a recipe renamed. The import survives that as a binding to nothing. Nothing
extracts the call, so every rule it would have contributed is absent and the class the component asks for has nothing
behind it — while the build prints its ticks and exits 0.

```
error: 1 call(s) name a binding that does not exist:

src/modal.tsx
  `stack` is not a pattern — `../styled-system/patterns` does not export it.
```

Reported per **call**, not per import: both entrypoints export types beside their functions — `FlexProperties`,
`ButtonVariantProps` — and importing one of those is ordinary. A binding nobody calls is left alone too, since nothing
asked it for a class.

This is the same test [`prune.unresolvedPath`](#prune) and extraction failures already apply, against the third way of
arriving at the same output: a stylesheet missing rules that a green build did not mention.

## Other options

### Plugins

**Type**: `BambooPlugin[]`

Plugins are simple objects that contain a `name` and a `hooks` object. They are the only way to register a hook — Bamboo
provides a set of callbacks you can hook into for more advanced use cases, and the [Hooks](/docs/concepts/hooks) docs
list them all.

They are called in sequence in the order they are defined in the `plugins` array.

A bare `hooks` key on the config used to be a second way to do this, treated as a nameless plugin appended last. It has
been removed, and a config still setting it fails naming the replacement.

```ts
import { defineConfig } from '@bamboocss/dev'

export default defineConfig({
  // ...
  plugins: [
    {
      name: 'token-format',
      hooks: {
        'tokens:created': ({ configure }) => {
          configure({
            formatTokenName: (path) => '$' + path.join('-'),
          })
        },
      },
    },
  ],
})
```
