Skip to content

sva

sva creates a slot recipe, which styles a component made of several elements, its slots, and returns an object with the class name of every slot. It is also exported as createSlotRecipe.

import { sva } from "@lynstack/class-recipe";
export const card = sva({
slots: ["root", "header", "body"],
base: {
root: "rounded-lg border",
header: "font-semibold",
body: "text-gray-600",
},
variants: {
size: {
sm: { root: "p-3", header: "text-sm" },
md: { root: "p-5", header: "text-base" },
},
elevated: {
true: { root: "shadow-md" },
},
},
compoundVariants: [
{
variants: { size: "md", elevated: true },
classNames: { header: "border-b" },
},
],
defaultVariants: { size: "md" },
});

PlaygroundChoose the variants. The classes a choice adds light up.

size
elevated
card()→ {    root: "rounded-lg border p-5",    header: "font-semibold text-base",    body: "text-gray-600",  }

A slot recipe takes the config of a recipe, with slots, and with an object of classes keyed by slot name wherever a recipe takes a string:

Property Description
slots The names of the slots, in the order of the result.
base Optional. The classes each slot always has.
variants For each variant name, the classes of each slot for each of its options.
compoundVariants Optional. Classes added to some slots, under classNames, when several variants match.
defaultVariants Optional. The option each variant uses when a selection leaves it out.
cache Optional. Whether the slot recipe caches its class names. Defaults to the cache option of createRecipes, which is true.

An option or compound variant gives classes only to the slots it names. A slot that slots does not name is a type error, and its classes are ignored.

A slot recipe returns an object with the class name of every slot, in the order of slots, and "" for a slot without classes. Each slot’s classes are added in the order of a recipe: base, the variants, the matching compound variants, then the slot’s classNames. The result is frozen, and calling the recipe again with the same variants returns the same object, which keeps props stable for memoized components:

card({ elevated: true }) === card({ elevated: true }); // => true

A variant listed in defaultVariants may be left out or passed as undefined, and then uses its default. A variant without a default is required. When every variant has a default, the argument itself is optional, as for card, whose elevated is boolean:

card()→ {    root: "rounded-lg border p-5",    header: "font-semibold text-base",    body: "text-gray-600",  }card({ size: undefined })→ {    root: "rounded-lg border p-5",    header: "font-semibold text-base",    body: "text-gray-600",  }

alert has no default for tone, so a call cannot leave it out:

import { sva } from "@lynstack/class-recipe";
export const alert = sva({
slots: ["root", "title"],
base: { root: "rounded-md p-4", title: "font-semibold" },
variants: {
tone: {
info: { root: "bg-blue-50", title: "text-blue-900" },
danger: { root: "bg-red-50", title: "text-red-900" },
},
size: { sm: { root: "text-sm" }, md: { root: "text-base" } },
},
defaultVariants: { size: "md" },
});
alert({ tone: "danger" }).root→ "rounded-md p-4 bg-red-50 text-base"

A variant with an option named "true" or "false" also accepts the booleans true and false, and declares the missing one of those two options without classes. A variant whose only options are "true" and "false" is optional and defaults to false, as elevated does:

card({ elevated: false }).root→ "rounded-lg border p-5"card({ elevated: true }).root→ "rounded-lg border p-5 shadow-md"

An option whose name is a number accepts that number as well as the string:

import { sva } from "@lynstack/class-recipe";
export const heading = sva({
slots: ["root", "anchor"],
base: { anchor: "opacity-0" },
variants: {
level: { 1: { root: "text-3xl" }, 2: { root: "text-2xl" } },
},
});
heading({ level: 1 }).root→ "text-3xl"heading({ level: "2" }).root→ "text-2xl"

A compound variant adds its classNames, keyed by slot, when several variants have particular options at the same time. For each variant it names, it gives one option or a list of options; a variant it leaves out matches any option. In card, the header gets a border only when the card is both md and elevated:

card({ elevated: true }).header→ "font-semibold text-base border-b"card({ size: "sm", elevated: true }).header→ "font-semibold text-sm"

Conditions are checked after defaults are applied, which is why the first call matches size: "md" without passing it. Matching compound variants are added in the order they are declared. A compound variant that names an undeclared variant or option never matches; its config is a type error.

Pass classNames, an object of classes keyed by slot name, to add classes after every class of those slots:

card({ size: "sm", classNames: { body: "italic" } })→ {    root: "rounded-lg border p-3",    header: "font-semibold text-sm",    body: "text-gray-600 italic",  }

Passing it with at least one class returns a new object and leaves the cached one unchanged. With the default join they are added, not substituted: one that sets the same CSS property as a class of the slot leaves both in its class name. Design the recipe so that it needs no override (see Writing conflict-free recipes), or use a join function such as twMerge (see Resolving conflicts with tailwind-merge) to let these classes replace conflicting ones.

  • The types accept only the options the config declares. A value from untyped data can still bypass them: an option that its variant does not declare adds no classes, and its result is built on every call instead of being cached.
  • Properties of the selection that are not variants, besides classNames, are ignored, so a component can pass a slot recipe all of its props.
  • Calling a slot recipe without a selection, or with undefined or null, is the same as calling it with an empty one.

A slot recipe lists the names of its variants, in the order of variants, in variantKeys, a frozen array typed with those names:

card.variantKeys; // => ["size", "elevated"]

Use it to split a component’s props into the recipe’s variants and the rest (see Building components).