Skip to content

feat(rules): add 10 motion, accessibility, and design lint rules (reopen #825)#850

Open
rayhanadev wants to merge 11 commits into
mainfrom
add-design-motion-a11y-rules
Open

feat(rules): add 10 motion, accessibility, and design lint rules (reopen #825)#850
rayhanadev wants to merge 11 commits into
mainfrom
add-design-motion-a11y-rules

Conversation

@rayhanadev

@rayhanadev rayhanadev commented Jun 16, 2026

Copy link
Copy Markdown
Member

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 tip 74cf07bb) 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

Motionno-transition-all extended to the Tailwind transition-all class; new no-tailwind-layout-transition.
Accessibilityno-autoplay-without-muted, no-uninformative-aria-label, no-target-blank-without-rel, no-low-contrast-inline-style (real WCAG 2.1 ratio).
Design / Tailwind hygieneno-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 (new tailwind:4 capability).

Adds get-wcag-contrast-ratio + get-class-name-tokens utils; 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-all now flags Tailwind transition-all (not only inline transition / transitionProperty: "all"). New no-tailwind-layout-transition reports arbitrary transition-[…] utilities that animate layout properties (width, height, margin, etc.).

Accessibility: New rules for autoPlay without muted on native <video>/<audio>, vague aria-labels (e.g. "icon"), target="_blank" without noopener/noreferrer, and no-low-contrast-inline-style, which computes WCAG 2.1 contrast from statically resolvable inline color + background (with conservative skips for spreads, alpha, var(), gradients).

Design / Tailwind: Rules for redundant default display classes, truncate shorthand, w-screen / 100vw overflow, SVG currentColor vs fill-*/stroke-* conflicts, text-[Npx] → rem, h-screen/100vh → dvh (requires tailwind:3.4), and Tailwind v4 renamed utilities (requires tailwind:4).

Core / infra: @react-doctor/core adds tailwind:4 only when Tailwind major ≥ 4 is confirmed (stricter than the optimistic tailwind:3.4 gate for unparseable versions). Shared helpers include getClassNameTokens, getStringLiteralAttributeValue, centralized hasJsxSpreadAttribute (also used by no-uncontrolled-input), and getWcagContrastRatio. In-house a11y rules are listed so customRulesOnly does 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.

aidenybai and others added 9 commits June 16, 2026 00:54
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>
@pkg-pr-new

pkg-pr-new Bot commented Jun 16, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/eslint-plugin-react-doctor@850
npm i https://pkg.pr.new/oxlint-plugin-react-doctor@850
npm i https://pkg.pr.new/react-doctor@850

commit: b69e1df

@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

No React Doctor issues found. 🎉

Reviewed by React Doctor for commit b69e1df.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

Open in Devin Review

- 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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ 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;
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

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";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b69e1df. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants