Use when defining or modifying variants on a shadcn ui component, writing a new component that needs `variant` / `size` props, extending the Button with a custom variant, debugging why a `className` override is not winning, or inferring TypeScript props from a `cva()` call. Prevents the common mistakes : concatenating Tailwind class strings with `+ " "` instead of `cn()`, forgetting `tailwind-merge` so duplicate utilities leak through, mis-ordering `compoundVariants` so the override loses to the default, omitting `VariantProps` so consumers cannot extend the component type, and re-implementing variant logic with hand-written `if`-branches that destroy type inference. Covers the full `cva(base, config)` signature (base as string or string array, `variants`, `compoundVariants`, `defaultVariants`), the `VariantProps<typeof X>` inference pattern, the canonical `cn(...inputs)` helper composition of `clsx` plus `twMerge`, the evaluation order (default then variant then compound), and the Radix `Slot` (`asChild`) po
Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.
Quelldateien prüfen
Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.
Mit Codex oder Claude installieren Kopieren Sie diesen Prompt, fügen Sie ihn in Codex, Claude oder einen anderen Assistant ein und lassen Sie die Skill-Seite prüfen und installieren.
Ein direkter Befehl überspringt den Prüf-Prompt. Prüfen Sie die Quelle, bevor Sie ihn ausführen.
Use when defining or modifying variants on a shadcn ui component, writing a new component that needs `variant` / `size` props, extending the Button with a custom variant, debugging why a `className` override is not winning, or inferring TypeScript props from a `cva()` call. Prevents the common mistakes : concatenating Tailwind class strings with `+ " "` instead of `cn()`, forgetting `tailwind-merge` so duplicate utilities leak through, mis-ordering `compoundVariants` so the override loses to the default, omitting `VariantProps` so consumers cannot extend the component type, and re-implementing variant logic with hand-written `if`-branches that destroy type inference. Covers the full `cva(base, config)` signature (base as string or string array, `variants`, `compoundVariants`, `defaultVariants`), the `VariantProps<typeof X>` inference pattern, the canonical `cn(...inputs)` helper composition of `clsx` plus `twMerge`, the evaluation order (default then variant then compound), and the Radix `Slot` (`asChild`) polymorphic-rendering pattern. Code examples are version-explicit for Tailwind v3 versus Tailwind v4. Keywords: cva, class-variance-authority, VariantProps, cn helper, twMerge, tailwind-merge, clsx, compoundVariants, defaultVariants, variants, Slot, asChild, polymorphic component, buttonVariants, variant not applying, className override not working, duplicate tailwind classes, classes not merging, how do I add a variant, what is cva, how does VariantProps work, why is className ignored, why are my Tailwind classes duplicated, type-safe variants, Radix Slot.
license
MIT
compatibility
Designed for Claude Code. Requires shadcn ui evergreen-2026.
metadata
{"author":"OpenAEC-Foundation","version":"1.0"}
shadcn ui : Variant API with cva
shadcn components express their visual variants through class-variance-authority (cva). The single most common source of styling bugs in a shadcn project is misuse of cva or its companion helper cn(). ALWAYS internalise this skill before adding, editing, or reviewing a variants object.
ALWAYS pass the result of buttonVariants({ ... }) through cn(...) together with any caller className. NEVER return the raw cva() output to the DOM.
ALWAYS export VariantProps<typeof buttonVariants> (directly or via a Props type) so consumers can extend the component. NEVER hand-roll the variant union by typing strings.
ALWAYS list variant values as quoted string keys (outline: "..."), never raw booleans or numbers as keys ; cva treats keys as strings under the hood and TypeScript inference relies on it.
Canonical Button pattern (verbatim from shadcn registry, Tailwind v4)
Both forms produce identical output. NEVER concatenate with + " " ; pass the array verbatim.
variants : the variant axis enumeration
Each top-level key is one variant axis (variant, size, tone, ...). Each value is a map from value-name to a class string (or array of strings for clsx-style composition) :
ALWAYS use quoted string keys. NEVER use raw identifiers like true: or 1: without quotes ; cva will accept them, but TypeScript inference via VariantProps then surfaces awkward union members. For boolean-shaped variants, use "true" and "false" as string keys (see references/anti-patterns.md AP-3).
compoundVariants : combination-only overrides
Each entry is an object that lists one OR MORE variant-keys to match, plus a class string (or array) to apply when ALL listed keys match the caller's invocation :
ALWAYS understand the evaluation order : defaultVariants resolve first (filling in missing props), THEN variants classes apply, THEN compoundVariants classes apply LAST. The last-applied class wins via tailwind-merge only because of position in the string ; if a compound entry needs to override a variant entry, ALWAYS rely on cn() (and therefore twMerge) to resolve the conflict. NEVER assume the compound class wins solely because it is declared in compoundVariants ; without twMerge, conflicting utilities BOTH appear in the DOM and CSS specificity decides arbitrarily.
ALWAYS provide a defaultVariants entry for EVERY variant axis. Consumers may omit any prop ; missing defaults produce undefined lookups that yield no classes for that axis. Setting a variant to null in defaultVariants explicitly opts out of any default for that axis (verified at https://cva.style/docs/api-reference, 2026-05-19).
VariantProps inference pattern
VariantProps is a TypeScript helper exported by class-variance-authority (verified at https://cva.style/docs/getting-started/typescript). Use it to derive the variant prop union from the cva return value :
ALWAYS export buttonVariants (the cva return value) AND the component. Without the variants export consumers cannot reuse the variant classes for sibling components (link styled as a button, Next.js <Link> carrying button visuals, etc.). NEVER duplicate the variant-class strings into a second cva() call ; reuse the existing one.
The cn() helper composes clsx and tailwind-merge. The shadcn CLI ships it at @/lib/utils (verified, present in every project initialised with pnpm dlx shadcn@latest init) :
CONFLICT resolution : twMerge("px-2 p-3") returns "p-3", dropping the override-loser
The cn() ALWAYS-rule
ALWAYS pass any composed Tailwind class string through cn() before assigning to className. This applies to :
The output of buttonVariants({ variant, size, className }) (callers can pass className to override).
Any inline conditional class composition (cn("base", active && "bg-primary")).
Any prop-pass-through where the parent forwards className to a child.
NEVER write either of these :
// WRONG : template-string concat. Conflicting utilities both ship to DOM, twMerge bypassed.
<button className={`bg-primary ${className}`}>
// WRONG : plus-string concat. Same problem ; also brittle on undefined.<buttonclassName={"bg-primary " + (active ? "ring-2" : "")}>
ALWAYS write :
<button className={cn("bg-primary", active && "ring-2", className)}>
The conflict-resolution role of twMerge is what makes caller className "win" over base classes. Skip it and your component silently fails to honour parent overrides. See references/anti-patterns.md AP-1 for the full damage report.
Variant evaluation order
When buttonVariants({ variant: "outline", size: "sm", className: "bg-red-500" }) is called, classes accumulate in this order :
Base : the first cva argument (e.g. "inline-flex shrink-0 ...").
defaultVariants : applied for any axis the caller did not specify. (Here the caller specified both variant and size, so defaults are skipped.)
variants[axis][value] : for each axis the caller specified : variant.outline then size.sm.
compoundVariants : every entry whose match-keys ALL equal the caller's props.
className (when included in the call object) : the caller's literal override string.
The accumulator is then passed through clsx (to flatten) and twMerge (to resolve conflicts). The LAST conflicting utility wins ; the order of accumulation determines that last-position.
Why ordering matters in practice
Suppose variant.outline sets border-input and a compound entry { variant: "outline", size: "lg" } sets border-primary. Because the compound runs AFTER the variant, twMerge correctly drops border-input. If you flip the conceptual model and treat compound as "an extra hint" rather than "the final word", you will write compound classes that lose to variants and wonder why nothing changes. ALWAYS treat compound entries as final-position overrides ; NEVER expect them to be merged via specificity.
asChild Slot pattern
Shadcn components accept an asChild?: boolean prop. When true, the component renders its child as the actual DOM element while still applying all of its own classes and props. The mechanism is Radix's Slot.Root (since the Feb 2026 unified radix-ui package ; pre-unification it was @radix-ui/react-slot's Slot) :
ALWAYS pass exactly ONE child to a component with asChild. Radix Slot throws on zero or multiple children.
ALWAYS ensure the child accepts the props the parent forwards (className, onClick, ref). A plain <div> accepts most ; a custom component must spread props.
NEVER nest asChild more than one level without inspecting forwarded refs ; ref-forwarding through multiple Slots requires Slot.Root + Slot.Slottable (see Radix docs).
NEVER use asChild to inject a SECOND interactive element. The combined element must still be a single accessible widget (button OR link, not both).
See references/anti-patterns.md AP-5 for the multi-child failure mode.
Companion Skills
shadcn-core-stack : where cva and tailwind-merge sit in the dependency layer cake
shadcn-syntax-button : button variants and sizes in full, plus asChild worked examples
shadcn-syntax-form : Form components compose cn() for FormControl wiring