Skip to content
UI Craft/ docs

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. h1h3)
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: alltransition-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


Spotted something out of date? Open an issue on GitHub →