---
title: Writing Styles
description: Bamboo generates the utilities you need to style your components with confidence.
---

Styles in Bamboo are written as objects.

## Atomic Styles

When you write styles in Bamboo, it generates an atomic stylesheet scoped to the `@layer utilities` cascade layer.

Bamboo exposes a `css` function that can be used to author styles. It accepts a style object and returns a className
string.

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

const styles = css({
  backgroundColor: 'gainsboro',
  borderRadius: '9999px',
  fontSize: '13px',
  padding: '10px 15px'
})

// Generated className:
// --> p_10px_15px bdr_9999px bg-c_gainsboro fs_13px

<div className={styles}>
  <p>Hello World</p>
</div>
```

The styles generated at build time end up like this:

```css
@layer utilities {
  .p_10px_15px {
    padding: 10px 15px;
  }

  .bdr_9999px {
    border-radius: 9999px;
  }

  .bg-c_gainsboro {
    background-color: gainsboro;
  }

  .fs_13px {
    font-size: 13px;
  }
}
```

### Shorthand Properties

Bamboo provides shorthands for common css properties to help improve the speed of development and reduce the visual
density of your style declarations.

Properties like `borderRadius`, `backgroundColor`, and `padding` can be swapped to their shorthand equivalent `rounded`,
`bg`, and `p`.

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

// BEFORE - Good
const styles = css({
  backgroundColor: 'gainsboro',
  borderRadius: '9999px',
  fontSize: '13px',
  padding: '10px 15px',
})

// AFTER - Better
const styles = css({
  bg: 'gainsboro',
  rounded: '9999px',
  fontSize: '13px',
  p: '10px 15px',
})
```

> Shorthands are documented alongside their respective properties in the [utilities](/docs/utilities/background)
> section.

### Type safety

Bamboo is built with TypeScript and provides type safety for all style properties and shorthands. Most of the style
properties are connected to either the native CSS properties or their respective token value defined as defined in the
`theme` object.

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

//                       ⤵ you'll get autocomplete for colors
const styles = css({ bg: '|' })
```

> You can also enable the `strictValues` setting in the Bamboo configuration. `true` allows only token values and
> prevents the use of custom or raw CSS values. A _misspelled_ token needs no setting — the build reports it either way;
> see [below](#a-misspelled-token-is-the-builds-job-not-typescripts).

- `config.strictValues` asks for `[14px]` around a raw css value. A keyword is not a raw value, so `display: 'flex'` is
  unaffected.
- `config.strictPropertyValues` will throw for properties that do not have config tokens, such as `display`, `content`,
  `willChange`, etc. when the value is not a predefined CSS value.

> In both cases, you can use the `[xxx]` escape-hatch syntax to use custom or raw CSS values without TypeScript errors.

#### strictValues

With `config.strictValues` enabled, you can only use token values in your styles. This prevents the use of custom or raw
CSS values.

```ts filename="bamboo.config.ts"
import { css } from '../styled-system/css'

css({ bg: 'red' }) // ❌ Error: "red" is not a valid token value
css({ fontSize: '123px' }) // ❌ Error: "123px" is not a valid token value

css({ bg: 'red.400' }) // ✅ Valid
css({ fontSize: '[123px]' }) // ✅ Valid, since `[123px]` is using the escape-hatch syntax
css({ content: 'abc' }) // ✅ Valid, since `content` isn't bound to a config token
```

For one-off styles, you can always use the escape-hatch syntax `[xxx]` to use custom or raw CSS values without
TypeScript errors.

```ts filename="bamboo.config.ts"
import { css } from '../styled-system/css'

css({ bg: '[red]' }) // ✅ Valid, since `[red]` is using the escape-hatch syntax
css({ fontSize: '[123px]' }) // ✅ Valid, since `[123px]` is using the escape-hatch syntax
```

A token can carry an important mark or a colour opacity modifier, and the path in front of one is checked as usual:

```ts
css({ bg: 'red.400!' }) // ✅ Valid
css({ bg: 'red.400/50' }) // ✅ Valid
css({ bg: 'rd.400!' }) // ❌ Error: `rd.400` is not a token, mark or no mark
```

What follows the mark is **not** checked — `bg: 'red.400!nonsense'` typechecks. Spelling that out as a closed set of
templates multiplies every token in the union by five, and it was around half the cost of typechecking a `css()` call
under `strictValues`. [`unresolvedToken`](/docs/references/config#unresolvedtoken) resolves the path underneath the
mark, so the build still reports the value — warning by default, failing the build under `'error'`.

#### A misspelled token is the build's job, not TypeScript's

`color: 'mutedd'` names no token. It used to be accepted by TypeScript and by the build, ship as `color: mutedd`, and be
dropped by the browser at compute time — surfacing as a colour that never applied, a long way from the typo.

The build reports it now, with no setting to turn on and no migration to do:

```
`color: mutedd` — no such `colors` token. It is emitted as written, and the browser will drop it.
Write `[mutedd]` to mean it literally.
```

It reads the CSS grammar rather than a TypeScript union, which is what lets it be precise about the interesting case — a
name that exists, on the wrong shelf:

```
`top: navH` — `navH` is declared under `sizes`, but `top` reads `spacing`.
It is emitted as written, and the browser will drop it.
Use a `spacing` token, or write `[navH]` to mean it literally.
```

No type error can say that. TypeScript can only report that a string is not assignable to a union of two hundred
members, and guess a near-miss by spelling — which is how `transitionProperty: 'color'`, ordinary CSS, came to be
rejected in favour of `'colors'`, a utility value that emits seven declarations instead of one.

It also sees what the types cannot: values in `globalCss`, in config recipes, in a `.vue` or `.svelte` template, and in
any project not using TypeScript at all. Two of the four findings on this documentation site were in config recipes,
which `tsc` never checked.

Grade it with [`unresolvedToken`](/docs/references/config#unresolvedtoken) — `'warn'` by default, `'error'` to fail the
build, `'off'` to say nothing.

#### strictPropertyValues

With `config.strictPropertyValues` enabled, you can only use valid CSS values for properties that do have a predefined
list of values in your styles. This prevents the use of custom or raw CSS values.

```ts filename="bamboo.config.ts"
css({ display: 'flex' }) // ✅ Valid
css({ display: 'block' }) // ✅ Valid

css({ display: 'abc' }) // ❌ will throw since 'abc' is not part of predefined values of 'display'
css({ pos: 'absolute123' }) // ❌ will throw since 'absolute123' is not part of predefined values of 'position'
css({ display: '[var(--btn-display)]' }) // ✅ Valid, since `[var(--btn-display)]` is using the escape-hatch syntax

css({ content: '""' }) // ✅ Valid, since `content` does not have a predefined list of values
css({ flex: '0 1' }) // ✅ Valid, since `flex` does not have a predefined list of values
```

`config.strictPropertyValues` only applies to properties that have a fixed set of values — layout, box-model and
text-flow properties such as `display`, `position`, `overflow`, `flexDirection`, `alignItems`, `visibility` and the
`*Style` border properties. Border shorthands such as `border` and `borderTop` are not in the set. The generated
`styled-system/types/style-props.d.ts` is the authoritative list: a restricted property is typed there as a union of its
allowed values.

## Nested Styles

Bamboo provides different ways of nesting style declarations. You can use the native css nesting syntax, or the built-in
pseudo props like `_hover` and `_focus`. Pseudo props are covered more in-depth in the next section.

### Native CSS Nesting

Bamboo supports the native css nesting syntax. You can use the `&` selector to create nested styles.

> **Important:** It is required to use the "&" character when nesting styles.

```jsx
<div
  className={css({
    bg: 'red.400',
    '&:hover': {
      bg: 'orange.400',
    },
  })}
/>
```

You can also target children and siblings using the `&` syntax.

```jsx
<div
  className={css({
    bg: 'red.400',
    '& span': {
      color: 'pink.400',
    },
  })}
/>
```

We recommend not using descendant selectors, and the reason is worth knowing before you reach for one: `& span` is
`(0,1,1)` where a class is `(0,1,0)`, so it outranks any `css()` applied to that span directly. Cascade layers do not
separate the two — both are in `utilities`, where specificity decides — so the span carries the class it was given and
renders with the other value, with nothing to report. See
[inside a layer, specificity still decides](/docs/concepts/cascade-layers#inside-a-layer-specificity-still-decides).

Colocating styles directly on the element is the preferred way of writing styles in Bamboo.

### Using Pseudo Props

Bamboo provides a set of pseudo props that can be used to create nested styles. The pseudo props are prefixed with an
underscore `_` to avoid conflicts with the native pseudo selectors.

For example, to create a hover style, you can use the `_hover` pseudo prop.

```jsx
<div
  className={css({
    bg: 'red.400',
    _hover: {
      bg: 'orange.400',
    },
  })}
/>
```

> See the [pseudo props](/docs/concepts/conditional-styles#reference) section for a list of all available pseudo props.

## Global styles

Global styles are useful for applying additional global resets or font faces. Use the `global.css` property in the
`bamboo.config.ts` file to define global styles.

Global styles are inserted at the top of the stylesheet and are scoped to the `@layer base` cascade layer.

> For resets, global variables, theming patterns, and more examples, see [Global styles](/docs/concepts/global-styles).

```js filename="bamboo.config.ts"
import { defineConfig, defineGlobalStyles } from '@bamboocss/dev'

const globalCss = defineGlobalStyles({
  'html, body': {
    color: 'gray.900',
    lineHeight: '1.5',
  },
})

export default defineConfig({
  // ...
  global: { css: globalCss },
})
```

The styles generated at build time will look like this:

```css
@layer base {
  html,
  body {
    color: var(--colors-gray-900);
    line-height: 1.5;
  }
}
```

## Style Composition

### Merging styles

Passing multiple styles to the `css` function will deeply merge the styles, allowing you to override styles in a
predictable way.

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

const result = css({ mx: '3', paddingTop: '4' }, { mx: '10', pt: '6' })
//    ^? result = "mx_10 pt_6"
```

To design a component that supports style overrides, you can provide the `css` prop as a style object, and it'll be
merged correctly.

```tsx filename="src/components/Button.tsx"
import { css } from '../styled-system/css'

export const Button = ({ css: cssProp = {}, children }) => {
  const className = css({ display: 'flex', alignItems: 'center', color: 'black' }, cssProp)
  return <button className={className}>{children}</button>
}
```

Then you can use the `Button` component like this:

```tsx filename="src/app/page.tsx"
import { Button } from './Button'

export default function Page() {
  return (
    <Button css={{ color: 'pink', _hover: { color: 'red' } }}>
      will result in `class="d_flex ai_center c_pink hover:c_red"`
    </Button>
  )
}
```

---

You can use this approach as well with the `{cvaFn}.raw`, `{svaFn}.raw` and `{patternFn}.raw` functions, allowing style
objects to be merged as expected in any situation.

**Pattern Example:**

```tsx filename="src/components/Button.tsx"
import { flex } from '../styled-system/patterns'
import { css } from '../styled-system/css'

export const Button = ({ css: cssProp = {}, children }) => {
  // using the flex pattern
  const flexProps = flex.raw({
    align: 'center',
    border: '1px solid',
    _hover: { color: 'blue.400' },
  })

  // merging the styles
  const className = css(flexProps, cssProp)

  return <button className={className}>{children}</button>
}
```

**CVA Example:**

```tsx filename="src/components/Button.tsx"
import { css, cva } from '../styled-system/css'

const buttonRecipe = cva({
  base: { display: 'flex', fontSize: 'lg' },
  variants: {
    variant: {
      primary: { color: 'white', backgroundColor: 'blue.500' },
    },
  },
})

export const Button = ({ css: cssProp = {}, children }) => {
  const className = css(
    // using the button recipe
    buttonRecipe.raw({ variant: 'primary' }),

    // adding style overrides (internal)
    { _hover: { color: 'blue.400' } },

    // adding style overrides (external)
    cssProp,
  )

  return <button className={className}>{children}</button>
}
```

**SVA Example:**

An `sva` recipe's `.raw()` returns one style object per slot, so each slot merges the same way:

```tsx
const slotStyles = checkbox.raw({ size: 'md' })

// rootProps is a `css.raw({ ... })` object supplied by the caller
;<label className={css(slotStyles.root, rootProps)} />
```

See [Slot recipes](/docs/concepts/slot-recipes) for the recipe this comes from.

### Classname concatenation

Bamboo provides a `cx` function for combining class names. Vite gives it two deliberately different contracts:

- when every Bamboo argument is analyzable, the compiler merges their StyleSets before allocating classes;
- when an arbitrary runtime class is present, the generated helper is a tiny string join with no conflict guarantee.

```jsx
import { css, cx } from '../styled-system/css'

const styles = css({
  borderWidth: '1px',
  borderRadius: '8px',
  paddingX: '12px',
  paddingY: '24px',
})

const Card = ({ className, ...props }) => {
  const rootClassName = cx('group', styles, className)
  return <div className={rootClassName} {...props} />
}
```

With fully analyzable Bamboo arguments, later declarations win and the losing atom is never allocated:

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

The compiler cannot infer the declarations behind `className` in the component example. Take a style object when a
component needs a reliable override: `css(base, props.css)` merges per property before any class name exists.

```jsx
const Card = ({ css: cssProp, ...rest }) => <div className={css({ p: '4' }, cssProp)} {...rest} />
```

Use `css(a, b)` directly when both style objects are already available. Use a recipe variant when the choices form a
finite public API.

<Callout type="warning">
  A component that styles itself with bare `css()` and accepts a `className` is the case to watch. Both end up in
  `utilities`, and an opaque runtime class cannot be composed by the compiler. Prefer a style-object prop or a declared
  variant for an intentional override.
</Callout>

### Hashing

The `hash` option shortens class names and CSS variables, and it is the only thing that does. It applies to every build
path — Vite, the CLI and PostCSS alike — so a project wanting short names in its compiled output sets it here rather
than per integration.

```ts filename="bamboo.config.ts"
import { defineConfig } from '@bamboocss/dev'

export default defineConfig({
  // ...
  hash: true,
})
```

> You might need to generate a new code artifact by running `bamboo codegen --clean`

When you write a style like this:

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

const styles = css({
  display: 'flex',
  flexDirection: 'row',
  _hover: {
    bg: 'red.50',
  },
})
```

The hash generated css will look like:

```css
.fPSBzf {
  display: flex;
}

.ksWBqx {
  flex-direction: row;
}

.btpEVp:is(:hover, [data-hover]) {
  background: var(--bINrJX);
}
```

> We recommend that you use this in production builds only, as it can make debugging a bit harder.

## Important styles

Applying important styles works just like CSS

```js
css({
  color: 'red !important',
})
```

You can also apply important using just the exclamation syntax `!`

```js
css({
  color: 'red!',
})
```

## Fallback values

Some CSS values have no single spelling every browser understands. CSS handles this by letting you declare the same
property more than once – a browser keeps the last declaration it can parse and discards the rest. A style object can't
express that, since it can't hold the same key twice, so Bamboo gives you `fallback(...)`.

Write the candidates **most-preferred first**, either as a string or through the `fallback` helper:

```js
import { css, fallback } from '../styled-system/css'

css({ height: fallback('calc(100dvh - 100px)', 'calc(100vh - 100px)') })
```

Both compile to the same value. The helper exists so the feature has an import to find and a signature to hover; the
string form is the one to reach for when a candidate is itself built by a call, since `token()` and friends cannot be
resolved from inside another function call:

```js
css({ height: `fallback(${token('sizes.4')}, 100vh)` }) // ✅
css({ height: fallback(token('sizes.4'), '100vh') }) // ❌ not extractable
```

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

css({
  height: 'fallback(calc(100dvh - 100px), calc(100vh - 100px))',
})
```

```css
@layer utilities {
  .h_fallback\(calc\(100dvh_-_100px\)\,_calc\(100vh_-_100px\)\) {
    height: calc(100vh - 100px);
    height: calc(100dvh - 100px);
  }
}
```

The declarations come out in reverse, so a browser that doesn't understand `dvh` stops at the `vh` line and every other
browser reaches the one you actually wanted.

You can list as many candidates as you like, and each one resolves the same way a normal value would – tokens, the
`[...]` escape hatch and shorthand properties all work inside a fallback:

```js
css({
  cursor: 'fallback(-webkit-grab, grab, move)',
  color: 'fallback(red.300, red)',
  paddingX: 'fallback(4, [2px])',
})
```

`fallback(...)` works anywhere a value does – conditions, breakpoints, `global.css`, recipes and patterns. Adding
`!important` marks every candidate, not just the winning one.

### What it can't do

The cascade only arbitrates between declarations of the **same property**: the browser keeps the last `height` it can
parse. So every candidate has to resolve to exactly one declaration. Most utilities do.

Some don't. `transitionProperty` emits a `--transition-prop` variable beside the property, `lineClamp` emits four
declarations for a number and one for `none`, and `divideX` emits a nested rule. The extra declarations aren't part of
the contest – they'd apply unconditionally, whichever candidate the browser actually took, so the variable and the
property could end up disagreeing. Bamboo will not guess: it warns and applies your preferred candidate on its own.

```
⚠️ `lineClamp: fallback(none, 3)` does not resolve to a single declaration per candidate,
   so no fallback was emitted. Only `none` was applied.
```

The same is true of tokens that expand, like the color-opacity modifier: `color: 'fallback(red.300/50, red)'` is
refused, because `red.300/50` emits a `--mix-color` variable alongside `color`. Write the two declarations yourself if
you need that.

### When you don't need it

If you use [LightningCSS](/docs/references/config#lightningcss), **don't hand-write vendor-prefix or color-space
fallbacks.** It already generates those from your browser targets, and it does a more thorough job:

```js
css({ width: 'stretch', color: 'oklch(70% 0.1 200)' })
```

```css
/* LightningCSS, targeting older browsers */
.w_stretch {
  width: -webkit-fill-available;
  width: -moz-available;
  width: stretch;
}
.c_oklch\(70\%_0\.1_200\) {
  color: #40b1b7;
  color: color(display-p3 0.381906 0.685023 0.710512);
  color: lab(66.1711% -31.3595 -12.905);
}
```

Reach for `fallback(...)` when the fallback is **a different design decision, not a polyfill** – something no optimizer
can infer for you:

| Case                                 | Why a tool can't do it                                            |
| ------------------------------------ | ----------------------------------------------------------------- |
| `fallback(100dvh, 100vh)`            | `100vh` is not equivalent to `100dvh`; you chose to accept it     |
| `fallback(-webkit-grab, grab, move)` | `move` is a different cursor, not a prefixed spelling of `grab`   |
| `fallback(stretch, 100%)`            | `100%` behaves differently; it's your call that it's close enough |

LightningCSS also **prunes** candidates it can prove are unreachable for your targets – a `100vh` fallback disappears
from a build that only targets browsers with `dvh`. It prunes on its own understanding of the values, so it can drop a
candidate at every target level when it considers it redundant. The default PostCSS pipeline keeps every candidate
exactly as written.

<Callout type="info">
  Only a value that is *entirely* one `fallback(...)` call is treated as a fallback list. `1px solid fallback(red,
  blue)` is left alone, because a candidate list has no meaning as part of a larger value.
</Callout>

### When you get it wrong

Anything Bamboo can recognise as a broken `fallback(...)` is reported at build time and the declaration is dropped,
rather than written out as text that isn't CSS:

- **`fallback(100dvh, 100vh`** – an unbalanced `(` or `[` can't be parsed as a candidate list.
- **`fallback(fallback(a, b), c)`** – nesting has no meaning; group the candidates in one call instead.
- **Candidates that don't resolve to a single declaration** – as above.

Two mistakes it **cannot** catch, because the value is an ordinary string as far as Bamboo and TypeScript are concerned.
Both reach the stylesheet verbatim and are ignored by the browser:

- **A misspelled name** – `fallbacks(...)`, or `fallback(...)` used as part of a larger value like
  `calc(fallback(a, b))` or `1px solid fallback(red, blue)`. Under
  [`strictValues`](/docs/references/config#strictvalues) a misspelling is a type error; the embedded forms are not.
- **The candidates themselves aren't type-checked.** `fallback(red.300, rd)` typechecks even under `strictValues`,
  because the whole thing is one string to TypeScript. The `[...]` escape hatch has the same property — the
  `no-escape-hatch` lint rule does still catch it, since it parses each candidate individually.

## TypeScript

Use the `SystemStyleObject` type if you want to type your styles.

```ts {2}
import { css } from '../styled-system/css'
import type { SystemStyleObject } from '../styled-system/types'

const styles: SystemStyleObject = {
  color: 'red',
}
```

## Property conflicts

When you combine shorthand and longhand properties, Bamboo will resolve the styles in a predictable way. The shorthand
property will take precedence over the longhand property.

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

const styles = css({
  paddingTop: '20px',
  padding: '10px',
})
```

The styles generated at build time will look like this:

```css
@layer utilities {
  .p_10px {
    padding: 10px;
  }

  .pt_20px {
    padding-top: 20px;
  }
}
```

## Global vars

You can use the `global.vars` property to define global
[CSS variables](https://developer.mozilla.org/en-US/docs/Web/CSS/--*) or custom CSS
[`@property`](https://developer.mozilla.org/en-US/docs/Web/CSS/@property) definitions.

Bamboo will automatically generate the corresponding CSS variables and suggest them in your style objects.

> They will be generated in the [`cssVarRoot`](/docs/references/config#cssvarroot) near your tokens.

This can be especially useful when using a 3rd party library that provides custom CSS variables, like a popper library
that exposes a `--popper-reference-width`.

```ts filename="bamboo.config.ts"
import { defineConfig } from '@bamboocss/dev'

export default defineConfig({
  // ...
  global: {
    vars: {
      '--popper-reference-width': '4px',
      // you can also generate a CSS @property
      '--button-color': {
        syntax: '<color>',
        inherits: false,
        initialValue: 'blue',
      },
    },
  },
})
```

> Note: Keys defined in `global.vars` will be available as a value for _every_ utilities, as they're not bound to token
> categories.

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

const className = css({
  '--button-color': 'colors.red.300',
  // ^^^^^^^^^^^^  will be suggested

  backgroundColor: 'var(--button-color)',
  //                ^^^^^^^^^^^^^^^^^^  will be suggested
})
```
