defineChakraConfig

The function that writes your Panda config, so one run produces your stylesheet, your styled-system and its types

defineChakraConfig() is the one part of theming here that is not Panda’s API. It replaces Panda’s defineConfig — it returns the same object, so it is still a plain panda.config.ts default export:

panda.config.ts
import { defineChakraConfig } from "@chakra-ui-solid/panda-preset";
 
export default defineChakraConfig({
  include: ["./node_modules/chakra-ui-solid/dist/panda.buildinfo.json", "./src/**/*.{ts,tsx}"],
  outdir: "styled-system-app",
});

It exists for two jobs, and both are about failures Panda gives you no other guard against.

Four keys decide what Panda extracts — not what it names, what it sees. Set differently, they produce a stylesheet that is missing rules nobody asked Panda for: every <chakra.button> in your source, every style prop, or Chakra’s palette merged with Panda’s. Nothing errors, and the page is simply unstyled. Those four are a type error to pass.

And three keys have no merging seam. A config’s presets, theme and staticCss replace a preset’s rather than adding to them, so this function does the merge for you — including the one Panda’s own documentation gets wrong, which is staticCss below.

You never spread the return value, so there is no shallow-spread hazard on top of either.

The naming knobs — hash, prefix, separator — are yours, and freely. One panda codegen run writes your stylesheet, the styled-system/chakra-system.ts you hand <ChakraProvider>, and the declarations that type them, so a name can only ever disagree with itself.

What it sets

Four keys, and none of them are yours to choose:

KeyValueWhy
ejecttrueDrops @pandacss/preset-panda, Panda’s default theme, whose palette disagrees with Chakra’s about colors.gray.* — leaving a theme that is neither, with nothing to say so. You keep every utility: the preset declares @pandacss/preset-base itself
jsxFramework"solid"Extracts style props from any capitalized JSX component, which is why <Box p="4"> in your source produces a rule at all
jsxFactory"chakra"A different knob, and the one with the silent failure: chakra.button is lowercase, so without it Panda’s isUpperCase fallback declines the tag and every <chakra.button bg="…"> you write emits nothing
importMapour packages, and your outdirRegisters the chakra factory. Panda takes a factory only from an import whose name is the jsxFactory and whose module is on this list, so unset, <chakra.div bg="…"> emits nothing either. Your own outdir stays on the map beside ours, so import { flex } from "./styled-system-app/patterns" in your source extracts as it would in any Panda project

It also sets presets to [chakraSolidPreset] — Chakra’s tokens and all 75 recipes — but that one is merged, not locked: yours are appended after it.

Writing any of the four is a type error whose message names the key:

panda.config.ts
export default defineChakraConfig({
  // ✗ `jsxFactory` is set by defineChakraConfig, because it decides what Panda
  //   extracts rather than what it names
  jsxFactory: "styled",
  include: [...],
});

Two keys are set as defaults, not locks, so whatever you pass wins: preflight: true, and prefix: { cssVar: "chakra" } — the React version’s own cssVarsPrefix, which keeps our ~547 token variables out of a collision with the identically-named ones your app declares. Panda merges nothing inside prefix, so a prefix of yours replaces it whole: write cssVar too, or the variables lose their namespace.

What you can pass

Three classes, and the type tells them apart for you.

KeysWhat happens
Lockedthe four aboveA type error. If one reaches Panda anyway — an untyped panda.config.js, or a spread of the return value — your Panda run prints [chakra-ui-solid] restoring … and puts our value back
Mergedpresets, plugins, theme, staticCssAdded to ours rather than replacing it. Your presets and plugins come after ours; theme accepts only extend; staticCss is merged for you
Yoursinclude, outdir, hash, prefix, separator, utilities, conditions, globalCss, preflight, cssVarRoot, hooks, the restPassed straight through

utilities and conditions are worth naming there rather than leaving in “the rest”: a style property or a condition you invent is a typed prop on our components, because your run generates both the isCssProperty that folds it into a class and the declaration that types it. Overview has both worked through.

What it generates

Three plugins are appended to your plugins array, and two of them write files into your outdir every panda codegen:

FileWhat it is
chakra-system.tsFive imports of data and no logic — the css, patterns and recipes namespaces, isCssProperty and token, passed to createSystem(). This is what you hand <ChakraProvider>
chakra-system-types.d.tsThe declare module block telling TypeScript what this config decided: a row per recipe’s variants, a row per utilities entry, a row per conditions entry. Nothing imports it — it is picked up because it is in your program

The third plugin is the gate behind components, and prints the line every run ends with.

Neither generated file is yours to edit: every run rewrites both, so a change belongs in panda.config.ts.

include is the one required key. Panda’s default (src/**/*.{js,jsx,ts,tsx}) misses chakra-ui-solid/dist/panda.buildinfo.json, and that path is not optional: it is the channel through which values our components name, and your source never writes, reach your extractor. It is one file rather than a glob over our published sources, which also means our files can never stand in for yours — if your own globs match nothing, the run says so instead of looking fine.

theme takes extend only

extend deep-merges; the key beside it replaces. A bare theme would drop Chakra’s whole token table and all 75 recipes, so theme here accepts extend and nothing else:

panda.config.ts
export default defineChakraConfig({
  theme: { extend: { tokens: { colors: { brand: { value: "#5b8" } } } } },
  include: [...],
});

theme: { tokens: … } is a type error naming the fix.

staticCss is merged for you

Write it the way Panda documents it — the merge is this function’s job:

panda.config.ts
export default defineChakraConfig({
  staticCss: { css: [{ properties: { color: ["red.500"] } }] },
  include: [...],
});

That one needs the merge more than theme does, and neither spelling avoids the need. A config’s staticCss.css replaces a preset’s whole array, and staticCss: { extend: { css } } replaces it too. So the preset’s own seven entries are written into your config alongside yours, rather than left behind in the preset for Panda to drop — and if you write an extend block, it is folded into that same union rather than passed through.

extend does not extend staticCss, whatever you have read. Panda’s Extend page lists staticCss among the parts the keyword extends. Measured on one merge against three of its list-mates: conditions, globalCss and utilities each keep the preset’s entry and yours; staticCss keeps only yours. The three are objects keyed by name, so a clash is per name — staticCss.css is an array, and the whole array loses. Nothing warns you, which is why this function does the union itself.

Those seven are not decoration: <Flex inline>, <Wrap> and every StackSeparator flip a value at runtime, Panda’s usage scan cannot see a value that appears in no source file, and the pre-generated rule is the only thing that makes the prop do anything. Lose them and those props go quietly inert.

Not mergeConfigs. It is not exported from @pandacss/dev, so a consumer who installed only the peer dependency cannot reach it, and it returns any — discarding the Config types this function exists to supply.

An extra preset

Ours stays first, so yours is later and wins on a conflict:

panda.config.ts
export default defineChakraConfig({
  presets: [myPreset],
  include: [...],
});

responsive

Recipe variants are pre-generated at one condition by default. Pass responsive to also generate them at every breakpoint, which is what <Button size={{ base: "sm", md: "lg" }}> needs:

panda.config.ts
export default defineChakraConfig({
  responsive: { button: ["size"] },
  include: [...],
});

Three grains, so nobody pays for conditions they do not use:

GrainMeaning
{ button: ["size"] }One variant key on one recipe
["button", "heading"]Every variant key on those recipes
trueEvery variant key on all 75

Each expands to a rule Panda already understands, appended to that recipe’s own staticCss list:

defineChakraConfig({ responsive: { button: ["size"] }, include: [] }).theme.extend.recipes.button
  .staticCss;
// => ["*", { size: ["*"], responsive: true }]

The recipe’s own list is where the rule has to land, and the "*" in front of it is worth knowing about if you ever write staticCss by hand. Panda resolves staticCss.recipes[name] from the recipe body whenever the body declares one, assigning it over whatever the config asked for that recipe. None of ours declares one until an opt-in puts it there — and the config-level entry it then replaces is the one deciding whether that recipe reaches your sheet at all, the one components widens. So the list opens by asking for the whole recipe again, and opting one variant key into breakpoints never costs you the rest of it.

Rules you write under staticCss.recipes are moved to the body for you, so both spellings work:

panda.config.ts
export default defineChakraConfig({
  responsive: { button: ["size"] },
  staticCss: { recipes: { button: [{ variant: ["*"] }] } },
  include: [...],
});
// button's staticCss => ["*", { size: ["*"], responsive: true }, { variant: ["*"] }]

Off by default because it multiplies a recipe’s variant values by six — base plus five breakpoints. There are 490 of those values across the 75 recipes, and the true grain opts every one of them in.

conditional

The sibling knob, for a variant that changes on a state rather than a breakpoint — which is what <IconButton variant={{ base: "ghost", _selected: "outline" }}> needs:

panda.config.ts
export default defineChakraConfig({
  conditional: { button: { variant: ["selected"] } },
  include: [...],
});

responsive does not cover this and cannot: Panda keeps breakpoints and conditions as two separate keys on a static-css rule, so a responsive opt-in on button.variant generates md:button--variant_outline and never selected:button--variant_outline. A class with no rule renders nothing and reports nothing, so the selected item simply looks like the others.

Condition names are Panda’s own, written without the underscore a prop uses — _selected in JSX, "selected" here. Two grains:

GrainMeaning
{ button: { variant: ["selected"] } }One variant key, under one condition
{ button: ["selected"] }Every variant key on that recipe, under it

There is no true: breakpoints are a closed set, and conditions are not — your own conditions block adds to them. Both knobs can name the same recipe, and the rules stack:

defineChakraConfig({
  responsive: { button: ["size"] },
  conditional: { button: { variant: ["selected"] } },
  include: [],
}).theme.extend.recipes.button.staticCss;
// => ["*", { size: ["*"], responsive: true }, { variant: ["*"], conditions: ["selected"] }]

components

Your stylesheet carries the recipes your source imports, and nothing else. Panda reads that off the import specifiers in the files your include globs cover, so it survives an alias, a namespaced part and a wrapper of your own alike — all three still name the component in the specifier. Every Panda run prints what it found, counting the recipes each component composes — Button brings spinner with it, Field brings fieldset and icon:

🐼 chakra-ui-solid  4 components → 7 recipes: Button, Dialog, Field, Table

components names the ones no file it scanned could show it:

panda.config.ts
export default defineChakraConfig({
  // Only for components reaching your app from somewhere Panda does not scan —
  // a dependency built on chakra-ui-solid. For your own workspace package,
  // add it to `include` instead.
  components: ["Menu", "Toast"],
  include: [...],
});

It takes component names spelled the way your source spells them, and it can only widen: nothing here removes a recipe the scan found.

If a run finds no import from this library at all, it says so and generates everything, because a scan that found nothing is a broken include rather than an app with no components:

🐼 chakra-ui-solid  0 components detected — generating all recipes.
   Nothing in `include` imports from "chakra-ui-solid". Check your `include` globs.

The recipe registry

Three more exports, read off the preset rather than typed out, so a Chakra release that adds a recipe is covered by the version bump alone:

import { recipeKeys, slotRecipeKeys, variantKeysFor } from "@chakra-ui-solid/panda-preset";
 
recipeKeys.length; // => 19 — button, input, heading, …
slotRecipeKeys.length; // => 56
variantKeysFor().find((entry) => entry.recipe === "button");
// => { recipe: "button", keys: ["size", "variant"] }

variantKeysFor() is what expands the coarser grains of both opt-ins: responsive: ["button"] and conditional: { button: ["selected"] } each mean every variant key on Button, and nothing else knows which those are.