Skip to content

Commit d1812f3

Browse files
committed
docs: make PANDA.md mistake-first
Rewrite the Panda guide around the errors agents actually make: dynamic values that emit no CSS, Tailwind class strings, `var(--x)` instead of token names, `:hover` instead of `_hover`, `md:` prefixes, and recipe variant props that only emit defaultVariants. Each is a wrong-vs-right pair with a one-line why.
1 parent 8bce12b commit d1812f3

4 files changed

Lines changed: 464 additions & 304 deletions

File tree

PANDA.md

Lines changed: 116 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,142 @@
1-
# How to write Panda in these examples
1+
# Writing Panda without the usual mistakes
22

3-
This is what you need to write correct Panda here and stop guessing. The examples run Panda v2 (`2.0.0-beta.12`). The authoring API is the same as v1; what changed is the compiler and the way libraries ship and get consumed. Full docs are at <https://panda-css.com>. For the beta specifics, see the [v2 migration guide](https://github.com/chakra-ui/panda/blob/main/V2_MIGRATION.md).
3+
Read this before you write a line of Panda in these examples. It's the API and, more to the point, the mistakes that trip up anyone coming from Tailwind or Panda v1. The examples run Panda v2 (`2.0.0-beta.12`). Full docs: <https://panda-css.com>. Beta specifics: the [v2 migration guide](https://github.com/chakra-ui/panda/blob/main/V2_MIGRATION.md).
44

5-
Panda runs at build time. You write style objects, the CLI extracts them statically and generates atomic CSS plus a typed `styled-system/`. It can't see runtime values, so style with static ones.
5+
## The one rule behind most mistakes: styles must be static
66

7-
## Where to import from
7+
Panda reads your code at build time and generates CSS from what it can see. It runs no JavaScript. If a style value isn't a literal it can read at the call site, it generates nothing, and you get a `className` with no CSS behind it. No error, just a missing style.
88

9-
| You want | Import from |
10-
| --- | --- |
11-
| `css`, `cx`, `cva`, `sva` | local `styled-system/css` |
12-
| patterns (`stack`, `hstack`, `grid`, …) | local `styled-system/patterns` |
13-
| pattern JSX (`<Stack>`, `<Box>`) | local `styled-system/jsx` |
14-
| generated recipe functions | local `styled-system/recipes` |
15-
| the `token()` helper | local `styled-system/tokens` |
9+
```tsx
10+
// ❌ nothing is generated — the value isn't known at build time
11+
css({ color: props.color })
12+
css({ color: `red.${shade}` })
13+
css({ color: colorByType[type] })
14+
15+
// ✅ literals, ternaries of literals, and same-file constants all work
16+
const accent = 'red.300'
17+
css({ color: accent })
18+
css({ color: isActive ? 'red.500' : 'red.600' }) // both classes emitted
19+
```
1620

17-
Always the local `styled-system`, the one this app's `panda build` generated. Never from the `pandacn` package. The `standalone-app` example has no `styled-system` at all; see its AGENTS.md.
21+
When a value is genuinely dynamic, pick one of these:
1822

19-
## Writing styles with css()
23+
```tsx
24+
// pick the class from a map of literals
25+
const byShade = { 300: css({ color: 'red.300' }), 500: css({ color: 'red.500' }) }
26+
<p className={byShade[shade]} />
2027

21-
```ts
22-
css({
23-
display: 'flex',
24-
px: '4', py: '2', // shorthands; '4' is a spacing token
25-
bg: 'primary', // token dot-path (a semantic token)
26-
color: 'red.400', // token dot-path (a scale token)
27-
rounded: 'md',
28-
width: '760px', // arbitrary value, just a string
29-
background: 'var(--colors-frame-brand)', // a raw CSS var when you need one
30-
})
28+
// or hand Panda a CSS var and set it inline with token()
29+
<div className={css({ color: 'var(--c)' })} style={{ '--c': token(`colors.${props.color}`) }} />
3130
```
3231

33-
Token names resolve to CSS vars; arbitrary values pass straight through. To read a token in JS, use `token('colors.primary')`.
32+
Or pre-generate variants with `staticCss` (see recipes below). The diagnostic for this is `panda_call_unextractable`.
3433

35-
## Conditions: pseudo, state, and selectors
34+
## Panda is not Tailwind
3635

37-
Underscore keys are conditions. String keys with `&` are raw selectors.
36+
There are no utility class strings. Style with objects through `css()`.
3837

39-
```ts
38+
```tsx
39+
// ❌ className="flex gap-4 hover:bg-red-500 md:px-5"
40+
//
41+
className={css({ display: 'flex', gap: '4', _hover: { bg: 'red.500' }, px: { base: '4', md: '5' } })}
42+
```
43+
44+
## Reference tokens by name, not by var
45+
46+
A token is a bare dot-path. Not a CSS var, not `$name`, not `theme()`.
47+
48+
```tsx
49+
// ❌ bg: 'var(--colors-red-400)' ❌ bg: '$red.400' ❌ bg: theme('colors.red.400')
50+
//
51+
css({ bg: 'red.400', color: 'primary' })
52+
```
53+
54+
Use a raw `var(...)` only when you're deliberately holding a runtime value (see the static rule above). `token('colors.red.300')` reads a token in JS; `token.var('colors.red.300')` gives its var reference.
55+
56+
## The spacing scale: '4' is a token, '4px' is not
57+
58+
Quoted scale steps hit the token scale. A raw length bypasses it.
59+
60+
```tsx
61+
css({ p: '4' }) // ✅ spacing token spacing.4 → 1rem
62+
css({ p: '4px' }) // ✅ literal length, no token — only when you mean an exact pixel value
63+
```
64+
65+
## Conditions use _hover, not :hover
66+
67+
State and pseudo-classes are underscore keys. Raw child selectors need a literal `&`. Pseudo-element `content` must carry its own quotes.
68+
69+
```tsx
70+
// ❌ ':hover': {…} ❌ '&:hover': {…} ❌ 'span': {…} (missing &)
71+
//
4072
css({
41-
color: 'ink',
42-
_hover: { color: 'brand' },
43-
_focusVisible: { boxShadow: '0 0 0 3px …' },
73+
_hover: { bg: 'red.700' },
4474
_disabled: { opacity: 0.5 },
45-
_dark: { color: 'white' },
46-
'& svg': { flexShrink: 0 },
47-
'&[data-state=open]': { bg: 'card' },
75+
'& span': { color: 'pink.400' },
76+
_before: { content: '"👋"' },
4877
})
4978
```
5079

51-
Dark mode here is class-based. The configs set `conditions: { extend: { dark: '.dark &' } }`, so `_dark` means "somewhere under a `.dark` ancestor". Toggle `.dark` on `<html>`, and semantic tokens apply their `_dark` values on their own.
52-
53-
## Responsive styles
80+
Order matters: `_dark: { _backdrop: {…} }` is valid, the reverse isn't. Dark mode here is class-based: the configs set `dark: '.dark &'`, so `_dark` applies under a `.dark` ancestor. Toggle `.dark` on `<html>`; semantic tokens switch on their own.
5481

55-
Mobile-first, keyed by breakpoint (`base`, `sm`, `md`, `lg`, `xl`, `2xl`):
82+
## Responsive is an object, not md: prefixes
5683

57-
```ts
58-
css({ paddingInline: { base: '5', md: '8' }, fontSize: { base: 'sm', lg: 'md' } })
84+
```tsx
85+
// ❌ className="md:px-5" ❌ <Box md={{ px: '5' }} />
86+
// ✅ per property
87+
css({ px: { base: '4', md: '5' } })
88+
// ✅ or a breakpoint block
89+
css({ base: { px: '4' }, md: { px: '5' } })
5990
```
6091

61-
The array form `['5', '8']` maps to breakpoints in order. Prefer the object form; it says what it means.
92+
Breakpoints are `sm md lg xl 2xl`, mobile-first. On patterns, put the breakpoints on the pattern prop itself: `<Grid columns={{ base: 1, md: 2 }} />`.
93+
94+
## Fading a color with /
95+
96+
Append `/{n}` to a color token to mix in transparency.
97+
98+
```tsx
99+
css({ bg: 'red.400/50' }) // ✅ 50% via color-mix
100+
css({ '--overlay': '{colors.black/50}' }) // ✅ inside a var, wrap the token in braces
101+
```
62102

63103
## Combining classes with cx()
64104

65-
`cx()` joins class strings and resolves atomic conflicts, last one wins. Reach for it in any component that takes a `className`:
105+
`cx()` joins class strings and resolves atomic conflicts, last one wins. Use it wherever a component takes a `className`:
66106

67-
```ts
107+
```tsx
68108
cx(button({ variant, size }), css({ mt: '2' }), className)
69109
```
70110

71-
## Recipes for a single element
111+
## Recipes, and the dynamic-variant trap
72112

73-
A recipe is variant-driven styling for one element. Author it with `defineRecipe`, register it in `panda.config.ts` under `theme.extend.recipes`, and Panda generates it into `styled-system/recipes`:
113+
A recipe is variant-driven styling for one element. Author it with `defineRecipe`, register it in `panda.config.ts` under `theme.extend.recipes`, and consume the generated function from `styled-system/recipes`:
74114

75115
```ts
76116
export const button = defineRecipe({
77117
className: 'btn',
78-
jsx: ['Button'],
79118
base: { display: 'inline-flex', rounded: 'md' },
80119
variants: {
81120
variant: { default: { bg: 'primary' }, ghost: { bg: 'transparent' } },
82121
size: { sm: { h: '8' }, md: { h: '9' } },
83122
},
84-
compoundVariants: [{ variant: 'ghost', size: 'sm', css: { px: '2' } }],
85123
defaultVariants: { variant: 'default', size: 'md' },
86124
})
87125
```
88126

89-
The generated function returns a class string:
127+
The same static rule applies to variant props:
90128

91129
```tsx
92-
import { button } from '../../styled-system/recipes'
93-
<button className={cx(button({ variant, size }), className)} />
130+
button({ size: 'lg' }) // ✅ emits lg
131+
button({ size: wide ? 'sm' : 'lg' }) // ✅ emits both
132+
button({ size }) // ❌ runtime prop → only defaultVariants generated
94133
```
95134

96-
Inline `cva({...})` from `styled-system/css` works the same but is atomic, and it emits every variant. A config recipe is JIT: it only emits variants it sees used, so a dynamic prop like `button({ variant: someProp })` falls back to `defaultVariants` unless `staticCss` ships it. That's why these examples set `staticCss: { recipes: '*' }`. One more catch: `compoundVariants` turns off responsive and conditional variant props on a config recipe. The generated function also carries `.raw()`, `.variantKeys`, and `.splitVariantProps(props)`.
135+
Fix a genuinely dynamic prop with `staticCss` on the recipe (`staticCss: ['*']`, or list the variants). These examples set `staticCss: { recipes: '*' }` in the config for exactly this reason. Two more catches: `compoundVariants` disables responsive variant props on a config recipe, and inline `cva({...})` from `styled-system/css` never supports responsive variant props (it does emit every variant, though). Recipe functions also carry `.raw()`, `.variantKeys`, and `.splitVariantProps(props)`.
97136

98-
## Recipes for multi-part components
137+
## Multi-part components use slot recipes
99138

100-
For a component with several parts (a Card is root, header, title, and so on), use `defineSlotRecipe` and register it under `theme.extend.slotRecipes`. The generated function returns one class per slot:
139+
For a component with parts (a Card is root, header, title), use `defineSlotRecipe` under `theme.extend.slotRecipes`. The generated function returns one class per slot:
101140

102141
```ts
103142
export const card = defineSlotRecipe({
@@ -112,7 +151,7 @@ export const card = defineSlotRecipe({
112151

113152
## Layout patterns
114153

115-
Patterns are prebuilt layout helpers: `stack`, `hstack`, `vstack`, `flex`, `grid`, `gridItem`, `box`, `center`, `circle`, `square`, `container`, `aspectRatio`, `bleed`, `float`, `spacer`, `divider`, `wrap`, `cq`, `linkOverlay`, `visuallyHidden`.
154+
Prebuilt layout helpers: `stack`, `hstack`, `vstack`, `flex`, `grid`, `gridItem`, `box`, `center`, `circle`, `square`, `container`, `aspectRatio`, `bleed`, `float`, `spacer`, `divider`, `wrap`, `cq`, `linkOverlay`, `visuallyHidden`.
116155

117156
```tsx
118157
import { stack } from '../styled-system/patterns'
@@ -122,11 +161,9 @@ import { Stack } from '../styled-system/jsx'
122161
<Stack gap="4" align="center" />
123162
```
124163

125-
Put breakpoints on the pattern prop itself (`columns={{ base: 1, md: 2 }}`), not in a separate condition block.
164+
## Defining tokens and semantic tokens
126165

127-
## Tokens and semantic tokens
128-
129-
Tokens are raw values. Semantic tokens resolve by condition, so they're how you do light and dark. Both nest under `{ value }`:
166+
Tokens are raw values; semantic tokens resolve by condition, which is how light and dark work. Both nest under `{ value }`:
130167

131168
```ts
132169
tokens: { colors: { brand: { value: '#5b8def' } }, fonts: { body: { value: 'Inter, sans-serif' } } }
@@ -136,32 +173,35 @@ semanticTokens: { colors: {
136173
} }
137174
```
138175

139-
Reference either by dot-path in `css()`: `bg: 'brand'`, `color: 'primary'`. A bare name resolves the token's `DEFAULT` key.
176+
Then reference by dot-path in `css()`: `bg: 'brand'`, `color: 'primary'`. A bare name resolves the token's `DEFAULT` key.
140177

141-
## Composing styles across files with css.raw()
178+
## Where to import from
142179

143-
`css.raw()` returns the style object instead of a class, so you can define styles in one file and compose them in another. v2 folds static named imports:
180+
Crossing these is a common mistake:
144181

145-
```ts
146-
export const iconStyle = css.raw({ width: '4', flexShrink: 0 }) // styles.ts
147-
css(iconStyle, { color: 'currentColor' }) // elsewhere
148-
css({ '& svg': { ...iconStyle } }) // spread into a selector
149-
```
182+
| You want | Import from |
183+
| --- | --- |
184+
| `css`, `cx`, `cva`, `sva`, `token`, `styled` | local `styled-system/*` (generated) |
185+
| patterns and pattern JSX | local `styled-system/patterns` and `styled-system/jsx` |
186+
| generated recipe functions | local `styled-system/recipes` |
187+
| `defineConfig`, `defineRecipe`, `defineSlotRecipe` | `@pandacss/dev` (config only) |
188+
189+
Always the local `styled-system`, the one this app's `panda build` generated. Never runtime helpers from the `pandacn` package. The `standalone-app` example has no `styled-system` at all; see its AGENTS.md.
150190

151191
## v2 beta gotchas
152192

153-
- ESM only, Node 22 or newer. No `require()`. `panda.config.ts` loads as ESM.
154-
- Presets aren't auto-injected. A config needs `presets: ['@pandacss/preset-base', '@pandacss/preset-panda']`, or `designSystem`, which pulls them in. Without them you get a bare system: no `bg`/`color` utilities, no scales, no `_hover`. Both preset packages have to be installed.
155-
- Re-run `panda build` after you change tokens, recipes, or patterns. These apps do it for you in `predev`/`prebuild`.
156-
- Don't edit `styled-system/`. It's generated, and your changes are overwritten.
157-
- `createStyleContext` is gone in v2. Use `createRecipeContext` for a `cva` recipe, `createSlotRecipeContext` for an `sva` one.
158-
- Extraction is static at the call site. A prop renamed or forwarded through a wrapper (`<Button size={circleSize} />`) isn't tracked. Keep real prop names; for arbitrary ones, pass `css.raw({...})`.
159-
- With `strictTokens` on, arbitrary values are rejected. Use the `[…]` escape hatch: `bg: '[#abc]'`, `fontSize: '[13px]'`. These examples don't turn it on.
193+
- ESM only, Node 22 or newer. No `require()`.
194+
- Presets aren't auto-injected. A config needs `presets: ['@pandacss/preset-base', '@pandacss/preset-panda']`, or `designSystem`, which pulls them in. Without them you get a bare system: no `bg`/`color`, no scales, no `_hover`. Both packages must be installed.
195+
- Run `panda build` after changing tokens, recipes, or patterns, and before `styled-system` types exist. These apps do it in `predev`/`prebuild`.
196+
- Don't edit `styled-system/`. It's generated and gets overwritten.
197+
- `!important` is a suffix on the value: `css({ color: 'red!' })`.
198+
- With `strictTokens` on, arbitrary values are rejected. Escape with brackets: `bg: '[#abc]'`, `fontSize: '[13px]'`. These examples don't turn it on.
199+
- `createStyleContext` is gone. Use `createRecipeContext` (cva) or `createSlotRecipeContext` (sva).
160200

161201
## Shipping and consuming the design system
162202

163-
Here's how `pandacn` reaches the three apps. The `monorepo` example is the source.
203+
How `pandacn` reaches the three apps. The `monorepo` example is the source.
164204

165-
- Shipping: `panda lib` builds `dist/panda/lib.json`, a `preset.mjs`, and build info, then syncs `package.json` exports. It bundles the whole system, every token, recipe, and variant, which is why the source sets `staticCss: { recipes: '*' }`.
166-
- Consuming with Panda: the app sets `designSystem: 'pandacn'` in `panda.config.ts`. Panda resolves `pandacn/panda/lib.json`, merges its preset, and the app emits only its own additions. You import from the app's local `styled-system`.
167-
- Consuming without Panda: import the prebuilt `pandacn/styles.css` and the React components. That's the `standalone-app` example.
205+
- Ship: `panda lib` builds `dist/panda/lib.json`, a `preset.mjs`, and build info, then syncs `package.json` exports. It bundles every token, recipe, and variant, which is why the source sets `staticCss: { recipes: '*' }`.
206+
- Consume with Panda: set `designSystem: 'pandacn'` in `panda.config.ts`. Panda merges its preset and the app emits only its own additions. Import from the app's local `styled-system`.
207+
- Consume without Panda: import the prebuilt `pandacn/styles.css` and the React components. That's the `standalone-app` example.

0 commit comments

Comments
 (0)