aiforui logo

Defining a system

The reason my skills have been downloaded over a million times is because they work well. They work well because they are strict. You can get good design by being strict as well, but you need to give AI a good foundation to build from.

Every visual decision that isn’t written down as code or text doesn’t exist, so an agent has to guess. The chance that the agent will get it exactly right is low, since it has lots of options to choose from. That’s where most “AI can’t do design” frustration comes from.

The thing is that your agent isn’t bad at design, it just has nothing good to pick from.

This lesson is about defining your visual decisions in a way that makes the right choice the easy choice. There are three layers to it: tokens, components, and docs.

Tokens

A high-leverage thing you can do is use semantic tokens. A semantic token is named after what it’s for, not what it looks like.

/* Primitive, the raw value */
--red-500: #ef4444;
 
/* Semantic, the role */
--text-danger: var(--red-500);
--bg-surface: var(--gray-100);
--border-subtle: var(--gray-300);

This matters more for agents than it ever did for humans. LLMs are language models, the names are the design system to them. When an agent builds a destructive button and your codebase offers --text-danger, it’s likely that an agent will use it.

When it offers just #ef4444 or red-500, the answer is not that clear. What is red-500 used for? The agent has to guess, and there is a lot of room for error.

It all comes down to codebase quality at the end of the day, so this doesn’t apply to styles only. As long as the agent has a good foundation to build on, it will be able to make the right choices more easily.

Setting up your color variables this way also gives you dark mode for free. Since the name stays the same, you can simply swap the values in your codebase when you switch to dark mode, so everything gets updated automatically. This (again) reduces the margin for error.

  • --bg-surfacegray-100
  • --accentblue-1000
  • --border-subtlegray-300

The variables don’t change, only the values they point at.

You should treat agents like new engineers on your team. Think about it: in the past when you didn’t have a well-defined design system, or at least a few reusable components, how would someone know what building blocks to use? They wouldn’t, because it’s not exposed anywhere. You had to point them to the right place.

If you set up your foundation in a way that is readable and easy to use for a human, it will be the same for an agent.

Components

The second layer is the component API.

<Button variant="primary" size="small">Save</Button>

The most important thing with components for agents is to have a finite number of choices. Even better, the fewer choices you expose, the less room there is for the agent to improvise, which is the same reason design systems work for large human teams.

Types make this enforceable. If variant is a union of "primary" | "secondary" | "danger", an invented variant would result in a build error which is easy to catch by an agent.

Agents often invent ghost and destructive variants because every codebase they were trained on had them. Figma’s guidelines for AI generation call this out explicitly: tell the model which variants exist, and that it must not use ones that don’t.

Avoid escape hatches where you can. If every component accepts an arbitrary className, there’s a chance for the agent to use it to “fix” spacing and colors. Try to reduce the margin for error as much as possible.

You can even go further and disallow the use of HTML elements and enforce the use of your components exclusively. That’s how Polar describes it in their blog post.

If you want to make sure the base components you create are following the right rules, you can use the /emil-component-design skill on the skills page:

Component Design Skill

Documentation

Good documentation was extremely important for humans. This way each engineer would know exactly when to use which component and what variants were available.

This is why during my time at Vercel, I worked on design system documentation quite a lot. I wanted each component to have a page with all variants that were available, so when you see a component in Figma, you can easily find the correct code to use.

Building out a whole visual documentation system is obviously a lot of work, and for agents it doesn’t matter that much. We can simply use markdown files to document our system so an agent can consume it before building and be aware of the available options.

Here’s what goes in it, per component:

  • When to use it, and when to use the alternative.
  • Which variant applies where. “primary is for the single most important action on a screen. Most buttons are secondary.”
  • A correct and an incorrect example. Models follow examples much better than descriptions.

Be specific. The first example below gives the agent nothing to act on, what does “sparingly” mean? The second one is a rule it can follow, because it’s specific.

Bad

Use small text sparingly.

Good

Body text is 16px, captions are 14px. Never go below 12px.

You could obviously complicate it way further, but again, this course is about a tool-agnostic, timeless approach. A few well-written markdown files next to your code get you most of the way there, and they’re where I’d start.

If you have a button component, create a button.md file describing when to use it and which variant to use.

I’ve packaged this whole section into a /emil-design-system-docs skill. It reads the tokens, components, and variants that exist in your codebase, then writes the docs for them following the rules above, and more:

Design System Docs Skill

Iterate on the system

Here’s how you know whether your system is defined well enough. Ask for a real screen with a one-sentence prompt:

Build a dialog for publishing a site’s pending changes. Use our existing components.

Then look at what went wrong. Every mistake points at a hole in the system:

The instinct is to correct the output, “use the Select component here”. But if you do that, you’ll fix just one screen. You can fix the whole system by fixing whatever the agent got wrong. Then, each time the agent builds a new thing, it’ll get it right.

Putting it all together

To wrap this up, here’s the whole idea in one playground. I built a tiny design system defined exactly the way we just covered, with a small flow built using this system. Step through it to see it work.

  • tokens.css - primitives at the top with semantic tokens below them. Components only ever touch the semantic layer.
  • components.tsx - a handful of components with a strict API. Think a Button component with three variants.
  • DESIGN.md - the rules an agent would read before building here: which token layer to use, when to pick which component.
  • App.tsx - the publish flow from the prompt above, built from the system: review the changes, add a release note, publish.
Code Playground
import { useEffect, useState } from "react";
import { Badge, Button, Dialog, Input, Row, Step, Steps, Switch } from "./components";
import "./tokens.css";
import "./styles.css";

type Change = "modified" | "new" | "deleted";

const CHANGES: { title: string; detail: string; change: Change }[] = [
  { title: "Pricing", detail: "Edited 2 hours ago", change: "modified" },
  { title: "August changelog", detail: "Created 1 hour ago", change: "new" },
  { title: "Legacy plans", detail: "Removed yesterday", change: "deleted" },
];

const CHANGE_BADGE = {
  modified: <Badge tone="neutral">Modified</Badge>,
  new: <Badge tone="neutral">New</Badge>,
  deleted: <Badge tone="danger">Deleted</Badge>,
};

const STEPS = 3;

export default function App() {
  const [step, setStep] = useState(0);
  const [notify, setNotify] = useState(true);
  const [dark, setDark] = useState(false);

  useEffect(() => {
    document.documentElement.dataset.theme = dark ? "dark" : "";
  }, [dark]);

  const last = step === STEPS - 1;

  return (
    <div className="app">
      <Dialog
        label="Publish changes"
        footer={
          <>
            <Button
              variant="secondary"
              disabled={step === 0 || last}
              onClick={() => setStep(step - 1)}
            >
              Back
            </Button>
            <Button variant="primary" onClick={() => setStep(last ? 0 : step + 1)}>
              {step === 0 ? "Continue" : last ? "Start over" : "Publish"}
            </Button>
          </>
        }
      >
        <Steps step={step} count={STEPS}>
          {step === 0 && (
            <Step title="Publish 3 changes" description="Review what goes live on acme.com.">
              <div className="list">
                {CHANGES.map((page) => (
                  <Row key={page.title} title={page.title} description={page.detail}>
                    {CHANGE_BADGE[page.change]}
                  </Row>
                ))}
              </div>
            </Step>
          )}
          {step === 1 && (
            <Step title="Add a release note" description="Subscribers see it in the changelog.">
              <Input id="note" label="Release note" placeholder="What changed?" autoFocus />
              <Row title="Notify subscribers" description="Email the changelog list.">
                <Switch checked={notify} onCheckedChange={setNotify} />
              </Row>
            </Step>
          )}
          {step === 2 && (
            <Step title="Published" description="acme.com is up to date.">
              <div className="list">
                {CHANGES.filter((page) => page.change !== "deleted").map((page) => (
                  <Row key={page.title} title={page.title} description="Published just now">
                    <Badge tone="success">Live</Badge>
                  </Row>
                ))}
              </div>
            </Step>
          )}
        </Steps>
      </Dialog>

      <div className="app-footer">
        <Row title="Dark mode" description="Same tokens, different values.">
          <Switch checked={dark} onCheckedChange={setDark} />
        </Row>
      </div>
    </div>
  );
}

Once you have a well-defined system, you’ll get drastically better results from your agents, since they’ll pick the right building blocks for the job.