ui-craft-detect
Zero-dependency static scanner for AI-generated UI anti-patterns. Rules mirror the Anti-Slop Test in the skill. Usable as a CI gate.
Updated 2026-07-03
ui-craft-detect is a standalone CLI. It scans a codebase for the same anti-patterns the skill rejects when generating UI — transition: all, bounce easing, purple/cyan gradients, ALL CAPS headings, emoji-as-icons, generic CTAs, glassmorphism stacks, and 12 more. Zero dependencies. One file. CI-ready.
Install nothing. Run it anywhere:
npx ui-craft-detect ./src
Exit code is 0 when clean, 1 when findings.
Install
# one-off
npx ui-craft-detect ./src
# add as a project dev dep
pnpm add -D ui-craft-detect
Published on npm: ui-craft-detect.
Or from a clone of the main repo:
node scripts/detect.mjs ./src
Rules
33 rules grouped by severity — 31 per-line + 2 file-level. They span AI slop, dark patterns, accessibility, forms, performance, tables, dataviz, state design, and placeholder copy shipped to prod.
Critical
| ID | Detects |
|---|---|
transition-all |
transition: all — animates unknown properties, causes paint thrash |
bounce-elastic-easing |
easeInOutBack, easeOutBounce, elastic cubic-béziers — instant AI-slop tell |
animate-bounce |
Tailwind’s animate-bounce utility |
purple-cyan-gradient |
Purple-to-cyan, violet-to-pink, indigo-to-pink gradients |
uppercase-heading |
ALL CAPS h1/h2/h3 — hard to read, aggressive by default |
left-top-animation |
Animating left / top / width / height — layout thrash, never compositor |
dark-pattern/confirmshaming |
Guilt-tripping opt-out copy (“No thanks, I hate savings”) |
dark-pattern/destructive-no-confirm |
Destructive button (delete/remove) with no confirmation step |
a11y/icon-only-button-no-label |
Icon-only button with no accessible name (aria-label / text) |
a11y/modal-without-dialog |
Custom div modals when native <dialog> or [popover] fits — skips files importing Radix / HeadlessUI / Ariakit / etc. |
a11y/outline-none-no-replacement |
outline: none without a :focus-visible replacement |
a11y/streaming-no-live-region |
Streaming / async-updating content without an aria-live region |
forms/placeholder-as-label |
Inputs with a placeholder but no associated <label> |
copy/placeholder-shipped |
Lorem ipsum, TODO, John Doe and other placeholder copy left in |
glassmorphism-stack (file) |
backdrop-filter + rgba(white, low) + border-white together |
Major
| ID | Detects |
|---|---|
gradient-text-metric |
Gradient fill on large numerals — unreadable, AI tell |
emoji-feature-icon |
Emoji used as a feature/section icon |
pure-black-text |
#000, rgb(0,0,0), oklch(0% ...) body text — never in real UI |
generic-cta |
“Learn more”, “Click here”, “Read more” as CTA labels |
absolute-zindex |
Nuclear z-index values (9999, 99999, 1000000+) |
setTimeout-animation |
setTimeout driving what should be a CSS or RAF animation |
aria-label-emoji |
Emoji inside aria-label — screen readers read them literally |
no-focus-visible |
Hover state defined without :focus-visible counterpart |
pixel-radius-inconsistency |
Token-based border-radius mixed with raw pixel values in one file |
a11y/heading-order-skip |
Heading levels skip (e.g. h1 → h3) |
perf/image-no-dimensions |
<img> without width/height or aspect-ratio → layout shift (CLS) |
forms/autocomplete-missing |
Recognizable input (email, name, etc.) without an autocomplete attribute |
tables/no-overflow-handling |
Tables without horizontal overflow or sticky thead (up to 2 findings per file) |
dataviz/categorical-rainbow |
Chart using an unnamed rainbow palette instead of a categorical scheme |
state/missing-empty-or-error |
Data-fetching component with no empty/error branch |
uniform-border-radius (file) |
Identical radius on every component — no hierarchy |
Warn
| ID | Detects |
|---|---|
inline-any-style |
Long inline style attribute — should be a class or token |
unit-mixing |
Mixed length units (px + rem + em) in the same block |
The two file-level rules (glassmorphism-stack, uniform-border-radius) look at the whole file rather than per line, because the anti-pattern is a composition of properties, not a single declaration.
Ignore comments
Silence a finding at the source. Three forms:
/* ui-craft-detect-ignore-file */
// whole-file skip, put at top
/* ui-craft-detect-ignore-next-line */
transition: all 200ms; // skipped
/* ui-craft-detect-ignore-rule: transition-all */
transition: all 200ms; // only that rule skipped on this line
.uicraftrc.json
Project-level config. Disable rules by id or change severity.
{
"rules": {
"pure-black-text": "off",
"generic-cta": "warn",
"uppercase-heading": "critical"
}
}
Place it at the project root. The CLI walks up from the scanned path to find it.
--fix and --fix-dry-run
Auto-fix the rules that have a safe replacement (for example transition: all → transition-property with the declared properties inferred from the block).
npx ui-craft-detect ./src --fix
npx ui-craft-detect ./src --fix-dry-run # shows the diff, doesn't write
Only rules with a fix_apply method are auto-fixable. Everything else still needs manual judgment.
--sarif
Emit SARIF v2.1.0 for GitHub Code Scanning or any SARIF-compatible viewer.
npx ui-craft-detect ./src --sarif > report.sarif
Upload report.sarif with github/codeql-action/upload-sarif@v3 to surface findings in the Security tab.
Pre-commit hook (Husky)
Install Husky once in the project, then add a hook that scans staged files.
pnpm add -D husky
pnpm exec husky init
Write .husky/pre-commit:
#!/usr/bin/env sh
npx ui-craft-detect $(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.(css|jsx|tsx|vue|svelte|astro)$')
Commits with findings fail. Skip ad-hoc with git commit --no-verify.
The main ui-craft repo ships a richer version at .githooks/pre-commit that also auto-versions marketplace.json. Enable per clone with git config core.hooksPath .githooks.
Diff-scoped scanning
Scope any scan to a git diff instead of the whole tree — useful locally for pre-commit-style gating, and it’s what the GitHub Action uses under the hood on pull requests.
# only findings on lines actually changed vs. the default branch
npx ui-craft-detect ./src --scope changed
# only findings in files touched by the diff (whole-file, not line-level)
npx ui-craft-detect ./src --scope files
# everything (default)
npx ui-craft-detect ./src --scope full
--base <ref> overrides the diff base (defaults to the merge-base with the repo’s default branch). If there’s no git history to diff against (shallow clone, not a git repo), it falls back to full with a warning rather than erroring.
Pair it with --fail-on none|warning|error (default error) to control which severities make the process exit non-zero — --fail-on none is advisory-only, useful for a first rollout on an existing codebase without blocking anyone yet.
npx ui-craft-detect ./src --scope changed --fail-on error
--markdown
Renders findings as a GitHub-flavored report instead of plain text — a small branded header, a rule | file:line | severity | fix table, and severity-grouped <details> sections (errors expanded, warnings collapsed). This is what feeds both the sticky PR comment and the GitHub Actions Job Summary — see below.
npx ui-craft-detect ./src --markdown
CI integration — ci install
One command sets up the whole CI story: a GitHub Action that runs on every pull request, diff-scoped by default (--scope changed), that:
- posts a sticky PR summary comment (rendered via
--markdown, edited in place on subsequent pushes — never duplicated) - posts inline review comments on the exact changed lines that triggered a finding
- publishes a commit status (score + error/warning counts) on pushes to the default branch
- also renders the same markdown report into the GitHub Actions Job Summary for that PR run
npx ui-craft-detect ci install
This writes .github/workflows/ui-craft-detect.yml, committed like any other file in your repo — there’s nothing else to configure to get the default experience.
Change settings anytime without reinstalling:
npx ui-craft-detect ci config --scope full # scan everything, not just the diff
npx ui-craft-detect ci config --fail-on none # advisory-only, never blocks the PR
npx ui-craft-detect ci config --comment false # turn off the sticky PR comment
npx ui-craft-detect ci config --inline-comments false
npx ui-craft-detect ci config --status false
Bump the workflow template to the latest version while keeping your settings:
npx ui-craft-detect ci upgrade
init-hook --github-action (the older, narrower version of this — full-repo scan, no PR comment, no inline comments, no commit status) still works if you’ve already got it installed, but ci install is the current recommended path.
For SARIF + GitHub Security tab integration instead of (or alongside) the above:
- name: Scan with SARIF output
run: npx ui-craft-detect ./src --sarif > ui-craft.sarif
continue-on-error: true
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: ui-craft.sarif
Source
- npm:
ui-craft-detect - Single-file source:
scripts/detect.mjs
Spotted something out of date? Open an issue on GitHub →