Recipes

Writing multi-variant styles with recipes in Cerberus.

Overview

Cerberus provides a way to write CSS-in-JS with better performance, developer experience, and composability. One of its key features is the ability to create multi-variant styles with a type-safe runtime API.

A recipe consists of these properties:

  • className: The className to attach to the component
  • base: The base styles for the component
  • variants: The different visual styles for the component
  • compoundVariants: The different combinations of variants for the component
  • defaultVariants: The default variant values for the component

Defining the recipe

Use the defineRecipe identity function to create a recipe.

button.recipe.ts
import { defineRecipe } from "@pandacss/dev"
export const buttonRecipe = defineRecipe({
base: {
display: "flex",
},
variants: {
visual: {
solid: { bg: "red.200", color: "white" },
outline: { borderWidth: "1px", borderColor: "red.200" },
},
size: {
sm: { padding: "4", fontSize: "12px" },
lg: { padding: "8", fontSize: "24px" },
},
},
})

Then add it to your theme:

panda.config.ts
import {
createCerberusConfig,
createCerberusPreset,
} from '@cerberus/panda-preset'
export default createCerberusConfig({
presets: [createCerberusPreset()],
include: ['./app/**/*.{ts,tsx}'],
exclude: [],
theme: {
extend: {
recipes: {
myButton: buttonRecipe,
},
},
}
})

Using the recipe

After defining recipes, we recommend using the Panda CLI to generate theme typings for your recipe.

Terminal window
npm panda codegen

There are two ways to use the recipe in a component:

  • Directly in the component
  • Creating a component (recommended) with the Cerberus factory

With the Cerberus Factory

Import the button recipe from your local styled-system/recipes folder and use it within your component.

button.tsx
import { createCerberusPrimitive } from "@cerberus/react"
import { buttonRecipe, type ButtonVariantProps } from "./button.recipe"
const { withRecipe } = createCerberusPrimitive<ButtonVariantProps>(buttonRecipe)
function MyCustomButton(props: ButtonVariantProps) {
const dynamicValue = props.something ? 'yes' : 'no'
return <button data-dynamic-thing={dynamicValue} {...props}>Click Me</button>
}
export const Button = withRecipe(MyCustomButton)

Directly in the Component

splitVariantProps

You can also use the [recipe].splitVariantProps function to split the recipe props from the component props. This is useful if you are not using the factory to create components.

button.tsx
'use client';
import { cerberus, type CerberusPrimitiveProps } from "@cerberus/react"
import { buttonRecipe, type ButtonVariantProps } from "./button.recipe"
interface CustomButtonProps extends ButtonVariantProps {
doSomething: () => void
}
export function MyCustomButton(props: CerberusPrimitiveProps<CustomButtonProps>) {
const [recipeProps, restProps] = buttonRecipe.splitVariantProps(props)
const styles = buttonRecipe(recipeProps)
return(
<cerberus.button
{...restProps}
className={styles}
onClick={restProps.doSomething}
>
Click Me
</cerberus.button>
)
}

TypeScript

To infer the recipe variant prop types, use the *VariantProps type.

button.tsx
import { buttonRecipe, type ButtonVariantProps } from "./button.recipe"
export interface ButtonProps extends ButtonVariantProps {}

Default Variants

The defaultVariants property is used to set the default variant values for the recipe. This is useful when you want to apply a variant by default.

button.recipe.ts
import { defineRecipe } from "@pandacss/dev"
export const buttonRecipe = defineRecipe({
base: {
display: "flex",
},
variants: {
visual: {
solid: { bg: "red.200", color: "white" },
outline: { borderWidth: "1px", borderColor: "red.200" },
},
size: {
sm: { padding: "4", fontSize: "12px" },
lg: { padding: "8", fontSize: "24px" },
},
},
defaultVariants: {
visual: "solid",
size: "sm",
},
})

Compound Variants

Use the compoundVariants property to define a set of variants that are applied based on a combination of other variants.

button.recipe.ts
import { defineRecipe } from "@pandacss/dev"
export const buttonRecipe = defineRecipe({
base: {
display: "flex",
},
variants: {
visual: {
solid: { bg: "red.200", color: "white" },
outline: { borderWidth: "1px", borderColor: "red.200" },
},
size: {
sm: { padding: "4", fontSize: "12px" },
lg: { padding: "8", fontSize: "24px" },
},
},
compoundVariants: [
{
size: "small",
visual: "outline",
css: {
borderColor: "blue.500",
},
},
]
})

When you use the size="small" and visual="outline" variants together, the compoundVariants will apply the css property to the component.

app.tsx
<Button size="small" visual="outline">
Click Me
</Button>

Caveat

Due to the design constraints, using compoundVariants with responsive values doesn't work.

This means a code like this will not work:

<Button size={{ base: "sm", md: "lg" }} visual="outline">
Click Me
</Button>

For this cases, we recommend rendering multiple versions of the component with different breakpoints, then hide/show as needed.