Design Lint
Rules written as text do not stop an agent from breaking them. standby-design-lint checks the code against the standby.design design rules. Each message names the violation, the fix and the rule, so the agent can correct it right away.
Free and open source (MIT). It runs on your machine and in your own CI.
Set up and update
Run this in the root folder of the project, where package.json is. It needs Node 20.19 or newer.
npx standby-design-lint@latest init
The command installs the package as a dev dependency and sets up these parts:
- a Claude Code hook in
.claude/settings.json - a pre-commit hook, in
.husky/if the project uses husky, otherwise in the git hooks folder - a
preparescript inpackage.json, so everynpm installsets up the pre-commit hook for teammates, too - the CI workflow
.github/workflows/design-lint.yml, if the project has a.githubfolder - the review skill in
.claude/skills/design-review - a short rules block in
AGENTS.mdandCLAUDE.md - the
standbyDesignLintsettings inpackage.json .design-review/in.gitignore
Run the same command again to update. It never adds an entry twice. Hooks from other tools and your own text in CLAUDE.md and AGENTS.md stay as they are. The workflow file and the skill are written anew on each run, so local changes to these two files are lost.
With the standby.design MCP server connected, you can tell the agent “Get the latest standby update” (German: „Hol Dir das Update“). The agent calls the tool update_project, which returns the steps. Then it runs init.
GitHub only accepts the workflow file from a token with the workflow scope. If the push is rejected, run this and push again:
gh auth refresh -s workflow
How strict
| Stage | When | What happens |
|---|---|---|
| Claude Code hook | After each edit | Checks the lines the agent just wrote. The agent gets the violations back and fixes them in the same session. |
| Pre-commit | Before each commit | Checks the staged lines and prints a warning. The commit goes through. |
| Pull request | On each pull request | Checks the lines added since the target branch. New violations fail the check. |
| Visual review | Once per finished screen | Takes screenshots, counts text styles and hands both to a fresh review subagent. |
The hook, the pre-commit hook and the CI check only look at new or changed lines. A project with existing violations does not turn red all at once. When the agent writes a whole file, the hook checks the whole file. To list every violation in a folder, run npx standby-design-lint src.
What it checks
Eight lint rules. Each one enforces a design rule and links to its page. The rule pages are in German. In ESLint the rule ids carry the prefix standby/.
| Rule | Finds | Design rule |
|---|---|---|
no-raw-values | Colors, sizes and fonts set by hand, Tailwind palette colors: bg-[#fff], p-[13px], bg-blue-500 | „Werte kommen aus Tokens, nie aus der Hand“ |
no-inline-style | Fixed values in inline styles: style={{ padding: 16 }}, StyleSheet.create | „Utility-Klassen statt Inline-Styles“ |
no-state-classes-in-views | Hover, focus and active styles outside the component folder: hover:bg-muted | „Verhalten und Aussehen werden nicht in der Ansicht nachgebaut“ |
no-transition-all | Transitions on every property: transition-all, transition: all | „Bewegung hat einen Wert und hält nichts auf“ |
no-opacity-on-semantic-colors | Opacity modifiers on semantic colors: bg-primary/10 | „Keine Deckkraft-Modifier auf semantischen Farben“ |
no-space-utilities | Spacing through margins on the children: space-y-4, last:mb-0 | „Abstand über gap, nicht über Margins am Kind“ |
no-disabled-by-validity | Buttons locked until a form is valid: disabled={!isValid} | „Ein Button ist nie gesperrt“ |
no-click-on-non-interactive | Click handlers on elements that are not controls: <div onClick> | „Name und Zustand stehen im Code“ |
The lint checks .js, .jsx, .ts, .tsx and .mjs files. It finds classes in the attributes className, class and tw, and in the helpers cn(), clsx(), classnames(), cx(), cva(), twMerge(), twJoin() and tv(). The messages are in German.
Exceptions
Some hits are on purpose, for example a brand color fixed by the logo guideline. Put an ESLint disable comment with a reason on the line before:
const logo = cn(
// eslint-disable-next-line standby/no-raw-values -- brand color from the logo guideline
'bg-[#ff2d9c]',
);
Between JSX elements, use the block form {/* eslint-disable-next-line … -- reason */}. The reason after -- tells the next reader why the rule does not apply here. The comment covers the next line only. A comment that suppresses nothing counts as a violation and fails the pull request check.
If the project runs its own ESLint, add recommended() to its config (see Settings). Otherwise that ESLint reports the disable comment as an unknown rule.
Visual review
The lint reads code line by line. Some rules only show in the rendered screen. The review command needs Playwright once:
npm i -D playwright && npx playwright install --only-shell chromium
npx standby-design-lint review http://localhost:5173/settings
It saves full-page screenshots at 1280 and 375 pixels wide and a report.md in .design-review/<screen>/. The report counts the text styles on the screen. A text style is one combination of font, size, weight, color and letter case. The review checks the count against the rule „Jeder Textstil hat eine Rolle“. The rule allows 4 to 7 styles per view. The default is 5. More than 7 is a violation, more than 5 a hint.
The report also flags font sizes outside the type scale of the tokens and styles without a role while other styles have one. It flags two styles one scale step apart that differ in nothing else, and a second font family that carries more than one role.
Before the agent reports a screen as done, it follows the Claude Code skill design-review. The agent starts a fresh subagent that does not know how the screen was built. The subagent gets the screenshots, the report and a checklist and returns at most ten findings. The agent fixes them and runs the review once more. After two rounds it stops and shows you what is still open, with the screenshot.
Text roles
The review finds the role of a text through the class text-role-<role> or the attribute data-text-role. The classes come from the export of the Type tool, as CSS classes, Tailwind v4 @utility rules and design tokens.
| Role | Class | Type level |
|---|---|---|
| Title 1 | text-role-title-1 | h1 |
| Title 2 | text-role-title-2 | h3 |
| Title 3 | text-role-title-3 | h5 |
| Figure | text-role-figure | h2, regular weight, tabular numbers |
| Body | text-role-body | body-m |
| Label | text-role-label | body-s, weight 500 |
| Meta | text-role-meta | caption |
The three title levels sit two steps apart on the type scale.
Settings
init adds this block if package.json has none. A second run keeps your settings. All keys are optional, the ignores entry is an example.
"standbyDesignLint": {
"componentPaths": ["/components/ui/"],
"allowedColors": [],
"ignores": ["src/legacy/**"]
}
componentPathslists the folders of the base components. Hover and focus styles are allowed only there.allowedColorslists Tailwind palette colors you allow, for example"white".ignoreslists glob patterns the lint skips.node_modules,dist,build,.next,coverageand*.d.tsfiles are always skipped.
If the project already has an ESLint config, add the rules there:
import { recommended } from 'standby-design-lint';
export default [recommended({ componentPaths: ['/components/ui/'] })];
Other agents
The hook runs only in Claude Code. Cursor, Codex and other agents read the rules block in AGENTS.md. Their violations show up in the pre-commit warning and fail the pull request check. They can run the review command directly. If the project has no CLAUDE.md, init creates one that imports AGENTS.md.
Privacy
The lint and init run locally and send no code or project data anywhere. The only network access of init is the package download from npm. The review opens the URL you pass in a local browser.
Links
- npm: standby-design-lint
- Source: GitHub
- Design rules: /docs/rules (German, also as Markdown)
- MCP server: /docs/mcp
- For agents: llms.txt
- Issues and questions: GitHub issues