Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Build scalable design systems with Tailwind CSS v4, design tokens, component libraries, and responsive patterns. Use when creating component libraries, implementing design systems, or standardizing UI patterns.
.claude/skills/asymmetric-al-tailwind-design-system/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 304% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 321% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 253% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 282% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 305% | 0% |
> Repo notice (Asymmetric-al/core): This repository is Base UI only. > Shared primitives come from @base-ui/react via the shadcn base-maia > style in packages/ui. Ignore any Radix UI guidance below — never add > radix-ui/@radix-ui/* imports or dependencies; composition uses Base > UI's render prop, not asChild. See docs/ai/rules/frontend.md.
Build production-ready design systems with Tailwind CSS v4, including CSS-first configuration, design tokens, component variants, responsive patterns, and accessibility.
> Note: This skill targets Tailwind CSS v4 (2024+). For v3 projects, refer to the upgrade guide.
| v3 Pattern | v4 Pattern | | ------------------------------------- | --------------------------------------------------------------------- | | tailwind.config.ts | @theme in CSS | | @tailwind base/components/utilities | @import "tailwindcss" | | darkMode: "class" | @custom-variant dark (&:where(.dark, .dark *)) | | theme.extend.colors | @theme { --color-*: value } | | require("tailwindcss-animate") | CSS @keyframes in @theme + @starting-style for entry animations |
css/* app.css - Tailwind v4 CSS-first configuration */ @import "tailwindcss"; /* Define your theme with @theme */ @theme { /* Semantic color tokens using OKLCH for better color perception */ --color-background: oklch(100% 0 0); --color-foreground: oklch(14.5% 0.025 264); --color-primary: oklch(14.5% 0.025 264); --color-primary-foreground: oklch(98% 0.01 264); --color-secondary: oklch(96% 0.01 264); --color-secondary-foreground: oklch(14.5% 0.025 264); --color-muted: oklch(96% 0.01 264); --color-muted-foreground: oklch(46% 0.02 264); --color-accent: oklch(96% 0.01 264); --color-accent-foreground: oklch(14.5% 0.025 264); --color-destructive: oklch(53% 0.22 27); --color-destructive-foreground: oklch(98% 0.01 264); --color-border: oklch(91% 0.01 264); --color-ring: oklch(14.5% 0.025 264); --color-card: oklch(100% 0 0); --color-card-foreground: oklch(14.5% 0.025 264); /* Ring offset for focus states */ --color-ring-offset: oklch(100% 0 0); /* Radius tokens */ --radius-sm: 0.25rem; --radius-md: 0.375rem; --radius-lg: 0.5rem; --radius-xl: 0.75rem; /* Animation tokens - keyframes inside @theme are output when referenced by --animate-* variables */ --animate-fade-in: fade-in 0.2s ease-out; --animate-fade-out: fade-out 0.2s ease-in; --animate-slide-in: slide-in 0.3s ease-out; --animate-slide-out: slide-out 0.3s ease-in; @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } @keyframes fade-out { from { opacity: 1; } to { opacity: 0; } } @keyframes slide-in { from { transform: translateY(-0.5rem); opacity: 0; } to { transform: translateY(0); opacity: 1; } } @keyframes slide-out { from { transform: translateY(0); opacity: 1; } to { transform: translateY(-0.5rem); opacity: 0; } } } /* Dark mode variant - use @custom-variant for class-based dark mode */ @custom-variant dark (&:where(.dark, .dark *)); /* Dark mode theme overrides */ .dark { --color-background: oklch(14.5% 0.025 264); --color-foreground: oklch(98% 0.01 264); --color-primary: oklch(98% 0.01 264); --color-primary-foreground: oklch(14.5% 0.025 264); --color-secondary: oklch(22% 0.02 264); --color-secondary-foreground: oklch(98% 0.01 264); --color-muted: oklch(22% 0.02 264); --color-muted-foreground: oklch(65% 0.02 264); --color-accent: oklch(22% 0.02 264); --color-accent-foreground: oklch(98% 0.01 264); --color-destructive: oklch(42% 0.15 27); --color-destructive-foreground: oklch(98% 0.01 264); --color-border: oklch(22% 0.02 264); --color-ring: oklch(83% 0.02 264); --color-card: oklch(14.5% 0.025 264); --color-card-foreground: oklch(98% 0.01 264); --color-ring-offset: oklch(14.5% 0.025 264); } /* Base styles */ @layer base { * { @apply border-border; } body { @apply bg-background text-foreground antialiased; } }
Brand Tokens (abstract)
└── Semantic Tokens (purpose)
└── Component Tokens (specific)
Example:
oklch(45% 0.2 260) → --color-primary → bg-primaryBase styles → Variants → Sizes → States → Overridestypescript// components/ui/button.tsx import { Slot } from '@radix-ui/react-slot' import { cva, type VariantProps } from 'class-variance-authority' import { cn } from '@/lib/utils' const buttonVariants = cva( // Base styles - v4 uses native CSS variables 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', { variants: { variant: { default: 'bg-primary text-primary-foreground hover:bg-primary/90', destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', outline: 'border border-border bg-background hover:bg-accent hover:text-accent-foreground', secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', ghost: 'hover:bg-accent hover:text-accent-foreground', link: 'text-primary underline-offset-4 hover:underline', }, size: { default: 'h-10 px-4 py-2', sm: 'h-9 rounded-md px-3', lg: 'h-11 rounded-md px-8', icon: 'size-10', }, }, defaultVariants: { variant: 'default', size: 'default', }, } ) export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> { asChild?: boolean } // React 19: No forwardRef needed export function Button({ className, variant, size, asChild = false, ref, ...props }: ButtonProps & { ref?: React.Ref<HTMLButtonElement> }) { const Comp = asChild ? Slot : 'button' return ( <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} /> ) } // Usage <Button variant="destructive" size="lg">Delete</Button> <Button variant="outline">Cancel</Button> <Button asChild><Link href="/home">Home</Link></Button>
typescript// components/ui/card.tsx import { cn } from '@/lib/utils' // React 19: ref is a regular prop, no forwardRef export function Card({ className, ref, ...props }: React.HTMLAttributes<HTMLDivElement> & { ref?: React.Ref<HTMLDivElement> }) { return ( <div ref={ref} className={cn( 'rounded-lg border border-border bg-card text-card-foreground shadow-sm', className )} {...props} /> ) } export function CardHeader({ className, ref, ...props }: React.HTMLAttributes<HTMLDivElement> & { ref?: React.Ref<HTMLDivElement> }) { return ( <div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} /> ) } export function CardTitle({ className, ref, ...props }: React.HTMLAttributes<HTMLHeadingElement> & { ref?: React.Ref<HTMLHeadingElement> }) { return ( <h3 ref={ref} className={cn('text-2xl font-semibold leading-none tracking-tight', className)} {...props} /> ) } export function CardDescription({ className, ref, ...props }: React.HTMLAttributes<HTMLParagraphElement> & { ref?: React.Ref<HTMLParagraphElement> }) { return ( <p ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} /> ) } export function CardContent({ className, ref, ...props }: React.HTMLAttributes<HTMLDivElement> & { ref?: React.Ref<HTMLDivElement> }) { return ( <div ref={ref} className={cn('p-6 pt-0', className)} {...props} /> ) } export function CardFooter({ className, ref, ...props }: React.HTMLAttributes<HTMLDivElement> & { ref?: React.Ref<HTMLDivElement> }) { return ( <div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} /> ) } // Usage <Card> <CardHeader> <CardTitle>Account</CardTitle> <CardDescription>Manage your account settings</CardDescription> </CardHeader> <CardContent> <form>...</form> </CardContent> <CardFooter> <Button>Save</Button> </CardFooter> </Card>
typescript// components/ui/input.tsx import { cn } from '@/lib/utils' export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> { error?: string ref?: React.Ref<HTMLInputElement> } export function Input({ className, type, error, ref, ...props }: InputProps) { return ( <div className="relative"> <input type={type} className={cn( 'flex h-10 w-full rounded-md border border-border bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', error && 'border-destructive focus-visible:ring-destructive', className )} ref={ref} aria-invalid={!!error} aria-describedby={error ? `${props.id}-error` : undefined} {...props} /> {error && ( <p id={`${props.id}-error`} className="mt-1 text-sm text-destructive" role="alert" > {error} </p> )} </div> ) } // components/ui/label.tsx import { cva, type VariantProps } from 'class-variance-authority' const labelVariants = cva( 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70' ) export function Label({ className, ref, ...props }: React.LabelHTMLAttributes<HTMLLabelElement> & { ref?: React.Ref<HTMLLabelElement> }) { return ( <label ref={ref} className={cn(labelVariants(), className)} {...props} /> ) } // Usage with React Hook Form + Zod import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { z } from 'zod' const schema = z.object({ email: z.string().email('Invalid email address'), password: z.string().min(8, 'Password must be at least 8 characters'), }) function LoginForm() { const { register, handleSubmit, formState: { errors } } = useForm({ resolver: zodResolver(schema), }) return ( <form onSubmit={handleSubmit(onSubmit)} className="space-y-4"> <div className="space-y-2"> <Label htmlFor="email">Email</Label> <Input id="email" type="email" {...register('email')} error={errors.email?.message} /> </div> <div className="space-y-2"> <Label htmlFor="password">Password</Label> <Input id="password" type="password" {...register('password')} error={errors.password?.message} /> </div> <Button type="submit" className="w-full">Sign In</Button> </form> ) }
typescript// components/ui/grid.tsx import { cn } from '@/lib/utils' import { cva, type VariantProps } from 'class-variance-authority' const gridVariants = cva('grid', { variants: { cols: { 1: 'grid-cols-1', 2: 'grid-cols-1 sm:grid-cols-2', 3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3', 4: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4', 5: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-5', 6: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-6', }, gap: { none: 'gap-0', sm: 'gap-2', md: 'gap-4', lg: 'gap-6', xl: 'gap-8', }, }, defaultVariants: { cols: 3, gap: 'md', }, }) interface GridProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof gridVariants> {} export function Grid({ className, cols, gap, ...props }: GridProps) { return ( <div className={cn(gridVariants({ cols, gap, className }))} {...props} /> ) } // Container component const containerVariants = cva('mx-auto w-full px-4 sm:px-6 lg:px-8', { variants: { size: { sm: 'max-w-screen-sm', md: 'max-w-screen-md', lg: 'max-w-screen-lg', xl: 'max-w-screen-xl', '2xl': 'max-w-screen-2xl', full: 'max-w-full', }, }, defaultVariants: { size: 'xl', }, }) interface ContainerProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof containerVariants> {} export function Container({ className, size, ...props }: ContainerProps) { return ( <div className={cn(containerVariants({ size, className }))} {...props} /> ) } // Usage <Container> <Grid cols={4} gap="lg"> {products.map((product) => ( <ProductCard key={product.id} product={product} /> ))} </Grid> </Container>
css/* In your CSS file - native @starting-style for entry animations */ @theme { --animate-dialog-in: dialog-fade-in 0.2s ease-out; --animate-dialog-out: dialog-fade-out 0.15s ease-in; } @keyframes dialog-fade-in { from { opacity: 0; transform: scale(0.95) translateY(-0.5rem); } to { opacity: 1; transform: scale(1) translateY(0); } } @keyframes dialog-fade-out { from { opacity: 1; transform: scale(1) translateY(0); } to { opacity: 0; transform: scale(0.95) translateY(-0.5rem); } } /* Native popover animations using @starting-style */ [popover] { transition: opacity 0.2s, transform 0.2s, display 0.2s allow-discrete; opacity: 0; transform: scale(0.95); } [popover]:popover-open { opacity: 1; transform: scale(1); } @starting-style { [popover]:popover-open { opacity: 0; transform: scale(0.95); } }
typescript// components/ui/dialog.tsx - Using native popover API import * as DialogPrimitive from '@radix-ui/react-dialog' import { cn } from '@/lib/utils' const DialogPortal = DialogPrimitive.Portal export function DialogOverlay({ className, ref, ...props }: React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> & { ref?: React.Ref<HTMLDivElement> }) { return ( <DialogPrimitive.Overlay ref={ref} className={cn( 'fixed inset-0 z-50 bg-black/80', 'data-[state=open]:animate-fade-in data-[state=closed]:animate-fade-out', className )} {...props} /> ) } export function DialogContent({ className, children, ref, ...props }: React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & { ref?: React.Ref<HTMLDivElement> }) { return ( <DialogPortal> <DialogOverlay /> <DialogPrimitive.Content ref={ref} className={cn( 'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border border-border bg-background p-6 shadow-lg sm:rounded-lg', 'data-[state=open]:animate-dialog-in data-[state=closed]:animate-dialog-out', className )} {...props} > {children} </DialogPrimitive.Content> </DialogPortal> ) }
typescript// providers/ThemeProvider.tsx - Simplified for v4 'use client' import { createContext, useContext, useEffect, useState } from 'react' type Theme = 'dark' | 'light' | 'system' interface ThemeContextType { theme: Theme setTheme: (theme: Theme) => void resolvedTheme: 'dark' | 'light' } const ThemeContext = createContext<ThemeContextType | undefined>(undefined) export function ThemeProvider({ children, defaultTheme = 'system', storageKey = 'theme', }: { children: React.ReactNode defaultTheme?: Theme storageKey?: string }) { const [theme, setTheme] = useState<Theme>(defaultTheme) const [resolvedTheme, setResolvedTheme] = useState<'dark' | 'light'>('light') useEffect(() => { const stored = localStorage.getItem(storageKey) as Theme | null if (stored) setTheme(stored) }, [storageKey]) useEffect(() => { const root = document.documentElement root.classList.remove('light', 'dark') const resolved = theme === 'system' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : theme root.classList.add(resolved) setResolvedTheme(resolved) // Update meta theme-color for mobile browsers const metaThemeColor = document.querySelector('meta[name="theme-color"]') if (metaThemeColor) { metaThemeColor.setAttribute('content', resolved === 'dark' ? '#09090b' : '#ffffff') } }, [theme]) return ( <ThemeContext.Provider value={{ theme, setTheme: (newTheme) => { localStorage.setItem(storageKey, newTheme) setTheme(newTheme) }, resolvedTheme, }}> {children} </ThemeContext.Provider> ) } export const useTheme = () => { const context = useContext(ThemeContext) if (!context) throw new Error('useTheme must be used within ThemeProvider') return context } // components/ThemeToggle.tsx import { Moon, Sun } from 'lucide-react' import { useTheme } from '@/providers/ThemeProvider' export function ThemeToggle() { const { resolvedTheme, setTheme } = useTheme() return ( <Button variant="ghost" size="icon" onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')} > <Sun className="size-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /> <Moon className="absolute size-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /> <span className="sr-only">Toggle theme</span> </Button> ) }
typescript// lib/utils.ts export { cn } from "cnfast"; export type { ClassValue } from "cnfast"; // Focus ring utility export const focusRing = cn( "focus-visible:outline-none focus-visible:ring-2", "focus-visible:ring-ring focus-visible:ring-offset-2", ); // Disabled utility export const disabled = "disabled:pointer-events-none disabled:opacity-50";
@utilityDefine reusable custom utilities:
css/* Custom utility for decorative lines */ @utility line-t { @apply relative before:absolute before:top-0 before:-left-[100vw] before:h-px before:w-[200vw] before:bg-gray-950/5 dark:before:bg-white/10; } /* Custom utility for text gradients */ @utility text-gradient { @apply bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent; }
css/* Use @theme inline when referencing other CSS variables */ @theme inline { --font-sans: var(--font-inter), system-ui; } /* Use @theme static to always generate CSS variables (even when unused) */ @theme static { --color-brand: oklch(65% 0.15 240); } /* Import with theme options */ @import "tailwindcss" theme(static);
css@theme { /* Clear all default colors and define your own */ --color-*: initial; --color-white: #fff; --color-black: #000; --color-primary: oklch(45% 0.2 260); --color-secondary: oklch(65% 0.15 200); /* Clear ALL defaults for a minimal setup */ /* --*: initial; */ }
css@theme { /* Use color-mix() for alpha variants */ --color-primary-50: color-mix(in oklab, var(--color-primary) 5%, transparent); --color-primary-100: color-mix( in oklab, var(--color-primary) 10%, transparent ); --color-primary-200: color-mix( in oklab, var(--color-primary) 20%, transparent ); }
css@theme { --container-xs: 20rem; --container-sm: 24rem; --container-md: 28rem; --container-lg: 32rem; }
tailwind.config.ts with CSS @theme block@tailwind base/components/utilities to @import "tailwindcss"@theme { --color-*: value }darkMode: "class" with @custom-variant dark@keyframes inside @theme blocks (ensures keyframes output with theme)require("tailwindcss-animate") with native CSS animationsh-10 w-10 to size-10 (new utility)forwardRef (React 19 passes ref as prop)@utility directives@theme blocks - CSS-first configuration is v4's core patternbg-primary not bg-blue-500size-* - New shorthand for w-* h-*tailwind.config.ts - Use CSS @theme instead@tailwind directives - Use @import "tailwindcss"forwardRef - React 19 passes ref as prop@theme instead| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-02 | fail→pass | 12,118 | 12,305 | +2% | 1 | 1 | 0% | 2,513 | 10,147 | +304% | 0 | 0 | — |
case-01 | fail→pass | 11,143 | 12,242 | +10% | 1 | 1 | 0% | 2,359 | 9,936 | +321% | 0 | 0 | — |
case-03 | fail→pass | 16,278 | 15,454 | -5% | 1 | 1 | 0% | 3,125 | 11,018 | +253% | 0 | 0 | — |
case-04 | fail→pass | 11,246 | 6,351 | -44% | 1 | 1 | 0% | 2,252 | 8,613 | +282% | 0 | 0 | — |
case-05 | fail→pass | 11,474 | 7,383 | -36% | 1 | 1 | 0% | 2,212 | 8,953 | +305% | 0 | 0 | — |
case-06 | fail→pass | 9,627 | 4,078 | -58% | 1 | 1 | 0% | 1,891 | 8,007 | +323% | 0 | 0 | — |
case-07 | fail→pass | 12,579 | 6,665 | -47% | 1 | 1 | 0% | 2,562 | 8,689 | +239% | 0 | 0 | — |
case-08 | fail→pass | 13,048 | 9,081 | -30% | 1 | 1 | 0% | 2,773 | 9,090 | +228% | 0 | 0 | — |
case-09 | fail→pass | 13,056 | 8,656 | -34% | 1 | 1 | 0% | 2,585 | 8,992 | +248% | 0 | 0 | — |
case-10 | pass→pass | 15,223 | 10,801 | -29% | 1 | 1 | 0% | 3,284 | 10,052 | +206% | 0 | 0 | — |
case-11 | fail→pass | 12,431 | 9,233 | -26% | 1 | 1 | 0% | 2,771 | 9,369 | +238% | 0 | 0 | — |
case-12 | pass→pass | 9,850 | 8,813 | -11% | 1 | 1 | 0% | 2,061 | 9,262 | +349% | 0 | 0 | — |
case-13 | pass→pass | 11,531 | 10,234 | -11% | 1 | 1 | 0% | 2,597 | 9,865 | +280% | 0 | 0 | — |
case-14 | fail→fail | 12,513 | 8,031 | -36% | 1 | 1 | 0% | 2,806 | 8,938 | +219% | 0 | 0 | — |
case-15 | fail→pass | 8,157 | 4,971 | -39% | 1 | 1 | 0% | 1,826 | 8,338 | +357% | 0 | 0 | — |
case-16 | fail→pass | 9,928 | 3,747 | -62% | 1 | 1 | 0% | 1,852 | 7,952 | +329% | 0 | 0 | — |
case-17 | fail→pass | 9,239 | 6,269 | -32% | 1 | 1 | 0% | 1,821 | 8,651 | +375% | 0 | 0 | — |
case-18 | pass→pass | 14,049 | 14,602 | +4% | 1 | 1 | 0% | 3,120 | 10,876 | +249% | 0 | 0 | — |
case-19 | fail→pass | 9,662 | 4,600 | -52% | 1 | 1 | 0% | 1,926 | 8,237 | +328% | 0 | 0 | — |
case-20 | fail→pass | 4,264 | 2,426 | -43% | 1 | 1 | 0% | 745 | 7,756 | +941% | 0 | 0 | — |
case-21 | pass→pass | 3,396 | 3,657 | +8% | 1 | 1 | 0% | 705 | 7,989 | +1033% | 0 | 0 | — |
case-22 | pass→pass | 3,662 | 9,234 | +152% | 1 | 1 | 0% | 829 | 9,212 | +1011% | 0 | 0 | — |
case-23 | pass→pass | 7,478 | 6,267 | -16% | 1 | 1 | 0% | 1,590 | 8,407 | +429% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +65 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.