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" },});The config
Section titled “The config”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.
The result
Section titled “The result”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 }); // => trueRequired and default variants
Section titled “Required and default variants”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"Boolean variants
Section titled “Boolean variants”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"Option names that are numbers
Section titled “Option names that are numbers”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"Compound variants
Section titled “Compound variants”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.
Overriding classes
Section titled “Overriding classes”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.
Undeclared options and other props
Section titled “Undeclared options and other props”- 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
undefinedornull, is the same as calling it with an empty one.
variantKeys
Section titled “variantKeys”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).