DEV Community

Sherotree
Sherotree

Posted on

Constraining agent CSS to design tokens (without pasting the whole theme)

I used to drop the entire tokens.css into the agent prompt so it “wouldn’t invent colors.” It still invented colors. Just prettier ones — #3B82F6 instead of blue, 14px instead of space.3, a one-off radius that almost matched radius.md.

Pasting the whole theme teaches the model what exists. It does not teach it that everything else is illegal. Agents treat big token dumps like a style moodboard, then freestyle.

Here’s the thinner constraint that actually stuck in our agent UI PRs.

The failure mode: theme dump

A prompt like this looks responsible and fails quietly:

Here is our full design token file (800 lines of CSS variables). Style this settings panel to match.

What came back:

  • Hex that “felt close” to --color-accent-primary
  • padding: 18px because “the mock looked about that”
  • A new --card-shadow-soft declared inline in the component
  • Tailwind arbitrary values (bg-[#0f172a]) next to token classes

The model saw the tokens. It optimized for “looks like our product,” not “uses only the legal surface.”

What “token-constrained CSS” means

I define it as four hard rules the agent can fail against:

  1. No raw values — no hex, rgb, hsl, or bare px/rem for color, space, radius, or shadow in changed CSS/JSX
  2. Import / reference allowlist — only named tokens from the published set (var(--space-4), theme('spacing.4'), tokens.space[4], whatever your stack uses)
  3. No local token invention — do not declare new --* custom properties in feature CSS unless the ticket says “new token approved”
  4. Closest-token rule — if the design asks for something off-scale, stop and name the gap; don’t invent a near-miss

If the prompt only says “use our design system,” the agent hears “make it pretty.” Name the bans.

The slim token pack I actually paste

Instead of the whole theme, I keep a token cheat sheet under ~40–60 lines and refresh it when the public scale changes:

## Legal tokens (CSS / Tailwind / JS — same names)

### Color
- bg.default, bg.subtle, bg.muted
- fg.default, fg.muted, fg.danger
- border.default, border.strong
- accent.primary, accent.primary-hover
Forbidden: raw hex/rgb, inventing --color-*, opacity hacks that bake a new tint

### Space
- 1 2 3 4 5 6 8 10 12 (map to --space-* or spacing.*)
Forbidden: 13px, 18px, 22px, “eyeballed” padding, gap: 7

### Radius
- sm | md | lg | full
Forbidden: border-radius: 10px, rounded-[10px]

### Shadow
- shadow.sm | shadow.md | shadow.lg
Forbidden: one-off box-shadow stacks in feature CSS

### Type
- font.size: sm | md | lg | xl
- font.weight: regular | medium | semibold
Forbidden: font-size: 15px, font-weight: 450

## How to reference (pick your stack)
- CSS: var(--space-4), var(--color-fg-muted)
- Tailwind: p-4, text-fg-muted, rounded-md (token-mapped utilities only)
- JS/CSS-in-JS: tokens.space[4], tokens.color.fg.muted

## Acceptance (must pass before PR)
- [ ] Zero hex/rgb/hsl in the diff
- [ ] Zero bare px for space/radius/type (layout hacks like 1px hairlines need a one-line NOTE)
- [ ] Zero new --* declarations outside tokens/
- [ ] Any off-scale design ask is listed as a blocker, not approximated
Enter fullscreen mode Exit fullscreen mode

That’s the whole “theme” most feature tasks get. The full tokens.css stays in the repo for humans and for codegen — the prompt carries the decision surface.

Prompt shape that works

My default ask looks like this:

Style X using only the Legal tokens below.

Do not invent colors, spacing, radius, or shadows.

If a value isn’t on the scale, stop and name the missing token — don’t approximate.

Prefer existing utility / token classes over new CSS files.

When done, paste the Acceptance checklist with pass/fail and list every token you used.

I also add one explicit ban that cut a lot of noise:

Forbidden: Tailwind arbitrary values for color/space/radius (bg-[#…], p-[18px], rounded-[10px]). If the utility doesn’t exist, the token doesn’t exist for this task.

Arbitrary values are how agents smuggle freestyle CSS past a “we use Tailwind” rule.

A tiny CI gate that catches the same mistakes

Prompts leak. I pair them with a cheap lint on agent-touched paths:

# scripts/check-no-raw-css-values.sh — run on changed UI files
rg -n --pcre2 \
  '(#[0-9a-fA-F]{3,8}\b|rgba?\(|hsla?\(|\b\d+(\.\d+)?px\b|rounded-\[[^\]]+\]|bg-\[[^\]]+\]|p-\[[^\]]+\]|gap-\[[^\]]+\])' \
  src/components src/app \
  -g '!**/tokens/**' \
  -g '!**/*.test.*' \
  && exit 1 || exit 0
Enter fullscreen mode Exit fullscreen mode

False positives happen (SVG path data, 1px hairlines). Allowlist those with a comment:

/* RAW_OK: 1px hairline separator — not a spacing token */
border-top: 1px solid var(--color-border-default);
Enter fullscreen mode Exit fullscreen mode

The point isn’t zero exceptions. It’s that every exception is named, so agent PRs can’t quietly grow a second palette.

For Tailwind shops, eslint-plugin-tailwindcss with no-arbitrary-value (or a custom restrict) on src/features/** works even better — fail the PR when the model reaches for [#0f172a].

What I put in the PR template

A short checkbox beats another paragraph in the system prompt:

### Token compliance
- [ ] No raw color / space / radius / shadow values in the diff
- [ ] No new --* tokens outside tokens/
- [ ] Off-scale design requests called out (not approximated)
- [ ] `scripts/check-no-raw-css-values.sh` (or ESLint) passed
Enter fullscreen mode Exit fullscreen mode

Reviewers stop arguing taste (“that blue is fine”) and start enforcing the scale.

Review comments I actually leave

When I see freestyle CSS in an agent diff:

Token miss: padding: 18px isn’t on the space scale. Use var(--space-4) / p-4 or stop and request a new token. Don’t approximate.

When the model invents a local variable:

New --card-shadow-soft belongs in tokens/ with a design OK, not in the feature file. Revert and use shadow.md or file a token RFC.

When Tailwind arbitrary values show up:

bg-[#0f172a] is a banned escape hatch here. Map to bg-subtle / bg-muted or name the missing token. Arbitrary color values fail the token gate on purpose.

What I stopped doing

  • Pasting the entire theme “for completeness”
  • Saying “match the design system” without listing banned value types
  • Trusting the model to “prefer tokens” without a lint gate
  • Letting one-off hex land because “it’s only in this panel”

Constraining agent CSS to design tokens is less about teaching every variable name and more about shrinking the legal alphabet. Give a short allowlist, ban raw values out loud, and fail CI when freestyle sneaks in. The full theme can stay in the repo — it just doesn’t need to live in every prompt.

Top comments (0)