web-styling-cva-b658d9 — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited web-styling-cva-b658d9 (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Quick Guide:cva()turns a component's visual states into one typed configuration object: base classes, variant groups (size, intent, state),compoundVariantsfor combinations that need their own styles, anddefaultVariantsfor what a caller omits.VariantProps<typeof variants>derives the prop types from that object. It emits class-name strings and settles nothing about what those class names mean, so it works with utility classes, CSS modules or plain CSS alike.
Detailed Resources:
VariantProps, class names from a scoped stylesheet, call-site appending and merging, multi-part components, extending variants<critical_requirements>
Define every variant option inside the `variants` object. Conditional class logic outside cva() splits the definition in two, and the derived types stop describing the component.
Extract prop types with `VariantProps<typeof variants>`. A hand-written variant type drifts the moment a variant is added or renamed; a derived one cannot.
Set `defaultVariants` for every variant a caller may omit. Calling with no arguments otherwise returns the base classes alone — no intent, no size.
Reach for `compoundVariants` when two variants together need a third style. It states "when X and Y, also Z" once, in the definition, rather than at each call site.
</critical_requirements>
Auto-detection: cva, class-variance-authority, VariantProps, compoundVariants, defaultVariants, cx
Applies to:
Handled elsewhere:
cva() emits strings, and the CSS behind them is settled by whatever owns itcx() concatenates and filters falsy values; deciding which of px-2 and px-8 wins is a merge utility's job<philosophy>
CVA treats variants as a type system for UI states. The variant table is the single source of truth: the class strings and the prop types both come from it, so adding an option changes one object and TypeScript propagates the rest. What the definition describes is what each state _looks like_, never how to compute a class list — which is why the same definition ports across UI frameworks and CSS approaches unchanged.
</philosophy>
<patterns>
Base classes plus one entry per variant group. Arrays read better than space-separated strings once a group has more than two classes.
import { cva } from "class-variance-authority";
const buttonVariants = cva(["font-semibold", "border", "rounded"], {
variants: {
intent: {
primary: ["bg-blue-600", "text-white"],
secondary: ["bg-white", "text-gray-800"],
},
size: {
sm: ["text-sm", "py-1", "px-2"],
md: ["text-base", "py-2", "px-4"],
},
},
defaultVariants: { intent: "primary", size: "md" },
});
buttonVariants(); // primary + md
buttonVariants({ intent: "secondary" }); // secondary + mdFull code: examples/core.md
true/false keys model a binary state. Define both sides — the false branch is where the normal-state styles live.
const inputVariants = cva(["border", "rounded", "px-3", "py-2"], {
variants: {
disabled: {
false: ["bg-white", "cursor-text"],
true: ["bg-gray-100", "cursor-not-allowed"],
},
error: { false: ["border-gray-300"], true: ["border-red-500"] },
},
defaultVariants: { disabled: false, error: false },
});Full code: examples/core.md
A compoundVariants entry fires when every key on it matches. An array value matches any of its members, which collapses near-identical rules.
const buttonVariants = cva(["font-semibold", "rounded"], {
variants: {
intent: { primary: ["bg-blue-600"], secondary: ["bg-white"] },
disabled: { false: null, true: ["opacity-50", "cursor-not-allowed"] },
},
compoundVariants: [
// Hover only where the button is interactive
{ intent: "primary", disabled: false, class: ["hover:bg-blue-700"] },
{ intent: "secondary", disabled: false, class: ["hover:bg-gray-100"] },
{
intent: ["primary", "secondary"],
disabled: true,
class: ["pointer-events-none"],
},
],
defaultVariants: { intent: "primary", disabled: false },
});Full code: examples/compound-variants.md
VariantProps reads the definition and produces one optional, nullable prop per variant group.
import { cva, type VariantProps } from "class-variance-authority";
const cardVariants = cva(["rounded-lg", "border"], {
variants: {
elevation: { flat: ["shadow-none"], raised: ["shadow-md"] },
padding: { none: ["p-0"], sm: ["p-2"], md: ["p-4"] },
},
defaultVariants: { elevation: "flat", padding: "md" },
});
type CardVariants = VariantProps<typeof cardVariants>;
// { elevation?: "flat" | "raised" | null; padding?: "none" | "sm" | "md" | null }
// Make one variant mandatory: leave it out of defaultVariants, then narrow the type
type CardProps = Omit<CardVariants, "padding"> &
Required<Pick<CardVariants, "padding">>;Full code: examples/composition.md
cx() ships with the package (a re-export of clsx): it concatenates and drops falsy values. Conflict resolution is a separate utility's job.
import { cva, cx } from "class-variance-authority";
cx(buttonVariants({ intent: "primary" }), highlighted && "ring-2", className);
// Where a caller's className must be able to override a variant's own padding,
// wrap the call in a merging utility instead of concatenating
function button(variants: ButtonVariants, className?: string): string {
return cn(buttonVariants(variants), className);
}Full code: examples/composition.md
One cva() per styled part, sharing variant names so the parts move together.
const formFieldVariants = {
label: cva(["block", "font-medium"], {
variants: { size: { sm: ["text-sm"], md: ["text-base"] } },
defaultVariants: { size: "md" },
}),
input: cva(["w-full", "border", "rounded"], {
variants: {
size: { sm: ["text-sm", "px-2"], md: ["text-base", "px-3"] },
error: { false: ["border-gray-300"], true: ["border-red-500"] },
},
defaultVariants: { size: "md", error: false },
}),
helper: cva(["mt-1"], {
variants: {
size: { sm: ["text-xs"], md: ["text-sm"] },
error: { false: ["text-gray-500"], true: ["text-red-600"] },
},
defaultVariants: { size: "md", error: false },
}),
};Full code: examples/composition.md
Two definitions combined with cx() — a shared interactive base plus the component's own variants. Their VariantProps intersect into one prop type.
const interactiveVariants = cva(["transition-colors", "focus:ring-2"], {
variants: { focusRing: { blue: ["focus:ring-blue-500"] } },
defaultVariants: { focusRing: "blue" },
});
type ButtonProps = VariantProps<typeof interactiveVariants> &
VariantProps<typeof buttonVariants>;
function button({ focusRing, intent }: ButtonProps): string {
return cx(interactiveVariants({ focusRing }), buttonVariants({ intent }));
}Full code: examples/composition.md
</patterns>
<red_flags>
Produces the wrong classes:
buttonVariants() returns the base classes and nothing else, so a component rendered without props loses its intent and size. Give every optional variant a default.false branch with the base styles, or null where it genuinely needs none.VariantProps<typeof variants> cannot drift.compoundVariants enumerates the combinations instead.Surprising behaviour:
VariantProps makes every variant optional _and_ nullable. Required<Pick<T, "color">> is how one becomes mandatory.compoundVariants classes land after the regular variant classes in the returned string. String order is not CSS precedence, so two conflicting class names still need a merge utility.null is a valid variant value meaning "this option adds nothing" — distinct from omitting the key, which leaves the option undefined.class and className both work inside a compoundVariants entry. Pick one per project; mixing them makes the entries look like different shapes.</red_flags>
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.