Overview

A guide for configuring the chakra-ui-solid theming system

Architecture

The React version’s theming system is built around the API of Panda CSS. This one is built on Panda CSS. The config object is therefore close to the same object — it lives in panda.config.ts, and a build compiles it instead of an engine serializing it while your app runs.

Almost nothing on this page is ours. theme.extend, tokens, semantic tokens, recipes, conditions, utilities — all of it is Panda’s own API, and Panda’s docs are the reference for any key not covered here. This library contributes two things to it: the preset carrying Chakra’s design system, and defineChakraConfig(), which stands in for Panda’s defineConfig.

Three steps, and they are the React version’s three:

  • Define the styling system configuration using defineChakraConfig
  • Create the styling engine — panda codegen writes it for you, as styled-system/chakra-system.ts
  • Pass the styling engine to the ChakraProvider component
panda.config.ts
import { defineChakraConfig } from "@chakra-ui-solid/panda-preset";
 
export default defineChakraConfig({
  theme: {
    extend: {
      tokens: {
        colors: { brand: { 500: { value: "tomato" } } },
      },
    },
  },
  include: ["./node_modules/chakra-ui-solid/dist/panda.buildinfo.json", "./src/**/*.{ts,tsx}"],
  outdir: "styled-system-app",
});
src/app.tsx
import { Box, ChakraProvider } from "chakra-ui-solid";
import { system } from "../styled-system-app/chakra-system";
 
export default function App() {
  return (
    <ChakraProvider value={system}>
      <Box>Hello World</Box>
    </ChakraProvider>
  );
}

The one difference with the React version’s theme.ts is where the two halves come from. There, createSystem(defaultConfig, config) builds the engine while your app starts, and the engine writes the CSS. Here, one panda codegen run over one config writes both the stylesheet and the system module above — so the class names on the element and the rules behind them agree by construction rather than by two sides being configured the same way.

defineChakraConfig() already puts the preset in presets — do not import it again here. It is also the only call in the file: there is no defineConfig to wrap it in and nothing to spread, so no key you write can replace one it set. Installation covers the setup this page assumes.

Config

The system is configured using the defineChakraConfig function. This function accepts a configuration object that allows you to customize the styling system’s behavior.

After a config is defined, panda codegen reads it and writes the stylesheet and the system module.

cssVarRoot

cssVarRoot is the root element where the token CSS variables will be applied. Note the name: the React version spells it cssVarsRoot.

panda.config.ts
export default defineChakraConfig({
  cssVarRoot: ":where(:root, :host)",
  include: [...],
});

prefix

prefix names both halves at once: cssVar prefixes the token variables, className prefixes every rule in the sheet.

panda.config.ts
export default defineChakraConfig({
  prefix: { cssVar: "ck", className: "ck" },
  include: [...],
});

{ cssVar: "chakra" } is the default, which is what the React version’s cssVarsPrefix spells, and it keeps our ~547 token variables — --chakra-spacing-4, --chakra-colors-red-500 — out of a collision with the identically-named ones your app declares. Panda merges nothing inside this key, so a prefix of yours replaces the default outright: write cssVar too, or the variables lose their namespace.

className is what you need when two systems render on one page — see Multiple systems.

globalCss

globalCss is used to apply global styles to the system.

panda.config.ts
export default defineChakraConfig({
  globalCss: {
    "html, body": { margin: 0, padding: 0 },
  },
  include: [...],
});

preflight

preflight is used to apply css reset styles to the system. defineChakraConfig() turns it on; pass false to drop it, or a scope to confine it to one element.

panda.config.ts
export default defineChakraConfig({
  preflight: { scope: ".chakra-reset" },
  include: [...],
});

theme

Use the theme config property to define the system theme. Under extend, it accepts the same properties the React version’s does:

  • breakpoints: for defining breakpoints
  • keyframes: for defining css keyframes animations
  • tokens: for defining tokens
  • semanticTokens: for defining semantic tokens
  • textStyles: for defining typography styles
  • layerStyles: for defining layer styles
  • recipes: for defining component recipes
  • slotRecipes: for defining component slot recipes
panda.config.ts
export default defineChakraConfig({
  theme: {
    extend: {
      breakpoints: { sm: "320px", md: "768px", lg: "960px", xl: "1200px" },
      tokens: {
        colors: { red: { value: "#EE0F0F" } },
      },
      semanticTokens: {
        colors: { danger: { value: "{colors.red}" } },
      },
      keyframes: {
        spin: {
          from: { transform: "rotate(0deg)" },
          to: { transform: "rotate(360deg)" },
        },
      },
    },
  },
  include: [...],
});

Recipes are keyed by the names the preset already registers — button, input, heading and the rest — so overriding one is adding to that key rather than declaring a new recipe:

panda.config.ts
theme: {
  extend: {
    recipes: {
      button: { variants: { size: { xl: { px: "8", h: "14" } } } },
    },
  },
}

A variant key the recipe did not have works the same way, and reaches the component as a prop of its own:

panda.config.ts
theme: {
  extend: {
    recipes: {
      button: { variants: { tone: { brand: { bg: "brand.500" } } } },
    },
  },
}
<Button tone="brand">Save</Button>

The component reads its variant key list off the recipe your config produced, so tone is passed to the recipe rather than left on the <button> as an attribute.

A bare theme would drop Chakra’s whole token table and all 75 recipes — extend merges, the key itself replaces. So theme here accepts extend and nothing else: theme: { tokens: … } is a type error naming the fix.

utilities

Use the utilities config property to create a style property of your own, map an existing one to a set of values, or give it a shorthand. Three keys define one:

  • shorthand: the alias version of the property
  • values: what it accepts — a token category, an enum, a map, or string / number
  • transform: a function turning the value into a style object

Say you want a br property that applies a border radius:

panda.config.ts
export default defineChakraConfig({
  utilities: {
    extend: {
      br: {
        values: "radii",
        transform(value) {
          return { borderRadius: value };
        },
      },
    },
  },
  include: [...],
});

br is then a style prop on every component, and it type-checks:

src/app.tsx
import { Box } from "chakra-ui-solid";
 
<Box br="sm" />;

Enum values and mapped values work the same way, and transform is handed token as its second argument:

panda.config.ts
export default defineChakraConfig({
  utilities: {
    extend: {
      borderX: {
        values: ["1px", "2px", "4px"],
        shorthand: "bx",
        transform(value, { token }) {
          return { borderInlineWidth: value, borderColor: token("colors.red.200") };
        },
      },
    },
  },
  include: [...],
});

The name is a style key, not a second prop bag, so it is legal everywhere one goes — as a prop, inside css, inside a condition, and inside a condition inside a condition:

<Box br="sm" />
<Box css={{ br: "sm" }} />
<Box _hover={{ br: "lg", _dark: { br: "sm" } }} />

conditions

Use the conditions config property to define custom selectors and media query conditions for use in the system.

panda.config.ts
export default defineChakraConfig({
  conditions: { cqSm: "@container(min-width: 320px)", child: "& > *" },
  include: [...],
});

Write it with the leading underscore a prop uses, and it works on our components and in your own css() alike:

<Box mt="40px" _cqSm={{ mt: "0px" }}>
  <Text>Hello World</Text>
</Box>

It nests as far as a built-in condition does, because it is one — the same generated Conditions interface carries both:

<Box _hover={{ _cqSm: { mt: "0px" } }} />

strictTokens

strictTokens enforces the usage of only design tokens, raising a TS error on a raw value. Setting it applies to the runtime your Panda run generates. It does not tighten the style props on our components: those types come from our published declarations, which were generated without it.

TypeScript

There is no separate typegen step, and nothing to remember to run. panda codegen writes styled-system/chakra-system-types.d.ts beside the system module, off the types it generated from the same config:

pnpm panda codegen

That file is what makes a utilities entry a typed style prop, a conditions entry a typed condition, and a recipe variant you added — value or key — a typed prop. Nothing imports it; it is picked up because it sits in your tsconfig.json’s program.

The React version reaches the same place with @chakra-ui/cli typegen, one step later and one step you have to remember. Add panda codegen to a prepare script and there is no step at all:

package.json
{ "scripts": { "prepare": "panda codegen" } }

Keys the config sets for you

Four, and each decides what Panda extracts rather than what it names: eject, jsxFramework, jsxFactory and importMap. Changing one produces a stylesheet missing rules nobody asked Panda for, with nothing anywhere to say so, so all four are a type error to pass.

presets and staticCss are merged instead — your presets come after ours, and your staticCss is unioned with the preset’s. defineChakraConfig is the full account.

Every naming knob is yours: hash, prefix and separator decide what the classes and variables are called, and one Panda run names both the element and the sheet.

The system object

The React version’s createSystem returns a system with helpers hanging off it. Ours is the same object with the members Panda already consumed at build time left off, and panda codegen assembles it into styled-system/chakra-system.ts:

The React versionHere
createSystem(defaultConfig, config)system from ./styled-system/chakra-system
system.token("colors.red.500")token("colors.red.500") from ./styled-system/tokens
system.token.var("colors.red.500")token.var("colors.red.500")
system.css({ ... })css({ ... }) from ./styled-system/css
system.cva({ ... })cva({ ... })
system.sva({ ... })sva({ ... })
system.isValidProperty(prop)isCssProperty(prop) from ./styled-system/jsx/is-valid-prop
system.splitCssProps(props)splitCssProps(props) from the same module
system.breakpoints.up("sm")No counterpart
system.tokens.flatMapNo counterpart
system._global, system.layersNo counterpart — the provider renders no stylesheet
import { css } from "../styled-system-app/css";
import { token } from "../styled-system-app/tokens";
 
token("colors.red.500");
// => "#ef4444"
 
token.var("colors.red.500");
// => "var(--chakra-colors-red-500)"
 
css({ color: "red.500", bg: "blue.200" });
// => "c_red.500 bg_blue.200"

One difference worth reading twice: system.css returns a style object for a CSS-in-JS library to insert, and css here returns a class name string naming rules your stylesheet already contains. cva and sva likewise return functions that return class names.

Inside a component, the same system is on the context the provider carries:

import { useChakraContext } from "chakra-ui-solid";
 
function Swatch() {
  const system = useChakraContext();
  return <div class={system().css({ bg: "red.500" })} />;
}

It is an accessor, because Solid’s contexts are not reactive: read inside the class getter, a provider handed a new system restyles what is already on the page. That is the whole of Multiple systems.