Loading...
Loading...
Weekly AI insights —
Real strategies, no fluff. Unsubscribe anytime.
Written by Gareth Simono, Founder and CEO of Agentik {OS}. Full-stack developer and AI architect with years of experience shipping production applications across SaaS, mobile, and enterprise platforms. Gareth orchestrates 267 specialized AI agents to deliver production software 10x faster than traditional development teams.
Founder & CEO, Agentik {OS}
47 shades of blue happen when teams lack a system. Tailwind CSS 4 makes consistency the path of least resistance with CSS variables, cascade layers, and CVA.

I inherited a codebase last year with 47 different shades of blue. Not 47 uses of blue. 47 distinct hex values, each slightly different, spread across 200 components. The designer had one blue. The implementation had 47.
This is what happens without a design system. Everyone makes reasonable local decisions that compound into global chaos.
Tailwind CSS 4 makes the right choice the easy choice. Its new architecture with CSS custom properties as the foundation and cascade layers for specificity management changes how design systems work in practice.
Let me show you how to build one properly.
Tailwind CSS 4 is not an incremental update. The architecture changed fundamentally in ways that affect how you structure design systems.
CSS-first configuration: Tailwind 4 moves configuration from tailwind.config.js to a CSS file using @theme. Your design tokens live in CSS as custom properties. This is architecturally important because CSS custom properties cascade naturally with the browser's theming mechanisms.
Oxide engine: The new Rust-based engine processes styles dramatically faster. Build times drop by 5-10x for most projects. Incremental builds are near-instantaneous.
Cascade layers: Tailwind 4 uses CSS @layer to organize its styles, giving you explicit control over specificity. This solves a class of overriding problems that caused headaches in earlier versions.
Automatic content detection: No more content configuration listing every file path. Tailwind 4 scans your project automatically.
The practical impact on design systems: you define your design tokens once in CSS, they cascade to all components naturally, and the theming system (light/dark, brand variants) works with standard CSS custom properties rather than Tailwind-specific mechanisms.
/* tailwind.config.css - Your design tokens live here now */
@import "tailwindcss";
@theme {
--color-primary: oklch(0.9247 0.0524 66.17);
--color-primary-foreground: oklch(0.1776 0 0);
--color-secondary: oklch(0.2 0 0);
--color-muted: oklch(0.3 0 0);
--color-muted-foreground: oklch(0.6 0 0);
--color-border: oklch(0.25 0.01 60);
--color-background: oklch(0.1776 0 0);
--color-foreground: oklch(0.95 0 0);
--radius-sm: 0.375rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
--font-sans: 'Inter', sans-serif;
--font-mono: 'JetBrains Mono', monospace;
--shadow-card: 0 1px 3px oklch(0 0 0 / 0.3);
}Design tokens are the atoms of your design system. Colors, spacing, typography, shadows. They are the primitives that everything else is built from.
A well-structured token system has three layers:
Layer 1: Primitive tokens. Raw values. These do not carry semantic meaning.
@theme {
/* Primitive color scale */
--color-amber-100: oklch(0.97 0.03 80);
--color-amber-200: oklch(0.94 0.05 75);
--color-amber-400: oklch(0.85 0.10 70);
--color-amber-600: oklch(0.70 0.12 65);
--color-amber-900: oklch(0.35 0.08 55);
--color-neutral-50: oklch(0.98 0 0);
--color-neutral-100: oklch(0.95 0 0);
--color-neutral-800: oklch(0.25 0 0);
--color-neutral-950: oklch(0.10 0 0);
}Layer 2: Semantic tokens. Map primitives to purpose. These are what your components use.
@theme {
/* Semantic tokens - map to primitives */
--color-primary: var(--color-amber-600);
--color-primary-hover: var(--color-amber-700);
--color-background: var(--color-neutral-950);
--color-surface: var(--color-neutral-900);
--color-muted-text: var(--color-neutral-400);
--color-border: var(--color-neutral-800);
}Layer 3: Component tokens. Component-specific values that pull from semantic tokens.
/* In your button component or global styles */
:root {
--btn-primary-bg: var(--color-primary);
--btn-primary-text: var(--color-primary-foreground);
--btn-primary-hover: var(--color-primary-hover);
--btn-radius: var(--radius-md);
}This three-layer architecture means you can change the entire color scheme by updating primitive tokens. Or change primary color by updating the semantic layer. Or adjust a specific component without touching anything else.
The three-layer architecture is what prevents the 47-shades-of-blue problem. Every color in your application traces back to one canonical value. Change the canonical value, everything updates.
Every design system needs component variants. Buttons come in primary, secondary, destructive, ghost, and outline flavors. At different sizes. With or without icons.
Managing this with raw Tailwind classes creates two problems: components become unreadable strings of conditionals, and variant logic is duplicated across every use.
CVA solves this elegantly:
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 gap-2 rounded-[--radius-md] text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
primary: "bg-[--color-primary] text-[--color-primary-foreground] hover:bg-[--color-primary-hover]",
secondary: "bg-[--color-surface] text-[--color-foreground] border border-[--color-border] hover:bg-[--color-border]",
ghost: "hover:bg-[--color-surface] text-[--color-foreground]",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-[--color-border] bg-transparent hover:bg-[--color-surface]",
},
size: {
sm: "h-8 px-3 text-xs",
md: "h-9 px-4",
lg: "h-10 px-6 text-base",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "primary",
size: "md",
},
}
);
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
loading?: boolean;
}
export function Button({
className,
variant,
size,
loading,
children,
disabled,
...props
}: ButtonProps) {
return (
<button
className={cn(buttonVariants({ variant, size }), className)}
disabled={disabled || loading}
{...props}
>
{loading && (
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
)}
{children}
</button>
);
}The power of this pattern: every button variant is defined once. The TypeScript types enforce valid variant combinations. Adding a new variant is a single block in the CVA config.
Modern applications need at minimum light and dark themes. Brand-specific themes (for white-labeling or multi-tenant applications) are increasingly common.
Tailwind CSS 4 with CSS custom properties makes multi-theme implementation straightforward:
/* Default theme (dark) */
@theme {
--color-background: oklch(0.1776 0 0);
--color-foreground: oklch(0.95 0 0);
--color-surface: oklch(0.22 0 0);
--color-border: oklch(0.28 0 0);
--color-primary: oklch(0.70 0.12 65);
}
/* Light theme overrides */
.light {
--color-background: oklch(0.98 0 0);
--color-foreground: oklch(0.12 0 0);
--color-surface: oklch(0.95 0 0);
--color-border: oklch(0.88 0 0);
--color-primary: oklch(0.45 0.10 60);
}
/* Brand variant: blue theme */
.theme-blue {
--color-primary: oklch(0.65 0.15 255);
--color-primary-hover: oklch(0.58 0.18 255);
--color-primary-foreground: oklch(0.98 0 0);
}
/* Brand variant: green theme */
.theme-green {
--color-primary: oklch(0.65 0.20 145);
--color-primary-hover: oklch(0.58 0.22 145);
--color-primary-foreground: oklch(0.98 0 0);
}With this setup, switching themes is toggling a class on the root element. All components update automatically because they reference the semantic tokens, not hardcoded values.
// Theme provider implementation
"use client";
import { createContext, useContext, useEffect, useState } from "react";
type Theme = "dark" | "light";
type BrandTheme = "default" | "blue" | "green";
interface ThemeContextValue {
theme: Theme;
brandTheme: BrandTheme;
setTheme: (theme: Theme) => void;
setBrandTheme: (brand: BrandTheme) => void;
}
const ThemeContext = createContext<ThemeContextValue | null>(null);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>("dark");
const [brandTheme, setBrandTheme] = useState<BrandTheme>("default");
useEffect(() => {
const root = document.documentElement;
root.className = [
theme,
brandTheme !== "default" ? `theme-${brandTheme}` : "",
]
.filter(Boolean)
.join(" ");
}, [theme, brandTheme]);
return (
<ThemeContext.Provider value={{ theme, brandTheme, setTheme, setBrandTheme }}>
{children}
</ThemeContext.Provider>
);
}
export const useTheme = () => {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be used within ThemeProvider");
return ctx;
};Typography is where design systems most often fall apart. Too many scales, inconsistent line heights, arbitrary font sizes that do not relate to each other.
A disciplined typography system:
@theme {
/* Type scale - modular, based on 1.25 ratio */
--text-xs: 0.75rem; /* 12px */
--text-sm: 0.875rem; /* 14px */
--text-base: 1rem; /* 16px */
--text-lg: 1.125rem; /* 18px */
--text-xl: 1.25rem; /* 20px */
--text-2xl: 1.5rem; /* 24px */
--text-3xl: 1.875rem; /* 30px */
--text-4xl: 2.25rem; /* 36px */
--text-5xl: 3rem; /* 48px */
/* Line heights that actually work for each size */
--leading-tight: 1.25;
--leading-snug: 1.375;
--leading-normal: 1.5;
--leading-relaxed: 1.625;
/* Font weights with semantic names */
--font-normal: 400;
--font-medium: 500;
--font-semibold: 600;
--font-bold: 700;
}Implement as a Typography component that enforces the scale:
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const typographyVariants = cva("", {
variants: {
variant: {
h1: "text-4xl font-bold leading-tight tracking-tight",
h2: "text-3xl font-semibold leading-tight",
h3: "text-2xl font-semibold leading-snug",
h4: "text-xl font-semibold leading-snug",
h5: "text-lg font-medium",
h6: "text-base font-medium",
body: "text-base leading-normal",
"body-sm": "text-sm leading-normal",
caption: "text-xs leading-normal text-[--color-muted-text]",
label: "text-sm font-medium leading-none",
code: "font-mono text-sm",
},
},
defaultVariants: { variant: "body" },
});
type TypographyElement = "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p" | "span" | "div" | "code";
interface TypographyProps
extends React.HTMLAttributes<HTMLElement>,
VariantProps<typeof typographyVariants> {
as?: TypographyElement;
}
export function Typography({
variant = "body",
as,
className,
...props
}: TypographyProps) {
const variantToElement: Record<string, TypographyElement> = {
h1: "h1", h2: "h2", h3: "h3", h4: "h4", h5: "h5", h6: "h6",
body: "p", "body-sm": "p", caption: "span", label: "span", code: "code",
};
const Tag = as || (variantToElement[variant!] as TypographyElement) || "p";
return <Tag className={cn(typographyVariants({ variant }), className)} {...props} />;
}A design system without documentation gets ignored. With Tailwind CSS 4 and CVA, you can auto-generate interactive documentation from the variant definitions themselves.
// Using Storybook or a custom doc page
import { buttonVariants } from "@/components/ui/button";
import { Button } from "@/components/ui/button";
// Extract variants programmatically from CVA definition
// to generate interactive docs
export default function ButtonDocs() {
const variants = ["primary", "secondary", "ghost", "destructive", "outline"] as const;
const sizes = ["sm", "md", "lg"] as const;
return (
<div className="space-y-8 p-8">
{variants.map((variant) => (
<div key={variant} className="space-y-3">
<Typography variant="h4">{variant}</Typography>
<div className="flex gap-3 items-center">
{sizes.map((size) => (
<Button key={size} variant={variant} size={size}>
Button {size}
</Button>
))}
</div>
</div>
))}
</div>
);
}Inconsistent spacing creates visual noise even when the design is otherwise strong. A 4px base grid eliminates this.
@theme {
/* 4px base grid spacing */
--spacing-1: 0.25rem; /* 4px */
--spacing-2: 0.5rem; /* 8px */
--spacing-3: 0.75rem; /* 12px */
--spacing-4: 1rem; /* 16px */
--spacing-5: 1.25rem; /* 20px */
--spacing-6: 1.5rem; /* 24px */
--spacing-8: 2rem; /* 32px */
--spacing-10: 2.5rem; /* 40px */
--spacing-12: 3rem; /* 48px */
--spacing-16: 4rem; /* 64px */
--spacing-20: 5rem; /* 80px */
--spacing-24: 6rem; /* 96px */
}Tailwind's default spacing scale is already close to this. The value of defining it explicitly in your @theme block is that it becomes a named, documented decision rather than an implicit assumption.
Motion in design systems is often an afterthought. Define it upfront:
@theme {
/* Duration */
--duration-instant: 75ms;
--duration-fast: 150ms;
--duration-normal: 250ms;
--duration-slow: 350ms;
/* Easing */
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
}With Framer Motion:
// Shared animation presets
export const motionPresets = {
fadeIn: {
initial: { opacity: 0 },
animate: { opacity: 1 },
transition: { duration: 0.25, ease: [0, 0, 0.2, 1] },
},
slideUp: {
initial: { opacity: 0, y: 8 },
animate: { opacity: 1, y: 0 },
transition: { duration: 0.25, ease: [0, 0, 0.2, 1] },
},
scaleIn: {
initial: { opacity: 0, scale: 0.95 },
animate: { opacity: 1, scale: 1 },
transition: { duration: 0.15, ease: [0.34, 1.56, 0.64, 1] },
},
} as const;
// Usage in component
import { motion } from "framer-motion";
import { motionPresets } from "@/lib/motion";
export function Card({ children }: { children: React.ReactNode }) {
return (
<motion.div
{...motionPresets.fadeIn}
className="rounded-[--radius-lg] border border-[--color-border] bg-[--color-surface] p-6"
>
{children}
</motion.div>
);
}After building the system, maintaining it requires periodic audits. The goal: ensure every color, spacing value, and font size in the codebase traces back to a design token.
I run this audit with a simple script:
// scripts/audit-design-tokens.ts
import { readFileSync } from "fs";
import { glob } from "glob";
// Patterns that indicate hardcoded values that should be tokens
const RED_FLAGS = [
/\btextwhite\b/,
/\btext-\[#[0-9a-f]{3,6}\]/gi, // Arbitrary hex text colors
/\bbg-\[#[0-9a-f]{3,6}\]/gi, // Arbitrary hex backgrounds
/\bborder-\[#[0-9a-f]{3,6}\]/gi, // Arbitrary hex borders
/\btext-\[rgb/gi,
/\bbg-\[rgb/gi,
];
async function auditTokenUsage() {
const files = await glob("src/**/*.{tsx,jsx}");
const violations: Array<{ file: string; line: number; match: string }> = [];
for (const file of files) {
const content = readFileSync(file, "utf8");
const lines = content.split("\n");
lines.forEach((line, i) => {
RED_FLAGS.forEach((pattern) => {
if (pattern.test(line)) {
violations.push({ file, line: i + 1, match: line.trim() });
}
});
});
}
if (violations.length > 0) {
console.log(`Found ${violations.length} design token violations:`);
violations.forEach(({ file, line, match }) => {
console.log(` ${file}:${line} - ${match}`);
});
process.exit(1);
} else {
console.log("No design token violations found.");
}
}
auditTokenUsage();Run this in CI. When a developer uses bg-[#e0b860] instead of bg-primary, the build fails with a clear explanation of what to use instead.
The design system is only as good as its enforcement. Without automated checks, manual consistency degrades over time until you have 47 shades of blue again.
Q: What is new in Tailwind CSS 4?
Tailwind CSS 4 introduces a Rust-based engine (up to 10x faster builds), CSS-first configuration replacing tailwind.config.js, native CSS cascade layers, improved dark mode with automatic variant generation, and better integration with modern CSS features like oklch colors and container queries.
Q: How do you build a design system with Tailwind CSS 4?
Build a Tailwind 4 design system by defining design tokens as CSS custom properties, creating a component library with consistent utility patterns, using CSS layers for proper specificity control, implementing light/dark themes through oklch color tokens, and documenting patterns with examples. AI agents can generate consistent component variants from design tokens.
Q: Is Tailwind CSS 4 good for AI-assisted development?
Tailwind CSS 4 is excellent for AI-assisted development because utility classes are predictable and well-documented, design tokens provide clear constraints, and the class-based approach produces consistent output across an entire codebase. AI agents generate Tailwind code more reliably than custom CSS because patterns are standardized.
Full-stack developer and AI architect with years of experience shipping production applications across SaaS, mobile, and enterprise. Gareth built Agentik {OS} to prove that one person with the right AI system can outperform an entire traditional development team. He has personally architected and shipped 7+ production applications using AI-first workflows.

React 19 Patterns for AI Apps: What Actually Works
Server actions, streaming with use(), optimistic updates, and error boundaries. React 19 was built for exactly the problems AI interfaces create.

Next.js 16 + AI: Build Intelligent Apps Fast
Next.js 16 solves AI's hardest problems: secret exposure, blocking UIs, and scaling costs. Here's the architecture that actually works in production.

Build a Design System with AI: From Zero to Production
Design systems take months to build right. With AI and shadcn/ui as a foundation, you can ship a consistent, documented system in days. Here's how.
Stop reading about AI and start building with it. Book a free discovery call and see how AI agents can accelerate your business.