Sailop vs ESLint: Why Design Needs Its Own Linter
ESLint will green-light a `bg-blue-500 rounded-lg shadow-md transition-all` card without blinking — valid code, maximum AI slop. Here is the linter built for the layer ESLint can't see.
ESLint will pass this card without a single warning:
<div className="bg-blue-500 rounded-lg shadow-md p-6
transition-all duration-300 ease-in-out">
<h3 className="text-xl font-bold text-white">Feature</h3>
<p className="text-blue-100 mt-2">Description here.</p>
</div>Valid JSX. Clean className strings. Correct component structure. And visually, it is the single most generic block on the web in 2026: blue card (#3b82f6), rounded-lg corners, shadow-md, transition-all, ease-in-out. It is AI slop, and ESLint is constitutionally incapable of seeing it.
That blind spot is the size of an ocean: ESLint has zero opinions about what your code looks like once a browser renders it. That is the gap Sailop fills.
What ESLint Catches
ESLint operates on the abstract syntax tree of your JavaScript or TypeScript. It understands variables, functions, imports, and control flow:
// ESLint catches this:
const x = 5; // unused variable
if (x = 5) {} // assignment instead of comparison
useState(); // hooks called outside a component
// ESLint enforces this:
const name = 'hello'; // prefer const over let
function calc(a: number): number { return a * 2; } // consistent return typesIts rules are about correctness and consistency. They ask: is this code well-structured, does it follow the team's conventions, will it cause bugs? Good questions. None of them touch the rendered output.
What ESLint Cannot Catch
ESLint parses syntax, not pixels. So the card above sails through. Every rule passes. The component is, by ESLint's definition, perfect.
It is also indistinguishable from the default output of Cursor, v0, Bolt, and Lovable. The reason every AI-generated site looks the same is that the patterns making them identical live entirely below ESLint's resolution. There is no rule that fires on "this hue sits in the Tailwind-blue default band" or "this is the eleventh fade-in-up on the page."
What Sailop Catches
Sailop parses the same files but reads different things. Not syntax trees — visual declarations: colors, fonts, spacing, animations, layout patterns, component structures.
sailop check ./src/components/FeatureCard.tsx
# Findings:
# [HIGH] color-blue-range: Primary hue 217 sits in AI-default band (200-290)
# [HIGH] animation-transition-all: transition-all detected, specify exact properties
# [HIGH] animation-ease-in-out: ease-in-out is an AI default easing
# [MEDIUM] component-rounded-uniform: rounded-lg used on every element
# [MEDIUM] component-shadow-md: shadow-md is the #1 AI shadow pattern
# [LOW] spacing-4px-grid: All spacing values are multiples of 4px
#
# Score: 76/100 (Grade D)Six findings in the exact file where ESLint found nothing. Not because ESLint is bad — because the two tools measure different dimensions of quality. Sailop scores against seven dimensions of generic design; ESLint scores against none of them.
The Comparison Table
| Dimension | ESLint | Sailop | |-----------|--------|--------| | Unused variables | Yes | No | | Type safety | Yes (with TS) | No | | Import order | Yes | No | | Color choices | No | Yes (12 rules) | | Typography patterns | No | Yes (11 rules) | | Layout defaults | No | Yes (10 rules) | | Animation quality | No | Yes (10 rules) | | Component uniformity | No | Yes (10 rules) | | Spacing patterns | No | Yes (10 rules) | | HTML structure | Partial | Yes (10 rules) | | Accessibility | Plugin | Partial |
ESLint covers code quality. Sailop covers design quality. The overlap is nearly zero. They are complementary, not competitive.
Why Existing ESLint Plugins Do Not Solve This
"Can't I just write ESLint rules for CSS classes?" Technically, yes — eslint-plugin-tailwindcss exists. But it solves a different problem.
// eslint-plugin-tailwindcss catches:
<div className="bg-blue-500 text-white p-6 bg-red-500" />
// ^ conflicting background classes
// It does NOT catch:
<div className="bg-blue-500 rounded-lg shadow-md p-6
transition-all duration-300 ease-in-out" />
// ^ perfectly valid Tailwind, maximum AI slopThe plugin checks that classes exist, that they don't conflict, that the ordering is canonical. It enforces Tailwind's grammar. It has no idea what the combination of those classes looks like, or whether that combination matches an AI-default fingerprint.
Sailop's analysis is semantic, and it works on combinations. A lone rounded-lg is fine. But rounded-lg + shadow-md + bg-blue-500 + transition-all on one element is a high-confidence pattern match — the same way the three-identical-card grid is a slop tell not because any single card is wrong, but because the repetition is. Our definitive list of 90+ AI design patterns catalogs every combination Sailop scores against.
Running Them Together
The ideal setup runs both tools in CI:
# In your CI/CD script:
npx eslint ./src --max-warnings 0 # code patterns
npx tsc --noEmit # type errors
npx vitest run # behavioral bugs
npx sailop check ./src --max-score 50 # visual homogeneityIf any step fails, the build fails. Four tools, four distinct classes of problem, one gate. For the full pipeline walkthrough, see CI/CD for design: catching AI slop before it ships.
The Pre-Commit Hook Setup
For faster feedback, run both as pre-commit hooks via husky and lint-staged:
npm install -D husky
npx husky init
# .husky/pre-commit → npx lint-staged// package.json
{
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"sailop check --max-score 60"
],
"*.css": [
"sailop check --max-score 60"
]
}
}ESLint runs first with auto-fix. Then Sailop checks the result. If the visual score climbs above 60 — too close to the AI-default band — the commit is blocked before it ever reaches the branch.
Different Configurations for Different Teams
ESLint ships shareable configs (airbnb, standard, google). Sailop ships presets:
sailop check ./src --preset strict # all 73 rules, max score 30
sailop check ./src --preset moderate # 50 highest-impact rules, max score 50
sailop check ./src --preset minimal # high-severity only, max score 70Start with minimal and tighten over time — the same gradual adoption path you used with ESLint. Bumping a team from minimal to strict is exactly how one project went from grade F to grade A.
What Happens When Both Disagree
Rarely, the two pull against each other — ESLint's import-order rule might sort CSS imports differently from what a Sailop layout rule expects. Because they operate on different dimensions, this is rare. When it happens, you override the finding with an inline comment, justification attached:
{/* sailop-disable component-rounded-uniform */}
<div className="rounded-lg shadow-md p-6">
{/* uniform rounding is intentional here: matches the design token */}
</div>
{/* sailop-enable component-rounded-uniform */}Same muscle memory as // eslint-disable-next-line. You acknowledge the finding instead of silently ignoring it.
The Quality Stack
A complete frontend quality stack has five layers, none of them redundant:
TypeScript → catches type errors
ESLint → catches code-quality issues
Prettier → enforces formatting
Vitest → catches behavioral bugs
Sailop → catches design-quality issuesEach is best in class at one job. Drop any one and a whole category of defects walks into production unchallenged. For years the design layer was the missing one — the layer no linter watched.
npx sailop install
sailop check ./src --max-score 50Add it to the stack. ESLint keeps your code clean; Sailop keeps your design from looking like everyone else's. Read the complete guide to anti-AI design for the full workflow, or get started at sailop.com.
SHIP CODE THAT LOOKS INTENTIONAL
Scan your frontend for AI patterns. Generate a unique design system. Stop shipping the same blue gradient as everyone else.