| name | design-system-mechanical-lint |
| description | Use when setting up a design system in a project where agents author UI. Mechanical grep-based CI guardrails ban inline style attributes, raw hex colors, and pixel literals in TSX files — forcing all styling through Tailwind classes that read from CSS-variable tokens. Without this, agents reach for `style={{...}}` and `color: "#FF6600"` whenever convenient and the design system silently rots. |
Mechanical design-system lint (grep-based CI guardrails)
Agents are remarkably good at writing CSS-in-JS shortcuts ("just a little inline style here," "hardcoding this hex because it's the same color as the design token") that bypass the design system. If your project has any UI velocity and any agent authorship, you need mechanical guardrails — a lint:design script that fails CI when banned patterns appear, run pre-commit and pre-push.
The script doesn't have to be sophisticated. Grep gets you 90% of the way.
When to use
Reach for this skill if you're:
- Setting up a new agentic-dev project's UI conventions
- Watching design drift in an existing project (inline styles creeping in, ad-hoc hex colors,
px literals)
- Onboarding an agent and want to prevent the obvious shortcuts
The pattern — pnpm lint:design
A bash script with four checks, each backed by a grep. Run from CI on every PR and pre-commit hook locally.
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
red() { printf '\033[31m%s\033[0m\n' "$*"; }
green() { printf '\033[32m%s\033[0m\n' "$*"; }
ok() { green " ✓ $1"; }
fail() { red " ✗ $1"; FAILED=1; }
FAILED=0
FILES=()
while IFS= read -r f; do
FILES+=("$f")
done < <(
find app components -type f -name '*.tsx' 2>/dev/null \
| grep -v 'components/chart/canvas-bridge.tsx' \
| grep -v 'components/ui/horizontal-bar-list.tsx' \
| grep -v 'components/layout/resizable-panel.tsx' \
| sort
)
if grep -nE 'style=\{\{' "${FILES[@]}" >/dev/null 2>&1; then
fail "inline 'style={{' found — use Tailwind classes:"
grep -nE 'style=\{\{' "${FILES[@]}" | sed 's/^/ /'
else
ok "no inline styles"
fi
if grep -nEi '#[0-9a-f]{3,8}\b' "${FILES[@]}" >/dev/null 2>&1; then
fail "raw hex colors found — add a token to globals.css and use a class:"
grep -nEi '#[0-9a-f]{3,8}\b' "${FILES[@]}" | sed 's/^/ /'
else
ok "no raw hex colors"
fi
if grep -nE '[^a-zA-Z0-9_/-]([0-9]+)px\b' "${FILES[@]}" >/dev/null 2>&1; then
fail "pixel literals found — use Tailwind's spacing scale (gap-4, h-10, etc.):"
else
ok "no pixel literals"
fi
if [[ -f globals.css ]]; then
if grep -nE ':[^;]*(oklch\(|\blab\(|\bcolor\()' globals.css >/dev/null 2>&1; then
fail "wide-gamut color value in globals.css — use hex instead"
else
ok "globals.css uses hex colors only"
fi
fi
if [[ $FAILED -eq 1 ]]; then
red "design lint failed — see CLAUDE.md for the rules"
exit 1
fi
green "design lint passed (${#FILES[@]} files)"
The four checks and why they each matter
-
No style={{...}} in TSX. The first thing an agent reaches for when "Tailwind doesn't have exactly the right class" is inline styles. Once one inline style ships, every future PR will copy the pattern. The whole point of a token system is preventing the "but it's just this one color" drift.
-
No raw hex colors in TSX. Even when not in a style block, agents will pass hex strings to props (color="#FF6600", <Chart bg="#0b0e14">). Force them through the token table by failing CI on any #abc / #aabbcc / #aabbccdd pattern in TSX. Add a new color to globals.css first; THEN use the class form.
-
No px literals in TSX. gap-[16px] and h-[24px] are Tailwind arbitrary values that smell like inline styles. Use the scale (gap-4, h-10). The scale exists for a reason — once you allow arbitrary px values, agents lose the "what's the standard spacing" anchor and the UI fragments visually.
-
globals.css uses hex, NOT wide-gamut. oklch() and lab() look modern but break canvas/WebGL libraries. See lightweight-charts-integration for the painful discovery. Authoring in hex eliminates the boundary problem.
Explicit exceptions
Some files genuinely need to bridge CSS variables to non-CSS-native APIs:
- A canvas-charting adapter (reads CSS variables via
getComputedStyle and passes hex to canvas APIs)
- Data-driven bar widths (a
<div style={{ width: percent + '%' }}> IS the cleanest API)
- A drag-resizable panel whose height is a runtime value
For each of these, list them in the script's grep exclusion. Document the reason next to the exclusion so future maintainers know not to lift it casually.
Wiring into the project
{
"scripts": {
"lint:design": "bash scripts/lint-design.sh"
}
}
- run: pnpm lint:design
And — most importantly — call out the rules in the project's CLAUDE.md so an agent reads them before opening its first TSX file.
Gotchas
- The lint script needs to be FAST. Grep across 100s of TSX files is milliseconds. Don't reach for AST parsers unless grep stops being enough — premature complexity.
- The exclusion list must be tiny. Every exclusion is a place the rules don't apply. Each one should have a one-line justification in the script.
- False positives are death. If the rule fires on a legitimate use, the next agent will just add another exclusion. Tune the regex tight; for
px literals, exclude lines that are comments (the comment may legitimately mention pixel dimensions).
- Don't grep CSS files for hex colors. Hex is the REQUIRED format there. Only TSX is forbidden.
- Run pre-commit too, not just CI. Catching design drift after a PR is open is too late — the agent has already moved on to the next task.
Reference implementation
The pattern was applied in a Next.js + Tailwind + shadcn/ui project. Layout:
- A
scripts/lint-design.sh bash file with the four checks
- A
package.json lint:design npm script wrapper
- A
CLAUDE.md section enumerating "No inline style={{...}}", "No raw hex colors or px literals in TSX" as numbered hard rules
- An
app/globals.css with @theme token block as the single source of color/spacing truth
Related skills
lightweight-charts-integration — the canvas-color-parsing crash that motivated the hex-only rule
outcome-shaped-specs — spec acceptance tests should NOT include "make the code style consistent" — that's the design lint's job