UNPKG

opencode-agent-kit

Version:

Multi-stack OpenCode agent toolkit — 33+ specialized AI agents, 200+ skills, 46 commands, 8 MCP servers (Nuxt, React, Node.js, Laravel, CI3, Android, Flutter, DevOps, SEO, SonarQube, and more)

287 lines (221 loc) 9 kB
--- title: Styling description: Conditional and composable styling with Tailwind classes. type: guide summary: Using tailwind-merge, clsx, the cn utility, and Class Variance Authority for conditional and composable component styling. prerequisites: - /composition related: - /design-tokens - /data-attributes --- Modern component libraries need flexible styling systems that can handle complex requirements without sacrificing developer experience. The combination of Tailwind CSS with intelligent class merging has emerged as a powerful pattern for building customizable components. This approach solves the fundamental tension between providing sensible defaults and allowing complete customization - a challenge that has plagued component libraries for years. ## The problem with traditional styling Traditional CSS approaches often lead to specificity wars, style conflicts, and unpredictable overrides. When you pass `className="bg-blue-500"` to a component that already has `bg-red-500`, which one wins? Without proper handling, both classes apply and the result depends on a lot of factors - CSS source order, the specificity of the classes, the bundler's class merging algorithm, etc. ## Merging classes intelligently The `tailwind-merge` library solves this by understanding Tailwind's class structure and intelligently resolving conflicts. When two classes target the same CSS property, it keeps only the last one. ```tsx title="Without tailwind-merge" // Both bg-red-500 and bg-blue-500 apply - unpredictable result <Button className="bg-blue-500" /> // Renders: className="bg-red-500 bg-blue-500" ``` ```tsx title="With tailwind-merge" import { twMerge } from "tailwind-merge"; // bg-blue-500 wins as it comes last const className = twMerge("bg-red-500", "bg-blue-500"); // Returns: "bg-blue-500" ``` This works for all Tailwind utilities: ```tsx twMerge("px-4 py-2", "px-8"); // Returns: "py-2 px-8" twMerge("text-sm", "text-lg"); // Returns: "text-lg" twMerge("hover:bg-red-500", "hover:bg-blue-500"); // Returns: "hover:bg-blue-500" ``` The library understands Tailwind's modifier system too: ```tsx // Modifiers are handled correctly twMerge("hover:bg-red-500 focus:bg-red-500", "hover:bg-blue-500"); // Returns: "focus:bg-red-500 hover:bg-blue-500" ``` ## Conditional classes Often you need to apply classes conditionally based on props or state. The `clsx` library provides a clean API for this: ```tsx title="Using clsx" import clsx from "clsx"; // Basic conditionals clsx("base", isActive && "active"); // Returns: "base active" (if isActive is true) // Object syntax clsx("base", { active: isActive, disabled: isDisabled, }); // Arrays clsx(["base", isLarge ? "text-lg" : "text-sm"]); // Mixed clsx( "base", ["array-item"], { "object-conditional": true }, isActive && "conditional", ); ``` A common pattern is to merge a default set of classes with incoming props, as well as any custom logic we have: ```tsx title="component.tsx" const Component = ({ className, ...props }: ComponentProps) => { const [isOpen, setIsOpen] = useState(false); return ( <div className={cn( "rounded-lg border bg-white shadow-sm", isOpen && "bg-blue-500", className, )} {...props} /> ); }; ``` ## The `cn` utility function The `cn` function, popularized by [shadcn/ui](https://ui.shadcn.com/), combines `clsx` and `tailwind-merge` to give you both conditional logic and intelligent merging: ```tsx title="lib/utils.ts" import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } ``` The power comes from the ordering - base styles first, conditionals second, user overrides last. This ensures predictable behavior while maintaining full customization. ## Class Variance Authority (CVA) For complex components with many variants, manually managing conditional classes becomes unwieldy. [Class Variance Authority (CVA)](https://cva.style/docs) provides a declarative API for defining component variants. For example, here's an extract from the [Button](https://ui.shadcn.com/docs/components/button) component from shadcn/ui: ```tsx title="@/components/ui/button.tsx" const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", { variants: { variant: { default: "bg-primary text-primary-foreground hover:bg-primary/90", destructive: "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", link: "text-primary underline-offset-4 hover:underline", }, size: { default: "h-9 px-4 py-2 has-[>svg]:px-3", sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", lg: "h-10 rounded-md px-6 has-[>svg]:px-4", icon: "size-9", }, }, defaultVariants: { variant: "default", size: "default", }, }, ); ``` ## Best practices ### 1. Order matters Always apply classes in this order: 1. Base styles (always applied) 2. Variant styles (based on props) 3. Conditional styles (based on state) 4. User overrides (className prop) ```tsx className={cn( 'base-styles', // 1. Base variant && variantStyles, // 2. Variants isActive && 'active', // 3. Conditionals className // 4. User overrides )} ``` ### 2. Document your variants Use TypeScript and JSDoc to document what each variant does: ```tsx type ButtonProps = { /** * The visual style of the button * @default "primary" */ variant?: "primary" | "secondary" | "destructive" | "ghost"; /** * The size of the button * @default "md" */ size?: "sm" | "md" | "lg"; }; ``` ### 3. Extract repeated patterns If you find yourself writing the same conditional logic repeatedly, extract it: ```tsx title="utils/styles.ts" export const focusRing = 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500'; export const disabled = 'disabled:pointer-events-none disabled:opacity-50'; // Use in components className={cn(focusRing, disabled, className)} ``` ## Migration guide If you're migrating from a different styling approach, here's how to adapt common patterns: ### From CSS Modules ```tsx title="Before - CSS Modules" import styles from "./Button.module.css"; <button className={`${styles.button} ${styles[variant]} ${className}`} />; ``` ```tsx title="After - cn + Tailwind" import { cn } from "@/lib/utils"; <button className={cn( "px-4 py-2 rounded-lg", variant === "primary" && "bg-blue-500 text-white", className, )} />; ``` ### From styled-components ```tsx title="Before - styled-components" const Button = styled.button<{ $primary?: boolean }>` padding: 8px 16px; background: ${(props) => (props.$primary ? "blue" : "gray")}; `; ``` ```tsx title="After - cn + Tailwind" function Button({ primary, className, ...props }) { return ( <button className={cn( "px-4 py-2", primary ? "bg-blue-500" : "bg-gray-500", className, )} {...props} /> ); } ``` ## Performance considerations Both `clsx` and `tailwind-merge` are highly optimized, but keep these tips in mind: 1. **Define variants outside components** - CVA variants should be defined outside the component to avoid recreation on every render. 2. **Memoize complex computations** - If you have expensive conditional logic, consider memoizing: ```tsx const className = useMemo( () => cn(baseStyles, expensiveComputation(props), className), [props, className], ); ``` 3. **Use CSS variables for dynamic values** - Instead of generating classes dynamically, use CSS variables: ```tsx title="Prefer CSS variables" // Good <div className="bg-[var(--color)]" style={{ '--color': dynamicColor } as React.CSSProperties} /> // Avoid <div className={`bg-[${dynamicColor}]`} /> ``` The combination of Tailwind CSS, intelligent class merging, and variant APIs provides a robust foundation for component styling. This approach scales from simple buttons to complex design systems while maintaining predictability and developer experience.