-
Notifications
You must be signed in to change notification settings - Fork 7
Anti Pattern Catalog 100
This is the full indexed catalog of the 100 anti-pattern rules that ship in data/anti-patterns.json. Every rule has a stable id, a severity, a category, a one-line "why," and a one-line "fix." The linter walks this catalog on every ux lint run.
For how the linter works, severity ramps, and CI integration, see Linter Rules. For the engine that loads the catalog into every recommendation as active guardrails, see Recommender Engine.
The catalog is grouped by category in descending count order. Each section header carries the running total — ## A11y (23 rules — 1–23 of 100) means rules 1 through 23 are in the A11y bucket. The categories add to 100.
For every rule:
- # — its position in the catalog (1–100).
-
ID — kebab-case, stable across versions. Quote it in CI annotations, in
--ruleflags, in PR comments. -
Severity — one of
critical(4),high(33),medium(42),low(21). See Linter Rules for what each level means. - Name — what the linter prints in findings.
- Why — the load-bearing argument. One sentence. The reason this is bad enough to ship as a rule.
- Fix — the do-instead. One sentence. Concrete enough to action.
For the full regex, scope list, and flag string of every rule, read data/anti-patterns.json — every entry there has a detection.pattern, detection.flags, and detection.scope field.
| Severity | Count | Share | CI behavior at default --threshold high
|
|---|---|---|---|
| critical | 4 | 4% | Always fails CI |
| high | 33 | 33% | Fails CI |
| medium | 42 | 42% | Reported, doesn't fail CI |
| low | 21 | 21% | Reported, doesn't fail CI |
The 4 critical rules are the WCAG-violation ones — pinch-zoom blocked, focus rings removed without :focus-visible, click handler on a <div> without role, aria-hidden on a focusable interactive element. These never ship.
| Category | Count | Share | Range |
|---|---|---|---|
| A11y | 23 | 23% | 1–23 |
| Content | 15 | 15% | 24–38 |
| Layout | 13 | 13% | 39–51 |
| Typography | 10 | 10% | 52–61 |
| Color | 9 | 9% | 62–70 |
| Visual | 9 | 9% | 71–79 |
| Quality | 9 | 9% | 80–88 |
| Motion | 8 | 8% | 89–96 |
| Performance | 4 | 4% | 97–100 |
A11y leads (23%) because accessibility is the most-frequently broken thing in AI-generated UI and the easiest to detect with regex. Content (15%) is the second-largest category — placeholder names, lorem ipsum, "Elevate / Seamless / Unleash" marketing verbs, suspiciously round numbers. Together A11y and Content account for over a third of the catalog.
| # | ID | Severity | Name | Why | Fix |
|---|---|---|---|---|---|
| 1 | inline-svg-no-aria |
high | SVG without aria-label or aria-hidden | SVG with no aria-label and no aria-hidden is announced by screen readers as 'graphic' with no context - failure of WCAG 1.1.1. | Decorative SVG gets aria-hidden="true". |
| 2 | button-no-type |
medium | Button missing type attribute | A inside a without type="button" defaults to type="submit" and silently submits the form on every click. | Always set type="button" or type="submit" explicitly. |
| 3 | img-no-alt |
high | Image missing alt attribute | Missing alt breaks screen readers, SEO image indexing, and the broken-image fallback all at once - WCAG 1.1.1 failure. | Every |
| 4 | link-onclick-no-href |
high | Anchor with onClick but no href | An without href is not focusable by default and is invisible to assistive tech - it looks like a link but behaves like a button. | Use for actions. Use for navigation. Never an anchor without href as a click target. |
| 5 | heading-skip-h1-h3 |
medium | Skipped heading level | Heading hierarchy gaps (h1 then h3, or h3 with no h1/h2 preceding) break the screen-reader landmark tree and the SEO outline. | h1 once per page; nest h2/h3/h4 in order. |
| 6 | infinite-scroll-no-pagination |
medium | Infinite scroll without keyboard fallback | Pure infinite scroll with no Load more button locks out keyboard users and breaks back-button history - WCAG 2.4 failure plus a usability one. | Ship infinite scroll AND a visible Load more button. |
| 7 | viewport-no-zoom |
critical | Viewport blocks pinch-zoom | user-scalable=no blocks pinch-zoom for low-vision users on touch devices - a WCAG 1.4.4 failure that locks out anyone who needs to enlarge text. | Remove user-scalable=no entirely, or set it to yes. |
| 8 | outline-none-no-focus-visible |
critical | outline removed without focus-visible replacement | outline: none removes the only keyboard focus indicator and leaves keyboard users with no signal of focus state - a WCAG 2.4.7 failure. | Pair outline: none with a :focus-visible rule that restores a visible ring: :focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }. |
| 9 | placeholder-as-label |
high | Input placeholder used as the only label | Placeholders disappear on focus, sit at low contrast by default, and are not announced consistently by assistive tech - using one as the only label fails WCAG 3.3.2. | Always pair with a or aria-label. |
| 10 | div-onclick-no-role |
critical | Click handler on without role or tabindex
|
A is invisible to keyboard users and assistive tech - the action exists only for mouse users, a WCAG 2.1.1 and 4.1.2 failure.
|
Use for actions. |
| 11 | anchor-no-href-as-button |
high | Anchor styled as button without href | An without href is not focusable, not in the tab order, and not announced as a link - looks like a button but behaves like nothing. | Use for actions, for navigation. |
| 12 | cursor-pointer-non-interactive |
medium | cursor:pointer on non-interactive element | A or with cursor: pointer promises clickable behavior to sighted mouse users while keyboard and screen-reader users get nothing - a hidden interaction.
|
If it clicks, make it a or . |
| 13 | tooltip-on-required-info |
high | Load-bearing copy hidden in a title attribute | A title attribute carrying 60+ characters of real content is hover-only - invisible on touch, inconsistently announced by screen readers, undiscoverable on keyboard. | Move the content into the visible flow or into an aria-describedby popover that is reachable by keyboard and touch. |
| 14 | scroll-to-top-button-everywhere |
low | Fixed scroll-to-top button on short pages | A floating back-to-top button on a page under four viewports adds permanent visual noise for a vanishingly rare interaction - signals the generator included the pattern as decoration, not need. | Drop the button. If pages can grow past four viewports, gate the button on scroll position and hide it below threshold. Keyboard users have Home; mouse users have the wheel. |
| 15 | hover-only-card-actions |
high | Card actions revealed only on hover | Card actions hidden at opacity: 0 and revealed only on :hover are invisible to touch users, keyboard users, and anyone using assistive tech - the action does not exist for most of your audience. | Show card actions persistently at reduced visual weight (lower opacity, smaller scale, secondary color), or make them reachable via a visible focus-state. |
| 16 | aria-hidden-on-interactive |
critical | aria-hidden on a focusable interactive element | aria-hidden="true" on a focusable element (button, link, input) removes it from the accessibility tree while leaving it in the tab order - keyboard users can land on a control that screen readers refuse to announce. | If the control is decorative, remove it from the DOM or set tabindex="-1" AND aria-hidden together. |
| 17 | target-blank-no-noopener |
high | target="_blank" without rel="noopener noreferrer" | target="_blank" without rel="noopener" lets the opened page access window.opener and rewrite the source URL - a tabnabbing vector. | Always pair target="_blank" with rel="noopener noreferrer". |
| 18 | safe-area-inset-only-bottom |
medium | safe-area-inset-bottom without top/left/right siblings | Reaching only for env(safe-area-inset-bottom) signals the dev copied the iPhone-X home-indicator fix without thinking about the notch (top), Dynamic Island, landscape mode, or rotated devices where left/right inset matters. | If the surface goes edge-to-edge, set all four insets (or padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left) and let CSS pick the active ones). |
| 19 | overflow-hidden-on-html |
high | overflow: hidden applied to | overflow: hidden on the root element breaks browser scroll restoration on back-navigation, disables 'skip to content' anchor scrolling for screen-reader users, kills smooth-scroll APIs, and prevents the URL bar from collapsing on mobile. | Find the actual overflowing child (often a 100vw image, fixed-position element, or oversized inline-block) and constrain it. |
| 20 | lang-attribute-missing |
high | without lang attribute | without a lang attribute leaves screen readers guessing which pronunciation engine to use - Arabic content gets read in English phonemes, English headings get read with Spanish stress patterns, and automatic translation breaks. | Add lang="en" (or your primary locale) to . |
| 21 | cursor-not-allowed-no-visual |
high | cursor: not-allowed with no visible disabled state | cursor: not-allowed in a rule that has no opacity reduction, no muted color, no border tweak, and no pointer-events: none signals the disabled state was set by an AI that knew the cursor convention but never designed the actual visual. | A disabled control needs three signals: visual (opacity 0.5 + maybe muted color or removed border), behavioral (pointer-events: none, or aria-disabled='true' + handle the click), and announcement (aria-disabled on non-button elements; the disabled attribute on actual buttons does both). |
| 22 | screen-reader-only-without-class |
medium | Skip-to-content link with no sr-only / visually-hidden class | A 'Skip to content' link without an .sr-only / .visually-hidden / .skip-link class fallback is visually shown to all users as a stray link at the top-left of the page - signals the dev knew the accessibility convention by name but didn't apply the actual technique. | Wrap with the standard sr-only pattern: Skip to content. |
| 23 | pointer-events-none-on-link |
high | with pointer-events: none and no aria-disabled | An tag with pointer-events: none and no aria-disabled='true' is announced to screen readers as a fully working link, takes a position in the tab order, and gives keyboard users an Enter-key path to nothing - the click is silently swallowed. | If the link is permanently inactive, render a with the same visual treatment instead of an . |
| # | ID | Severity | Name | Why | Fix |
|---|---|---|---|---|---|
| 24 | fake-name-john-doe |
medium | Generic placeholder names | John/Jane Doe and their cousins signal that nobody thought about who would actually use the product - immediate AI-generated-tutorial vibe. | Use plausible names that fit the target market: Maya Iqbal, Adam Levin, Wen Zhang, Layla Haddad. |
| 25 | lorem-ipsum-leak |
high | Lorem ipsum in shipping code | Lorem ipsum in production source code is unshipped placeholder content - the most obvious draft-state leak. | Write real copy that makes a specific claim about the product. |
| 26 | emoji-in-ui |
high | Emoji used as UI element | Emojis render inconsistently across platforms, ignore brand color, and signal informality where SVG icons signal craft. | Inline SVG from Lucide, Feather, Phosphor, or Heroicons at 1.5-2px stroke with currentColor. |
| 27 | icon-emoji-stamp |
high | Emoji used as icon stamp | Emoji where an SVG icon belongs breaks brand color, scales poorly, and renders differently on every OS. | Inline SVG icon with currentColor and a consistent stroke width across the surface. |
| 28 | testimonial-fake-five-stars |
high | Hardcoded five-star testimonial | Five hardcoded stars next to a fake quote is the lowest-trust pattern on the web; users have learned to discount it on sight. | Show real, named, attributable testimonials with company affiliation and a specific outcome. |
| 29 | filler-marketing-verbs |
high | Filler marketing verb in headline | Elevate, Seamless, Unleash, Revolutionize, Empower, Supercharge, Transform - the unmistakable fingerprint of unedited marketing copy generated without a real product in mind. | Name the specific action and the specific outcome. |
| 30 | generic-cta-text |
high | Generic CTA text (Click here, Learn more) | Click here / Learn more / Get started reveals nothing about the destination and fails screen-reader scan-by-link - users hear a list of identical labels with no context. | Name the specific action: Read pricing, Start free trial, View dashboard, Open API docs. |
| 31 | round-number-stats |
medium | Round number marketing stat | 99.99%, 10x faster, 100% guaranteed - the suspiciously round numbers AI invents to sound credible. | Use the real number you measured: 99.93% uptime over Q1, 4.2x faster median build, refunded 218 of 244 reported issues. |
| 32 | placeholder-as-pricing |
high | Default AI pricing tier amounts | $9 / $19 / $29 / $49 / $99 / $199 are the prices AI reaches for when no real pricing exists - unconvincing placeholders that signal nobody priced the product. | Use the real price you charge, with the real currency and the real billing cadence. |
| 33 | trust-badge-no-source |
medium | Unsourced trust-by claim | Trusted by 1000+ teams / Used by Fortune 500 / Loved by 50k developers - unsourced trust claims that signal the generator invented the number to fill space. | Name the actual companies, link to the actual case studies, or show the actual review aggregate. |
| 34 | timestamp-just-now |
medium | Fake just-now timestamp in static markup | Just now / moments ago in static HTML is a fake activity signal - it stays Just now forever and signals nobody wired the real time-ago helper. | Render real timestamps via a time-ago helper bound to a real source (Date.now, server time, last_event_at). |
| 35 | fake-line-of-code-count |
medium | Unsourced large-number marketing claim | We saved 10,000 hours / Generated 1,000,000 lines of code / Reduced 50,000 tickets - large round numbers with no citation that signal the magnitude was invented to sound impressive. | Use the real measured number with the real source: 'In Q1 2026, customers saved 4,217 support hours (mean across 38 teams)'. |
| 36 | before-after-100-percent |
medium | Magnitude-default before-after claim | 0 to 100 / 100% guaranteed / 100% effective are the magnitude-by-default phrases AI reaches for - readers have learned to discount them. | Quote the real spread: 'from 14 manual steps to 3 automated'. |
| 37 | unsplash-photo-id-no-alt |
high | Unsplash photo URL with empty or missing alt | An images.unsplash.com/photo-* URL with no alt (or alt="") is the stock-photo placeholder fingerprint - signals the generator pasted the first credible image without choosing it for the content. | Replace with a real product or brand image and write an alt that describes the content (alt="Customer service team reviewing a dashboard"). |
| 38 | version-1-0-0-evergreen |
low | Hardcoded v1.0.0 in nav or footer | A static 'v1.0.0' in the nav or footer never updates with the real build - signals the generator added a version chip as decoration without wiring it to package.json or the deploy SHA. | Either pull the version from package.json at build time (Vite/Webpack define), bind it to the deploy SHA, or remove the chip entirely. |
| # | ID | Severity | Name | Why | Fix | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 39 | three-equal-card-grid |
high | Three equal cards in a row | Three equal cards with three icons and three short paragraphs is the safest default the generator reaches for and the strongest layout fingerprint in AI-generated marketing surfaces. | Use asymmetric layouts: bento grids, 2-and-1 splits, 4 with one spanning width. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 40 | centered-everything-hero |
medium | Centered hero composition | Centered headline + centered subtitle + centered button stack is the laziest hero composition and signals the generator picked it when it couldn't find a better layout. | Use left-aligned hero with editorial 7-5 or 8-4 grid split; let the imagery earn its own column. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 41 | pill-rounded-full-everywhere |
low | rounded-full applied to everything | rounded-full on every button and input is the iOS-tutorial fingerprint - signals the radius decision was skipped entirely. | rounded-full for pills, avatars, icon buttons only. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 42 | avatar-stack-overlapping |
medium | Generic overlapping avatar stack | Three overlapping circular avatars in the nav as 'our team' or 'join 10k users' is a content-marketing template tell. | Use real customer logos, named testimonials with quotes, or specific signal (Used by 312 engineering teams at Series-A companies). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 43 | nav-equal-hamburger-desktop |
low | Hamburger menu on desktop | Hamburger on a wide viewport hides nav from users for no reason; signals the responsive breakpoint was skipped. | Show inline nav on md: and up; hide the hamburger above that breakpoint with md:hidden. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 44 | h-screen-no-dvh-fallback |
high | 100vh without 100dvh fallback for mobile | 100vh on mobile includes the browser chrome that retracts on scroll, so the layout jumps as the address bar collapses - dynamic viewport units (dvh) fix this. | Use height: 100dvh or h-[100dvh] as the primary, with h-screen / 100vh as the legacy fallback for browsers that lack dvh. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 45 | fixed-height-text-block |
medium | Pixel height on a text container | Fixing a pixel height on a container that also styles text breaks reflow at every other zoom level and font scale - the layout is one breakpoint away from clipping. | Use min-height for floor protection, line-height + padding for vertical rhythm, and let content determine height. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 46 | logo-cloud-no-real-logos |
medium | Logo cloud image with empty alt | An |
Set alt to the real company name (alt="Stripe"), and only use the logo if you have permission. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 47 | aspect-ratio-1-1-default |
medium | Square aspect-ratio as default everywhere | Every image clipped to 1:1 is the Instagram-grid AI fingerprint - signals the generator picked the safest crop instead of letting content set the frame. | Vary aspect ratio by content: 3:2 for product shots, 4:5 for portraits, 16:9 for hero or video. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 48 | grid-cols-3-1fr-default |
low | repeat(3, 1fr) as a default grid | repeat(3, 1fr) with no minmax(), no auto-fit, and no asymmetric weighting is the laziest grid declaration - signals the layout was set by a generator that defaulted to 'three equal columns' without thinking about content priority or responsive behavior. | Use minmax for responsive floors: repeat(auto-fit, minmax(280px, 1fr)). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 49 | cta-buttons-clustered-in-hero |
medium | Three-plus CTA buttons clustered in hero | Three or more CTA-styled buttons within a single hero is decision paralysis - the AI's compromise when it can't choose the primary action. | Pick one primary CTA. Demote the rest to text links or move them to a secondary surface. If you genuinely need three actions, restructure the hero into a comparison or a stepped layout so the primary is unambiguous. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 50 | display-table-for-layout |
medium | display: table used for layout outside data tables |
display: table on a non- element is a 2008-era IE6 hack for vertical centering before flexbox existed.
The full JSON shape and the steps to add a 101st rule live in Linter Rules — Adding a new rule. The discipline: every rule needs a tight See also: Linter Rules · Recommender Engine · Architecture · Anti-AI slop ban list Source: data/anti-patterns.json Linter: engine/linter/core.py |
without 