feat(rules): add 10 motion, accessibility, and design lint rules (reopen #825)#850
feat(rules): add 10 motion, accessibility, and design lint rules (reopen #825)#850rayhanadev wants to merge 11 commits into
Conversation
Derived from a cross-resource design reference (every principle from the design knowledge base), deduped against the full existing rule inventory. Motion: extend no-transition-all to the Tailwind `transition-all` class (was inline-only) and add no-tailwind-layout-transition for arbitrary `transition-[<layout-prop>]`. Accessibility: no-autoplay-without-muted, no-uninformative-aria-label, no-target-blank-without-rel, and no-low-contrast-inline-style (computes the real WCAG 2.1 ratio from co-located inline color + backgroundColor). Design/Tailwind hygiene: no-redundant-display-class, prefer-truncate-shorthand, no-full-viewport-width, and no-svg-currentcolor-with-fill-class. All register via codegen (388 rules); 77 co-located tests pass; typecheck, lint, and format clean.
- no-low-contrast-inline-style: when the inline style has no `fontSize`, the text may be sized large via a class (`text-5xl`), so fall back to the lenient 3:1 large-text threshold instead of 4.5:1 to avoid flagging large text that only needs 3:1. Stricter 4.5:1 applies only when the size is a visible normal size. - no-redundant-display-class: `<li>` defaults to `display: list-item`, not `block`, so `block` on an `<li>` is meaningful — removed `li` from the block-default set. Adds regression tests for both.
- no-low-contrast-inline-style: use a solid `background` shorthand color as the background (was skipped entirely, missing low-contrast pairs); skip only gradients/images/ambiguous (both backgroundColor + background). Also treat a string `fontWeight: "700"` as bold for the large-text threshold. - no-transition-all: match `transition-all` as a whole Tailwind token (segment after variant prefixes) so compound classes like `transition-all-custom` are not falsely flagged. - no-svg-currentcolor-with-fill-class: exclude stroke-WIDTH utilities (`stroke-2`, `stroke-[1.5]`) — they set thickness, not color — so they no longer false-conflict with `stroke="currentColor"`. Adds regression tests for each.
- no-deprecated-tailwind-class: flag Tailwind v4 renames (bg-gradient-* → bg-linear-*, flex-shrink-* → shrink-*, flex-grow-* → grow-*, overflow-ellipsis → text-ellipsis). Gated on a new `tailwind:4` capability. - no-arbitrary-px-font-size: `text-[13px]` → rem so text scales with the user's root font size (px stays fine for border-*/outline-*). - prefer-dvh-over-vh: `h-screen`/`min-h-screen`/`h-[100vh]` → dvh (tailwind:3.4). Adds `tailwind:4` capability to @react-doctor/core and extracts a shared `get-class-name-tokens` util (variant-aware token splitting), reused by no-transition-all and no-svg-currentcolor-with-fill-class.
- no-tailwind-layout-transition: match transition-[…] property names exactly (comma-split + set) so SVG `stroke-width` / `border-width` — which merely contain "width" — are no longer flagged as HTML layout thrash. - no-full-viewport-width: drop `max-w-*` / `maxWidth` (a defensive cap, not the overflow footgun); keep `w-screen`/`w-[100vw]`/`width`/`minWidth`. - prefer-dvh-over-vh: drop `max-h-*` / `maxHeight` (a valid height cap, e.g. a scrollable modal); keep `h-screen`/`min-h-screen`/`height`/`minHeight`. - no-deprecated-tailwind-class: only rename `bg-gradient-to-*` → `bg-linear-to-*` (v4 radial/conic are `bg-radial`/`bg-conic`, not `bg-linear-radial`). Adds regression tests for each false positive.
- no-svg-currentcolor-with-fill-class: only treat UNPREFIXED `fill-*`/`stroke-*`
color classes as conflicts; `hover:fill-blue-600` / `dark:fill-white` (state-
gated, no static conflict with the base `fill="currentColor"`) are no longer
flagged — a very common icon pattern.
- no-low-contrast-inline-style: reject 4-arg `rgb(0,0,0,0.5)` / slash-form alpha
(was judged opaque), and bail when the style object contains a `{...spread}`
that could override the colors. Tag `test-noise` to match sibling no-tiny-text.
- no-full-viewport-width: drop the redundant `category: "Architecture"` override
(it's the design-bucket default).
- Extract `has-jsx-spread-attribute` util; use it in the two a11y rules.
Adds regression tests for the variant-prefixed svg classes, rgb-alpha, opaque
rgb, and the style-spread bail.
Consolidate duplication surfaced by an adversarial deslop pass; no behavior change (the same diagnostics fire for the same inputs): - no-autoplay-without-muted: fold isStaticallyTrue/isStaticallyFalse into one tri-state resolveStaticBoolean, mirroring the existing parseJsxValue idiom - no-svg-currentcolor-with-fill-class: collapse the duplicated fill/stroke branches into a single loop over the two paint attributes - no-low-contrast-inline-style: hoist the repeated `properties` fallback - no-uncontrolled-input: drop the local hasJsxSpreadAttribute copy in favor of the branch's new shared utils/has-jsx-spread-attribute, completing the extraction Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`overflow-clip` is a current Tailwind utility for `overflow: clip` (v3.1+, still present in v4) — it was never renamed to `text-clip`, which sets the unrelated `text-overflow: clip`. The stray mapping produced an incorrect "renamed in v4" suggestion (Cursor Bugbot). Only `overflow-ellipsis -> text-ellipsis` is a real rename; keep that. Adds a regression test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d:4 gate `tailwind:4` gates no-deprecated-tailwind-class, which tells users a class was renamed/removed in v4. The optimistic-on-null policy (assume latest for an unparseable spec like `workspace:*`) is right for the suggestion rule behind `tailwind:3.4`, but for a deprecation rule it surfaces confidently-wrong "renamed in v4" warnings on a v3 project. Require a CONFIRMED parsed major >= 4 for tailwind:4 — favor a false negative over a false positive (Cursor Bugbot). `tailwind:3.4` stays optimistic. Adds an unparseable-version regression test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
commit: |
|
No React Doctor issues found. 🎉 Reviewed by React Doctor for commit |
- no-full-viewport-width: match variant-prefixed `md:w-screen` / `lg:min-w-screen`, mirroring the sibling `prefer-dvh-over-vh` regex (overflow happens at every breakpoint).
- no-target-blank-without-rel: resolve `rel={'noopener'}` expression-container literals instead of treating them as dynamic, so reverse-tabnabbing isn't silently missed; share one literal resolver with the `target` check.
- Extract the bare `16` root-font-size divisor into `ROOT_FONT_SIZE_PX` per the magic-number convention.
Extracts no-target-blank-without-rel's inline getStringLiteralAttributeValue
into a shared util and uses it in no-uninformative-aria-label, so a static
braced literal like aria-label={'icon'} is judged instead of silently skipped
(getJsxPropStringValue only handles plain string literals). Adds regression
tests for the braced uninformative and braced descriptive cases.
Addresses Bugbot review on #850.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b69e1df. Configure here.
| if (value === "" || value === "current") return false; | ||
| if (/^\d/.test(value) || /^\[\d/.test(value)) return false; | ||
| return true; | ||
| }); |
There was a problem hiding this comment.
SVG rule misflags stroke utilities
Medium Severity
hasColorUtility treats any unprefixed stroke-* / fill-* token that is not width-like as a color. Tailwind also uses those prefixes for stroke-linecap-*, stroke-linejoin-*, fill-none, stroke-none, and similar non-color utilities, so common SVG icon class lists with stroke="currentColor" can be reported incorrectly.
Reviewed by Cursor Bugbot for commit b69e1df. Configure here.
|
|
||
| const isCurrentColor = (attribute: EsTreeNodeOfType<"JSXAttribute">): boolean => { | ||
| const value = getJsxPropStringValue(attribute); | ||
| return value !== null && value.trim().toLowerCase() === "currentcolor"; |
There was a problem hiding this comment.
Braced currentColor not detected
Low Severity
isCurrentColor reads attribute values only via getJsxPropStringValue, so fill={'currentColor'} / stroke={'currentColor'} is not recognized. The same PR adds getStringLiteralAttributeValue for braced string literals on other rules, but this rule skips it, missing real conflicts when a color class is present.
Reviewed by Cursor Bugbot for commit b69e1df. Configure here.


Reopens #825, which was squash-merged and then reverted in #849. GitHub will not reopen a merged PR, so this is a fresh PR from the same branch (
add-design-motion-a11y-rules, restored at its original tip74cf07bb) carrying the identical changes.Merge order: land the revert (#849) first, then this PR re-adds the batch cleanly to
main.Original description from #825:
Why
Adds 10 statically-lintable design rules derived from a cross-resource design knowledge base (anti-slop design languages, Tailwind/shadcn systems, motion sources, OKLCH/contrast, Apple HIG, jsx-a11y). Each was deduped against React Doctor's full existing inventory (not just
design/), so these fill real gaps — notably the Tailwind-class siblings of inline-only motion rules and a real WCAG-ratio contrast check.What changed
Motion —
no-transition-allextended to the Tailwindtransition-allclass; newno-tailwind-layout-transition.Accessibility —
no-autoplay-without-muted,no-uninformative-aria-label,no-target-blank-without-rel,no-low-contrast-inline-style(real WCAG 2.1 ratio).Design / Tailwind hygiene —
no-redundant-display-class,prefer-truncate-shorthand,no-full-viewport-width,no-svg-currentcolor-with-fill-class,no-arbitrary-px-font-size,prefer-dvh-over-vh,no-deprecated-tailwind-class(newtailwind:4capability).Adds
get-wcag-contrast-ratio+get-class-name-tokensutils; registry regenerated via codegen; changeset included.Validation
77 co-located tests pass; RDE / OSS eval across 500 repos with 0 false positives.
See #825 for the full original description and review history.
Note
Medium Risk
Adds many new warn-level diagnostics that could increase noise on large codebases, though rules lean on static analysis and version gates to limit false positives; a11y and contrast checks affect user-facing UI quality rather than runtime behavior.
Overview
This PR re-introduces a large set of React Doctor oxlint rules (after a prior merge/revert), covering motion performance, accessibility, and Tailwind/JSX hygiene, plus supporting utilities and capability wiring.
Motion:
no-transition-allnow flags Tailwindtransition-all(not only inlinetransition/transitionProperty: "all"). Newno-tailwind-layout-transitionreports arbitrarytransition-[…]utilities that animate layout properties (width, height, margin, etc.).Accessibility: New rules for
autoPlaywithoutmutedon native<video>/<audio>, vaguearia-labels (e.g."icon"),target="_blank"withoutnoopener/noreferrer, andno-low-contrast-inline-style, which computes WCAG 2.1 contrast from statically resolvable inlinecolor+ background (with conservative skips for spreads, alpha,var(), gradients).Design / Tailwind: Rules for redundant default display classes,
truncateshorthand,w-screen/100vwoverflow, SVGcurrentColorvsfill-*/stroke-*conflicts,text-[Npx]→ rem,h-screen/100vh→ dvh (requirestailwind:3.4), and Tailwind v4 renamed utilities (requirestailwind:4).Core / infra:
@react-doctor/coreaddstailwind:4only when Tailwind major ≥ 4 is confirmed (stricter than the optimistictailwind:3.4gate for unparseable versions). Shared helpers includegetClassNameTokens,getStringLiteralAttributeValue, centralizedhasJsxSpreadAttribute(also used byno-uncontrolled-input), andgetWcagContrastRatio. In-house a11y rules are listed socustomRulesOnlydoes not drop them; registry and tests are updated.Reviewed by Cursor Bugbot for commit b69e1df. Bugbot is set up for automated code reviews on this repo. Configure here.