Using Styleframe with AI agents
Overview
Coding agents can write a Styleframe config for you, from the tokens and themes to the utilities and recipes. They work best when every change gets an answer they can read: the CLI tells them what your config registers, checks what they wrote and fails when something is wrong.
Think of a recipe that points at a misspelt token, a utility that never emits a class, or a dark theme that quietly swaps your brand colour. Each of these builds with exit code 0, so an agent that only looks at the exit code never finds out.
This page walks you through setting an agent up to get it right. We'll look at:
- The loop: the commands an agent runs before and after each change, and what each one tells it.
- Common mistakes: the ones that build without a word, and how to avoid each.
The loop
Let's start with the loop. We recommend running these commands in this order, before and after every change an agent makes to your config:
Inspect what's registered
styleframe inspect --json
The inspect command prints every variable, theme, utility, modifier and recipe your config registers, as one JSON document. The agent reads the token names and variant options from it, instead of reading the source or the generated CSS. Pass a section, like styleframe inspect recipes --json, to print only that part.
Check the change
styleframe check --typecheck --json
The check command loads the config, writes nothing and exits with 1 when it finds a problem: a reference to a token that doesn't exist, a theme that overrides a token you never defined, or a token defined twice. With --typecheck, it also runs the TypeScript your project installs on the config, so a default variant that isn't one of the recipe's options shows up in the same call. Each problem comes back as a diagnostic with a code, a path to where it was made and a suggestion when a token name is close.
Build
styleframe build
The build command prints the same problems as warnings, without the TypeScript ones, and exits with 1 when it fails, so a script or an agent can trust its exit code.
Next, turn the warnings into errors. With strict: true, the build fails on any problem that check reports without --typecheck, and writes nothing:
import { styleframe } from "styleframe";
import {
useButtonRecipe,
useDesignTokensPreset,
useModifiersPreset,
useUtilitiesPreset,
} from "@styleframe/theme";
const s = styleframe({ strict: true });
useDesignTokensPreset(s, { colors: { primary: "#4f46e5" } });
useModifiersPreset(s);
useUtilitiesPreset(s);
useButtonRecipe(s);
export default s;
The strict option defaults to false, so existing builds keep working while you fix what the warnings report. See strict for the details.
AGENTS.md or CLAUDE.md. For example: "Before you edit styleframe.config.ts, run styleframe inspect --json. After each edit, run styleframe check --typecheck --json and fix every diagnostic it reports."Common mistakes
Now let's look at the mistakes agents make most often. Some of them build without a warning, and check can't see them yet. Others stop the build with an error that doesn't say how to fix it, and they stop check too: it prints the error on stderr, exits with 1 and prints no JSON on stdout. Either way, it's worth telling an agent about them, or pointing it at this page.
Call every utility creator
The utility() function returns a creator, and only the values you pass to the creator become classes. A creator that's never called produces no CSS, and nothing reports it:
import { styleframe } from "styleframe";
const s = styleframe();
const { variable, ref, utility } = s;
const spacingMd = variable("spacing.md", "1rem");
const createMarginUtility = utility("margin", ({ value }) => ({
margin: value,
}));
// Without this call, there's no _margin class at all.
createMarginUtility({ md: ref(spacingMd) });
export default s;
:root {
--spacing--md: 1rem;
}
._margin\:md {
margin: var(--spacing--md);
}
A recipe that uses a utility generates the classes it needs itself, so this only bites the utilities you use directly in your markup. See Utilities for both ways to call a creator.
Load the design tokens before the presets that use them
The utilities preset references the design-tokens preset's token names, like @z-index.base and @border-style.solid, and so do the theme's recipes, like @color.gray-200. Without those tokens, the utilities preset stops the build at the first missing one:
Error: [styleframe] Variable "z-index.base" is not defined. Check that the variable exists before referencing it with "@z-index.base".
It reports one missing token per build, so working through them one at a time takes several attempts. It's best to call useDesignTokensPreset() first, with your own values on top of its defaults, as in the strict example above. For the same reason, keep its meta.merge at true: with merge: false, the preset drops the defaults it references itself, like color.gray-200, and fails the same way.
Check the dark theme the preset adds
The design-tokens preset adds a dark theme by default, even when you don't pass themes. That theme sets its own primary, secondary and other colours, so with only colors: { primary: "#4f46e5" }, dark mode shows the preset's own primary, #007A99, instead of yours. Run styleframe inspect themes --json to see every theme your config emits.
To keep your brand in dark mode, set the dark colours too. A colors object under themes.dark replaces the preset's whole dark colour set, so start from darkModeColorValues to keep its background, surface and text colours:
import { styleframe } from "styleframe";
import { darkModeColorValues, useDesignTokensPreset } from "@styleframe/theme";
const s = styleframe();
useDesignTokensPreset(s, {
colors: { primary: "#4f46e5" },
themes: {
dark: { colors: { ...darkModeColorValues, primary: "#818cf8" } },
},
});
export default s;
:root {
--color--primary: oklch(0.5105731017445265 0.2300533888866952 276.9656355146575 / 1);
}
.dark-theme, [data-theme="dark"] {
--color--primary: oklch(0.6801126030892546 0.15830254166329877 276.9349018357025 / 1);
--color--background: oklch(0.20859664921828713 0.01279812659921767 264.24609038177596 / 1);
}
Write token references with @
A string value is either a token reference or an arbitrary value, and only the @ at its start tells them apart. Take a margin utility, called with a token and an arbitrary value:
import { styleframe } from "styleframe";
const s = styleframe();
const { variable, utility } = s;
variable("spacing.md", "1rem");
const createMarginUtility = utility("margin", ({ value }) => ({
margin: value,
}));
createMarginUtility(["@spacing.md", "2rem"]);
export default s;
Here's what each form of value gives with this utility:
| Value | Read as | Class | CSS |
|---|---|---|---|
"@spacing.md" | a reference to the spacing.md token | _margin:spacing.md | margin: var(--spacing--md) |
"2rem" | an arbitrary value | _margin:[2rem] | margin: 2rem |
"spacing.md" | an arbitrary value | _margin:[spacing.md] | margin: spacing.md |
"[4px]" | an arbitrary value, wrapped again | _margin:[[4px]] | margin: [4px] |
The last two rows are mistakes that build without a warning, and a browser drops both declarations. Write a token path with its @, and an arbitrary value without brackets, since Styleframe adds them to the class name itself. Recipe declarations read values the same way, so padding: "[4px]" in a recipe gives padding: [4px] too.
In a utility created with an array, like the one above, or in a recipe, a reference with @ to a token that doesn't exist is the one mistake here that check does catch, as unresolved-reference. In a selector, a theme declaration or a utility created with an object, the same reference throws when you define it, so the build stops there.
Register modifiers before a recipe uses them
A recipe can style a pseudo-element with a key like &:after or &::after, or a state with &:hover. Each key needs its modifier registered first, or the build fails:
Error: [styleframe] Modifier "after" not found in registry. Make sure the modifier is registered before using it in a recipe.
The useModifiersPreset() function registers the pseudo-element modifiers along with the others. To register only those, call usePseudoElementModifiers():
import { styleframe } from "styleframe";
import {
useDesignTokensPreset,
useModifiersPreset,
useUtilitiesPreset,
} from "@styleframe/theme";
const s = styleframe();
const { recipe } = s;
useDesignTokensPreset(s);
useModifiersPreset(s);
useUtilitiesPreset(s);
recipe({
name: "card",
base: {
position: "relative",
"&::after": { opacity: "0.5" },
},
});
export default s;
._after\:opacity\:\[0\.5\] {
&::after {
content: '';
opacity: 0.5;
}
}
Colour levels are fixed values
The design-tokens preset turns each colour into levels, shades and tints, like color.primary-50 and color.primary-shade-50. These are fixed oklch() values, computed when the config builds, not oklch(from var(--color--primary) …). So overriding --color--primary on its own, in your CSS or in a theme(), leaves its levels as they were:
import { styleframe } from "styleframe";
import { useDesignTokensPreset } from "@styleframe/theme";
const s = styleframe();
const { theme } = s;
useDesignTokensPreset(s, { colors: { primary: "#4f46e5" } });
// Overrides color.primary only. color.primary-50 and the rest keep their values.
theme("brand", (ctx) => {
ctx.variable("color.primary", "#e11d48");
});
export default s;
:root {
--color--primary: oklch(0.5105731017445265 0.2300533888866952 276.9656355146575 / 1);
--color--primary-50: oklch(0.97 0.0118 276.97 / 1);
}
.brand-theme, [data-theme="brand"] {
--color--primary: #e11d48;
}
To change a colour together with its levels, pass the new value to the preset instead, at the root or under themes, as in the dark theme example. The preset then computes the theme's levels from it.
Register utilities before recipes
A recipe turns each declaration into a utility class, so the utilities it uses must be registered before it. A recipe defined first fails the build:
Error: [styleframe] Utility "display" not found in registry. Make sure the utility is registered before using it in a recipe.
Keep the order of the examples on this page: design tokens, then modifiers and utilities, then recipes.
Best Practices
- Run
checkafter every change: it reports every problem in one pass, so the agent fixes them all at once instead of one per build. - Turn on
strictonce your config is clean: a build that fails on a problem is one an agent can't miss. - Read the registry before writing:
styleframe inspect --jsongives the real token names and variant options, which is better than guessing them from the docs. - Read the output as well as the exit code: for the mistakes above, open the generated
index.cssand look for the classes and values you expect.
FAQ
AGENTS.md.strict. It only decides whether a problem is a warning or an error.