install
source · Clone the upstream repo
git clone https://github.com/Intense-Visions/harness-engineering
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/claude-code/css-component-variants" ~/.claude/skills/intense-visions-harness-engineering-css-component-variants && rm -rf "$T"
manifest:
agents/skills/claude-code/css-component-variants/SKILL.mdsource content
Component Variants with CVA
Build type-safe component variants with class-variance-authority for consistent, composable styling APIs
When to Use
- Components with multiple visual variants (size, color, shape)
- Building a design system with enforced variant combinations
- Replacing complex conditional className logic with declarative APIs
- Wanting TypeScript autocompletion and validation for component style props
Instructions
- Install
:class-variance-authority
.npm install class-variance-authority - Define variants with
— base classes first, then variant definitions.cva() - Use
to avoid requiring every prop.defaultVariants - Use
for styles that apply only when specific variant combinations are active.compoundVariants - Extract the variant props type with
for TypeScript.VariantProps<typeof variantFn> - Combine with
(viatailwind-merge
helper) to allow className overrides.cn()
// components/button.tsx import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '@/lib/utils'; const buttonVariants = cva( // Base styles — always applied 'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', { variants: { variant: { primary: 'bg-blue-600 text-white hover:bg-blue-700 focus-visible:ring-blue-500', secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200 focus-visible:ring-gray-500', destructive: 'bg-red-600 text-white hover:bg-red-700 focus-visible:ring-red-500', ghost: 'hover:bg-gray-100 hover:text-gray-900', link: 'text-blue-600 underline-offset-4 hover:underline', }, size: { sm: 'h-8 px-3 text-xs', md: 'h-10 px-4 text-sm', lg: 'h-12 px-6 text-base', icon: 'h-10 w-10', }, }, compoundVariants: [ // Ghost + sm gets tighter padding { variant: 'ghost', size: 'sm', className: 'px-2' }, ], defaultVariants: { variant: 'primary', size: 'md', }, } ); interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> { asChild?: boolean; } function Button({ className, variant, size, ...props }: ButtonProps) { return ( <button className={cn(buttonVariants({ variant, size }), className)} {...props} /> ); } export { Button, buttonVariants };
// Usage <Button>Default Primary Medium</Button> <Button variant="destructive" size="lg">Delete Account</Button> <Button variant="ghost" size="sm">Cancel</Button> <Button className="w-full">Full Width Override</Button>
Details
The cn() helper combines
clsx and tailwind-merge for safe class merging:
// lib/utils.ts import { clsx, type ClassValue } from 'clsx'; import { twMerge } from 'tailwind-merge'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }
Why cva over conditional strings: Without cva:
// Messy, error-prone, no type safety className={`btn ${variant === 'primary' ? 'bg-blue-600 text-white' : ''} ${variant === 'secondary' ? 'bg-gray-100' : ''} ${size === 'lg' ? 'h-12 px-6' : 'h-10 px-4'}`}
compoundVariants: Apply classes only when a specific combination of variants is active. Useful for design exceptions:
compoundVariants: [ { variant: 'primary', size: 'lg', className: 'text-lg font-bold' }, { variant: 'destructive', size: 'sm', className: 'font-bold' }, ],
Extending variants: If a component wraps another, extend its variants:
const alertVariants = cva('rounded-lg p-4 border', { variants: { severity: { info: 'bg-blue-50 border-blue-200 text-blue-800', warning: 'bg-yellow-50 border-yellow-200 text-yellow-800', error: 'bg-red-50 border-red-200 text-red-800', }, }, defaultVariants: { severity: 'info' }, });
shadcn/ui pattern: shadcn/ui uses this exact pattern (cva + cn + VariantProps) for all components. If you are building a component library, this is the industry standard approach.
Source
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.