DESIGN.md vs tailwind.config.js — Why AI Agents Need the Semantic Wrapper
Comparison between DESIGN.md and putting tokens directly in tailwind.config.js. Why LLMs struggle to parse JavaScript configs and how DESIGN.md fixes it.
Quick Verdict
If you use Tailwind and AI coding agents (which is basically everyone in 2026), use both. The tailwind.config.js is the runtime, where the framework reads your tokens. DESIGN.md is the semantic manual, where the agent understands why those tokens exist and how to apply them. Putting everything in tailwind.config and expecting Cursor to understand intent is like handing someone a dictionary and expecting them to write poetry.
Comparison Table
| Dimension | DESIGN.md | tailwind.config.js |
|---|---|---|
| Format | YAML tokens + Markdown rationale | JavaScript/TypeScript object |
| Primary purpose | Semantic documentation for humans and AI | Tailwind runtime configuration |
| LLM consumption | Excellent. Native format for LLMs | Problematic. Nested JS, spread operators, dynamic imports |
| Design decisions | Embedded in the document | Nonexistent. Just raw values |
| Portability | Framework-agnostic | Tailwind only (and derivatives like UnoCSS with adapter) |
| Executable | No. It is documentation | Yes. Tailwind reads it and generates CSS |
| Conflicts | Rare. Small focused file | Common. Merge conflicts in large JS objects |
| Maintenance | Updates when design system changes | Updates when design system changes (duplication) |
The Real Problem: LLMs and JavaScript Config
Here is the elephant in the room that nobody discussed until AI agents went mainstream: LLMs are terrible at parsing complex JavaScript configuration.
A typical tailwind.config.js from a real project looks like this:
import { fontFamily } from 'tailwindcss/defaultTheme'
import colors from './tokens/colors.mjs'
import typography from './tokens/typography.mjs'
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
theme: {
extend: {
colors: {
...colors,
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
surface: {
DEFAULT: 'hsl(var(--surface))',
elevated: 'hsl(var(--surface-elevated))',
}
},
fontFamily: {
display: ['Cal Sans', ...fontFamily.sans],
body: ['Inter', ...fontFamily.sans],
},
spacing: generateSpacingScale(4),
borderRadius: {
...defaultRadius,
card: 'var(--radius-card)',
}
}
},
plugins: [require('@tailwindcss/typography'), require('@tailwindcss/forms')],
}
What the LLM actually sees here:
- Dynamic imports. It cannot know what
colors.mjscontains without reading another file. - Spread operators.
...colorsis a black box. - CSS variables.
hsl(var(--primary))is not a value, it is a reference. - Functions.
generateSpacingScale(4)requires mental execution. - Zero context. Why Cal Sans? Why a 4px ratio? Total silence.
When the agent gets a prompt like “create a pricing card following our design system,” it reads this config and… guesses. It uses colors that look right. Applies spacing that seems close enough. The result compiles, but it does not respect the intent.
DESIGN.md in Detail
The semantic wrapper
DESIGN.md does not replace tailwind.config. It wraps the same tokens with meaning:
colors:
primary:
value: "hsl(222 47% 11%)"
tailwind: "primary"
usage: "CTAs, primary links, section headers"
contrast: "Always pair with text-white or primary-foreground"
surface:
default:
value: "hsl(0 0% 100%)"
tailwind: "surface"
usage: "Default background for cards and containers"
elevated:
value: "hsl(210 20% 98%)"
tailwind: "surface-elevated"
usage: "Cards with elevation, modals, popovers"
typography:
philosophy: |
Escala modular com ratio 1.25 (Major Third).
Display usa Cal Sans para personalidade.
Body usa Inter para legibilidade em telas.
Nunca misturar display em corpo de texto.
scale:
- name: "hero"
class: "text-5xl font-display font-bold"
usage: "Apenas hero sections — um por página"
- name: "section-title"
class: "text-3xl font-display font-semibold"
usage: "Títulos de seção — h2 na hierarquia"
- name: "body"
class: "text-base font-body"
usage: "Texto corrido — parágrafos, descrições"
spacing:
base: "4px"
philosophy: |
Múltiplos de 4px. Componentes internos usam 2-4 (8-16px).
Entre componentes usa 6-8 (24-32px).
Entre seções usa 16-24 (64-96px).
Nunca usar valores arbitrários como p-[13px].
What changes in practice
With this DESIGN.md in context, the same “create a pricing card” prompt produces:
- Correct colors with proper contrast
- Hierarchical typography respecting the scale
- Consistent spacing following the system
- Correct Tailwind classes (it will not invent
bg-slate-50when it should bebg-surface-elevated)
Specific advantages over config alone
- Self-contained. Everything in one file, no imports to resolve.
- Explicit intent. “Never use display in body text” prevents errors before they happen.
- Mapped classes. The agent knows exactly which Tailwind class to apply.
- Usage examples. Concrete scenarios, not just abstract values.
- Documented constraints. What NOT to do matters as much as what to do.
tailwind.config.js in Detail
What it does well
The tailwind.config.js is execution. It is what the framework actually reads to generate CSS. Without it, there is no customized Tailwind. Full stop.
When it is enough on its own
In projects where:
- You do not use AI agents to generate code
- Small team (1-2 devs) who know the config by heart
- Simple design system (few colors, standard typography)
- Nobody new will join the project anytime soon
Real problems without DESIGN.md
Scenario 1: New developer joins the team
“What is the difference between bg-surface and bg-surface-elevated? When do I use each?”
Answer from tailwind.config: silence. Answer from DESIGN.md: “Surface for standard cards, surface-elevated for cards with visual elevation, like modals, popovers, and tooltips.”
Scenario 2: AI agent generating a page
Prompt: “Create a landing page with a hero, features grid, and pricing cards.”
Without DESIGN.md: The agent uses text-4xl because it seems right, mixes font-bold and font-semibold randomly, applies gap-4 and gap-6 without criteria.
With DESIGN.md: Hero uses text-5xl font-display font-bold (one per page). Features uses text-3xl font-display font-semibold. Section spacing is py-24. Automatic consistency.
Scenario 3: Design system evolves
You changed primary from blue to purple. In tailwind.config, the value changes and that is it. But what about contrast rules? What about pairings that used to work? What about combinations that now break?
DESIGN.md documents those relationships. The agent knows that changing primary affects CTAs, links, and headers, and it can adjust everything in a coordinated way.
The Correct Relationship: Layers
┌─────────────────────────────────────┐
│ DESIGN.md │ ← Semantics, intent, constraints
│ (consumed by AI + humans) │
├─────────────────────────────────────┤
│ tailwind.config.js │ ← Runtime, executable values
│ (consumed by Tailwind CLI) │
├─────────────────────────────────────┤
│ Generated CSS │ ← Final output
│ (consumed by the browser) │
└─────────────────────────────────────┘
DESIGN.md does not compete with tailwind.config. It sits above, as the layer that translates tokens into intent. The tailwind.config continues to exist, continues to be what Tailwind reads. But the agent reads DESIGN.md first and then knows which Tailwind classes to use.
When to Use Which
Use tailwind.config alone when:
- Small personal project, no AI agents involved
- You are the only developer and know every token by memory
- Minimal design system (brand colors + default typography)
- Nobody will ever need to be onboarded to this project
Use DESIGN.md + tailwind.config when:
- Any project using AI coding agents. That covers 90% of projects in 2026.
- Team with more than 1 developer
- Design system with more than 10 custom tokens
- Visual consistency across pages and components matters
- Open-source where contributors need to understand the design
Use DESIGN.md without Tailwind when:
- Project uses another CSS approach (vanilla, CSS Modules, styled-components)
- Design tokens need to be framework-agnostic
- Migrating from Tailwind to another solution
FAQ
Does DESIGN.md need to stay manually synced with tailwind.config?
Most of the time, yes, but it is less painful than it sounds. DESIGN.md changes rarely because a design system is stable by definition. When it does change, you update both. Community scripts exist that generate tailwind.config from DESIGN.md, inverting the direction: DESIGN.md becomes the canonical source and the config is derived.
If my tailwind.config is already well-organized, do I really need DESIGN.md?
Here is the test: does your AI agent generate consistent code? If you ask “create a card” and the result uses wrong tokens or mixes styles inconsistently, the config alone is not working as documentation. DESIGN.md fills exactly that gap.
Can I generate DESIGN.md from my existing tailwind.config?
Yes. The designmd.app offers automatic generation of DESIGN.md from existing configs. But the real value comes from adding the rationale, the explanations of why, which no script can extract automatically from the config.
What about Tailwind v4 with CSS-first config?
Tailwind v4 moves tokens into @theme in CSS, which is slightly more readable than the old JS config. But the fundamental problem remains: CSS does not carry usage semantics, constraints, or design philosophy. DESIGN.md remains the necessary semantic layer.
How many tokens justify creating a DESIGN.md?
If your tailwind.config has more than 5 custom colors, non-default typography, or custom spacing, you are already there. In practice, any project that has customized Tailwind beyond defaults benefits from DESIGN.md. The cost to create it is low (30 minutes), and the consistency benefit is permanent.
Conclusion
tailwind.config.js is infrastructure. DESIGN.md is communication.
One tells Tailwind “generate these CSS classes with these values.” The other tells the agent (and the human) “use these classes in these contexts for these reasons.”
The confusion happens when teams treat the config as documentation. It is not. It never was. It is a JavaScript object that configures a build tool. Trying to extract design intent from extend.colors.primary.DEFAULT: 'hsl(var(--primary))' is like trying to understand a recipe by reading only the ingredient list without the instructions.
DESIGN.md is the instructions. And in 2026, with AI agents cooking most of the frontend, instructions matter more than ever.
The 461 design systems on designmd.app include most of the relevant Tailwind projects, and each one demonstrates how the semantic wrapper transforms configuration into comprehension.