Atomic Design was made for AI agents
A living design system built on Atomic Design Methodology — where the AI agent works backward from pattern specs, composing primitives and tokens it already knows. The methodology finally makes sense when the maintainer never gets tired.
I’ve always liked Brad Frost’s Atomic Design Methodology. Atoms, molecules, organisms — the idea that you build complex UI from simple, composable pieces. Elegant in theory. Brutal in practice.
The problem was never the methodology. It was the maintenance. A design system is a living thing. Every time someone needs a button variant that doesn’t exist, they have two choices: add it to the design system (20 minutes of work for a 2-minute need) or inline it (2 minutes now, technical debt forever). In every team I’ve led, the inline option wins. The design system slowly becomes a museum of components nobody uses while the real UI lives in ad-hoc page files.
Then I started building with AI coding agents. And something clicked.
The methodology, adapted
I’m building the main website for a major Malaysian telco. It’s a largely static marketing site — the bulk of the work is UI. Dozens of pages, complex navigation, mega menus, promotional sections, content grids. The kind of project that lives or dies on visual consistency.
The codebase has a design system at /design-system with four layers. I renamed Frost’s atoms/molecules/organisms to something that makes more sense for a component library:
foundations ← design tokens (colors, typography, spacing, shadows, motion)
↑
primitives ← single-purpose elements (Link, Icon, StatusDot)
↑
composites ← 2-4 primitives combined (SegmentToggle, IconAction)
↑
patterns ← page-level assemblies (SiteHeader, HeroSection, PricingGrid)
The dependency direction is strict and one-way. A primitive can use tokens. A composite can use primitives and tokens. A pattern can use all three. Nothing imports upward. A primitive must never import a composite. A composite must never import a pattern.
This isn’t a guideline. It’s a rule written in CONVENTIONS.md that the AI agent reads every session. The agent follows it without the discipline fatigue that makes human developers cut corners.
Working backward
Here’s where it gets interesting. The work flows backward from the top.
I get a design spec — say, a full site header with a segment toggle, navigation bar, mega menu, and action icons. I write a SPEC.md file in the pattern’s folder:
# SiteHeader — Spec
## What was asked
Full site header — top bar with Personal/Business toggle + announcement,
main navigation bar with logo, L1 nav items, and action icons,
mega menu dropdown with multi-column links and promo cards.
## Primitives identified
- Link — nav variant for L1 items, menu variant for mega menu links
- Icon — consistent Lucide icon sizing for action icons
- StatusDot — announcement indicator dot
## Composites identified
- SegmentToggle — Personal/Business pill toggle
- IconAction — Icon + optional label for header action buttons
## Dependencies
primitives/Link ──┐
primitives/Icon ──┤
primitives/StatusDot ──┤
├── composites/SegmentToggle ──┐
├── composites/IconAction ─────┤
└──────────────────────────────┴── patterns/SiteHeader
The agent reads this spec and works downward. It checks what already exists. If Link is already a primitive with the right variants, it uses it. If SegmentToggle doesn’t exist yet, it creates it — as a proper composite in its own folder, with a barrel export, following the design system conventions.
The agent doesn’t just build the page. It builds the system that builds the page.
Why this changes the economics
The reason design systems decay is economic. The cost of maintaining them exceeds the perceived benefit for any single task. “I need a toggle. Should I spend 20 minutes making it a reusable composite with props and documentation, or 3 minutes hardcoding it in this page?” For a human, the 3-minute option is rational.
For an AI agent, there’s no difference. It doesn’t experience the 20 minutes as tedious. It doesn’t have a meeting in 15 minutes that makes it want to take the shortcut. It follows the rule: no UI components outside design-system/. If a page needs something new, it builds the design system component first, then uses it.
This inverts the maintenance equation. The design system grows as a natural byproduct of building pages. Every pattern the agent builds leaves behind primitives and composites that the next pattern can reuse. The system compounds instead of decaying.
The foundation layer
The bottom of the stack is src/styles/global.css — a @theme block with every design token as a CSS custom property:
@theme {
/* Primary (70-80% usage) */
--color-unifi-accent-orange: #ff5e00;
--color-unifi-orange: #ff7a00;
/* 11-stop shade ramps per color */
--color-unifi-accent-orange-50: #f9f7f5;
--color-unifi-accent-orange-100: #ebded6;
/* ... through 1000 */
/* Semantic colors */
--color-unifi-error-base: #d82e00;
--color-unifi-error-text: #571300;
/* Interaction states (hover −8%L, active −14%L) */
--color-unifi-accent-orange-hover: #d64f00;
--color-unifi-accent-orange-active: #b84300;
/* Dark mode surfaces */
--color-unifi-dark-accent-orange-bg: #2b1b12;
--color-unifi-dark-accent-orange-surface: #3e2a1e;
}
None of these values are hand-picked. They’re generated by src/lib/color.ts — a set of color math functions that take 8 brand hex values and produce 11-stop shade ramps, semantic mappings, hover/active states, dark mode surfaces, and colored shadows. Algorithmically.
export function generateRamp(hex: string): RampStop[] {
const { h, s } = hexToHSL(hex);
const stops: RampStop[] = [];
for (let i = 0; i < 11; i++) {
const t = i / 10;
const nl = Math.round(97 - (97 - 8) * t);
const ns = Math.min(100, Math.max(0,
Math.round(s * (0.25 + t * 0.85))));
const weight = i === 0 ? 50 : i * 100;
stops.push({ weight, hex: hslToHex(h, ns, nl) });
}
return stops;
}
Add a new brand color? You get 11 shade stops, hover and active states, semantic mappings, and dark mode surfaces automatically. The design system page and global.css are both generated from the same algorithms. They can’t go out of sync because they share the same source.
This is the kind of upfront investment that only makes sense when you know the system will actually be used. With an AI agent following the rules, it will be.
The registry
Every component in the design system is registered in a single TypeScript file:
export const registry: Record<ComponentLayer, ComponentEntry[]> = {
primitives: [
{
name: "Link",
slug: "link",
desc: "Nav pills, menu items, highlighted CTAs",
tag: "3 variants",
usedBy: ["composites/icon-action", "patterns/site-header"],
},
// ...
],
composites: [
{
name: "SegmentToggle",
slug: "segment-toggle",
desc: "Pill-shaped context switcher",
tag: "2 variants",
usedBy: ["patterns/site-header"],
},
// ...
],
patterns: [
{
name: "SiteHeader",
slug: "site-header",
desc: "Full header stack — segment toggle, nav bar, mega menu",
tag: "3 sub-components",
},
],
};
The sidebar navigation, index pages, and cross-reference badges all derive from this registry. When the agent creates a new component, it adds one entry here — everything else updates automatically. The usedBy field creates a dependency graph that documentation pages render as clickable cross-references.
Adding a component is three steps:
- Add it to the registry
- Create the component folder (
primitives/Button/Button.tsx+index.ts) - Create its doc page
That’s it. Sidebar picks it up. Cross-references resolve. The agent can do all three in one pass.
The conventions file
CONVENTIONS.md is the constitution. It sits next to the components and defines every rule the agent must follow:
## Colors
✅ text-unifi-accent-orange
✅ bg-unifi-cobalt-blue-500
❌ text-orange-500 ← Tailwind default, not our palette
❌ bg-[#FF5E00] ← raw hex
## Class Name Assembly
Always use cn() from @/lib/cn to join class names.
Never use template literals, string concatenation, or .trim().
✅ cn("base", isActive && "active", className)
❌ `base ${isActive ? "active" : ""} ${className}`.trim()
Every rule has a ✅/❌ example. Not “prefer token classes” — explicit “use this, not that.” The agent doesn’t interpret intent. It follows patterns. Concrete examples work better than abstract guidelines, for humans and AI alike.
The conventions also define an escape hatch for genuinely exceptional cases:
## Escape Hatch
1. Comment with @ds-override explaining why
2. Keep it minimal — override the smallest surface
3. File an issue to add the missing token/component
{/* @ds-override: third-party embed requires exact 63px height */}
<div className="h-[63px]">
@ds-override is grepable. I can search the codebase and see every deviation from the system. In practice, they’re rare — which is the point.
Living documentation
The design system isn’t a Figma file or a wiki page. It’s a route in the application — /design-system — with its own layout, sidebar, and top navigation. Every foundation token, every primitive, every composite, every pattern has a page with live interactive previews.
The SiteHeader pattern page shows:
- The original spec (what was asked, requirements, dependency tree)
- Usage code block
- Live preview with the mega menu rendered open
- Responsive preview across all breakpoints
- Anatomy breakdown of sub-components
- Built from badges linking to every primitive and composite it uses
- Behaviour documentation (hover timing, keyboard shortcuts)
- Props table
- File tree listing
The documentation pages use shared layout components — PropsTable.astro, UsageCode.astro, FileTree.astro, UsedBy.astro — so creating a doc page for a new component is formulaic. The agent follows the pattern of existing doc pages and produces consistent documentation every time.
The UsedBy component is particularly clever. It reads the registry’s usedBy field and renders clickable badges linking to every composite and pattern that consumes a given primitive. When the agent adds a new pattern that uses the Link primitive, it updates the registry — and the Link doc page automatically shows the new cross-reference.
The design system documents itself as it grows.
The data layer
One pattern I’m proud of: separating data from presentation at every level. Navigation structure doesn’t live inside the header component. It lives in src/data/navigation.ts:
export const navItems: NavItem[] = [
{ label: "Broadband", href: "/all-in-one" },
{ label: "Mobile", href: "/mobile" },
{ label: "Unifi TV", href: "#", hasMegaMenu: true },
// ...
];
export const unifiTvMenu: MegaMenuSubColumn[] = [
{
columns: [
{
title: "Overview",
links: [
{ label: "Explore Unifi TV", href: "#" },
{ label: "Watch Now", href: "#" },
],
promoCards: [
{ title: "TV Packs", href: "#",
bgClass: "from-gray-700 via-gray-800 to-gray-900" },
],
},
// ...
],
},
];
The mega menu component doesn’t know what links it contains. It receives a MegaMenuSubColumn[] and renders it. Change the navigation? Edit one data file. The component, the design system preview, and every page that uses the header all update together.
This matters for AI agents because it makes the boundary between “change the content” and “change the UI” explicit. The agent doesn’t need to read through component internals to update navigation links.
The polymorphic pattern
Small thing, but it surfaces a design decision I’d make differently in a non-AI workflow.
The Link primitive supports an as prop:
// Standard link
<Link variant="nav" href="/page">Nav item</Link>
// Interactive trigger (e.g. mega menu toggle)
<Link as="button" variant="nav" active={isOpen} onClick={toggle}>
Unifi TV
</Link>
Same visual treatment, different semantics. The mega menu trigger looks like a nav link but behaves like a button. Without the as prop, you’d either make the trigger a link with a # href (semantically wrong, accessibility issue) or duplicate the nav styling in a separate button component (redundant, diverges over time).
I wouldn’t have built this upfront in a manual workflow — it’s the kind of thing you add in a refactor pass when you notice the duplication. But when the agent is building the SiteHeader pattern, it identifies the need during the spec analysis and creates the polymorphic API from the start. The agent doesn’t have a “I’ll fix it later” instinct.
Two worlds
I should mention the context. I’m an indie developer on one side — shipping products solo, building the Second Brain and QuoteCraft, experimenting with autopilot and agent protocols. On the other side, I lead a corporate engineering team building production systems for millions of users.
These are completely different worlds. Indie work is fast, experimental, disposable. Corporate work is slow, deliberate, built to last. The indie side taught me that AI agents are shockingly capable builders. The corporate side forced me to think about what happens when the codebase outlives the sprint — when someone (or some agent) needs to modify a component six months from now and the context is gone.
The design system is the bridge. It’s the kind of architecture that corporate teams aspire to but rarely maintain. AI agents make it practical because they don’t experience the maintenance burden that kills design systems in the real world. They follow the conventions every time, not just the first week after the design system is launched.
The real unlock
Here’s what I didn’t expect: the design system changes how I think about delegating to the agent.
Without it, my prompts were implementation instructions. “Build a header with a dark top bar, a segment toggle, centered nav links, a mega menu…” — a wall of visual requirements. I was doing the design and telling the agent to type it out. That’s not leverage.
With the design system in place, my prompts became specs. “Build a SiteHeader pattern. It needs a segment toggle, L1 nav items, and a mega menu. Here’s the nav data structure.” The agent already knows what a segment toggle looks like — it’s a composite. It knows the color tokens, the spacing grid, the typography scale. It knows the interaction states. It knows the z-index layers.
I went from describing pixels to describing intent. The design system is the vocabulary that makes that possible. The agent reads the foundations and conventions once, and every subsequent component inherits the accumulated design decisions. I’m not repeating “use rounded-full, 4px grid, accent-orange for hover states” on every prompt — that knowledge lives in the system.
The setup cost
Let me be honest about the investment. Building the foundation layer — color.ts, global.css, the 12 documentation sections, the layout system, the sidebar, the registry — took real time. Multiple sessions. It’s not something you knock out in an afternoon.
But after that foundation exists, the marginal cost of adding a new component is near zero. The agent creates the component folder, registers it, writes a doc page using the existing helpers, and it’s done. The system absorbs each new component without slowing down.
The breakeven point was the SiteHeader. One pattern that needed three primitives and two composites. By the time the header was built, the design system had five reusable components, each with documentation, each ready for the next pattern. The second pattern I build will start with a library instead of a blank page.
That’s the compounding. The work on the system principle applied to visual design: invest in the system that builds UI, and the UI builds itself.
Atomic Design never failed as a methodology. It failed as a human workflow — too much friction, too much discipline required, too much maintenance for too little perceived benefit on any given day. AI agents remove every one of those objections. They don’t get tired. They don’t skip documentation. They don’t take shortcuts because the meeting starts in five minutes. They follow the conventions on day 300 the same way they did on day 1.
The methodology was always right. It was just waiting for a maintainer that doesn’t get bored.