tailwind-a11y 0.14.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -57,7 +57,7 @@ Exits `1` on violations — safe to use as a CI gate.
57
57
  | Touch target | 2.5.8 (AA) | Interactive elements under 24×24px — or 44×44px with `--strict` (2.5.5, AAA) |
58
58
  | Focus indicator | 2.4.7 (AA) | `focus:outline-none` with no visible replacement |
59
59
  | Focus indicator contrast | 1.4.11 (AA) | A present `outline-*`/`ring-*` focus indicator below 3:1 contrast — or also below the 2px minimum thickness with `--strict` (2.4.13, AAA) |
60
- | Reduced motion | 2.3.3 (AAA, `--strict` only) | A `hover:`/`focus:`/`focus-visible:`/`active:`-scoped `scale-*`/`rotate-*`/`translate-*`/`skew-*` change with an unscoped `transition`/`transition-all`/`transition-transform`, or an `animate-spin`/`-ping`/`-bounce` under the same variants, with no `motion-reduce:`/`motion-safe:` handling |
60
+ | Reduced motion | 2.3.3 (AAA, `--strict` only) | A `hover:`/`focus:`/`focus-visible:`/`active:`-scoped (or their `group-*`/`peer-*` equivalents) `scale-*`/`rotate-*`/`translate-*`/`skew-*` change with an unscoped `transition`/`transition-all`/`transition-transform`, or an `animate-spin`/`-ping`/`-bounce` under the same variants, with no `motion-reduce:`/`motion-safe:` handling |
61
61
 
62
62
  ## Scope
63
63
 
@@ -1,7 +1,34 @@
1
1
  import * as t from "@babel/types";
2
2
  import { getStaticClassName, parseJSX, traverse } from "./babelInterop.js";
3
3
  const TRANSITION_BASES = new Set(["transition", "transition-all", "transition-transform"]);
4
- const INTERACTION_VARIANTS = new Set(["hover", "focus", "focus-visible", "focus-within", "active"]);
4
+ // group-hover:/peer-hover:/etc. compile to the identical momentary-pseudo-
5
+ // class shape as bare hover:, just evaluated against an ancestor/sibling
6
+ // (.group/.peer marker) instead of the element itself -- verified against a
7
+ // real Tailwind v4 build (`.group-hover\:scale-110:is(:where(.group):hover *)`).
8
+ // Deliberately NOT extended to has-*:/arbitrary variants ([&:hover]:, already
9
+ // an established out-of-scope precedent for this file -- unbounded selector
10
+ // text, not a closed enumerable set) or in-*: (a real v4.1+ ancestor-state
11
+ // variant with the identical shape, but a legitimate separate follow-up, not
12
+ // folded into this set).
13
+ const INTERACTION_VARIANTS = new Set([
14
+ "hover", "focus", "focus-visible", "focus-within", "active",
15
+ "group-hover", "group-focus", "group-focus-visible", "group-focus-within", "group-active",
16
+ "peer-hover", "peer-focus", "peer-focus-visible", "peer-focus-within", "peer-active",
17
+ ]);
18
+ // Named groups/peers (`group-hover/sidebar:scale-110`) compile the group
19
+ // name into the variant token itself via a slash
20
+ // (`.group-hover\/sidebar\:scale-110:is(:where(.group\/sidebar):hover *)`),
21
+ // so variantSegments() returns "group-hover/sidebar" verbatim -- a plain
22
+ // Set.has() would miss it. Safe to slice on the first "/" and re-check:
23
+ // Tailwind's opacity-modifier slash (`text-black/50`) lives inside the base
24
+ // utility segment (after the last ":"), never inside a variant segment, so
25
+ // there's no collision to worry about here.
26
+ function isInteractionVariant(v) {
27
+ if (INTERACTION_VARIANTS.has(v))
28
+ return true;
29
+ const slash = v.indexOf("/");
30
+ return slash !== -1 && INTERACTION_VARIANTS.has(v.slice(0, slash));
31
+ }
5
32
  function baseUtility(raw) {
6
33
  return raw.slice(raw.lastIndexOf(":") + 1);
7
34
  }
@@ -33,7 +60,7 @@ export function extractReducedMotionChecks(code, filePath) {
33
60
  return;
34
61
  const classes = className.split(/\s+/).filter(Boolean);
35
62
  const hasTransitionBase = classes.some((raw) => TRANSITION_BASES.has(baseUtility(raw)));
36
- const hasInteractionClass = classes.some((raw) => variantSegments(raw).some((v) => INTERACTION_VARIANTS.has(v)));
63
+ const hasInteractionClass = classes.some((raw) => variantSegments(raw).some(isInteractionVariant));
37
64
  // A second, independent candidacy path for animate-* utilities, which
38
65
  // carry their own `animation` property and need no transition-* base
39
66
  // at all -- `hover:animate-bounce` alone must be a candidate even
@@ -51,7 +78,7 @@ export function extractReducedMotionChecks(code, filePath) {
51
78
  // documents for why unscoped animate-* is out of scope here.
52
79
  const hasInteractionScopedAnimate = classes.some((raw) => {
53
80
  const base = baseUtility(raw);
54
- return isAnimateBase(base) && variantSegments(raw).some((v) => INTERACTION_VARIANTS.has(v));
81
+ return isAnimateBase(base) && variantSegments(raw).some(isInteractionVariant);
55
82
  });
56
83
  // Not a candidate at all unless there's some transition utility
57
84
  // (scoped or not) *and* some interaction-scoped class, OR an
@@ -5,7 +5,25 @@
5
5
  // animates (the browser has nothing telling it to transition that
6
6
  // property), and correctly isn't flagged.
7
7
  const TRANSITION_BASES = new Set(["transition", "transition-all", "transition-transform"]);
8
- const INTERACTION_VARIANTS = new Set(["hover", "focus", "focus-visible", "focus-within", "active"]);
8
+ // group-hover:/peer-hover:/etc. compile to the identical momentary-pseudo-
9
+ // class shape as bare hover:, just evaluated against an ancestor/sibling
10
+ // (.group/.peer marker) instead of the element itself -- see the identical
11
+ // set (and full explanation) in extractReducedMotion.ts. Deliberately NOT
12
+ // extended to has-*:/arbitrary variants or in-*: -- see that file's comment.
13
+ const INTERACTION_VARIANTS = new Set([
14
+ "hover", "focus", "focus-visible", "focus-within", "active",
15
+ "group-hover", "group-focus", "group-focus-visible", "group-focus-within", "group-active",
16
+ "peer-hover", "peer-focus", "peer-focus-visible", "peer-focus-within", "peer-active",
17
+ ]);
18
+ // Named groups/peers (`group-hover/sidebar:...`) put the group name in the
19
+ // variant token itself via a slash -- see extractReducedMotion.ts for the
20
+ // full explanation of why slicing on the first "/" is safe here.
21
+ function isInteractionVariant(v) {
22
+ if (INTERACTION_VARIANTS.has(v))
23
+ return true;
24
+ const slash = v.indexOf("/");
25
+ return slash !== -1 && INTERACTION_VARIANTS.has(v.slice(0, slash));
26
+ }
9
27
  function baseUtility(raw) {
10
28
  return raw.slice(raw.lastIndexOf(":") + 1);
11
29
  }
@@ -99,7 +117,7 @@ export function checkReducedMotion(checks, strict = false) {
99
117
  // `dark:transition hover:scale-110` was silently treated as
100
118
  // compliant even though it animates on hover in dark mode
101
119
  // regardless of the user's motion preference.
102
- const isInteractionGated = segments.some((v) => INTERACTION_VARIANTS.has(v));
120
+ const isInteractionGated = segments.some(isInteractionVariant);
103
121
  const isMotionSafeGated = segments.includes("motion-safe");
104
122
  if (TRANSITION_BASES.has(base) && !isInteractionGated && !isMotionSafeGated)
105
123
  realTransition = raw;
@@ -138,7 +156,7 @@ export function checkReducedMotion(checks, strict = false) {
138
156
  // explanation of why both checks are per-candidate, not per-element.
139
157
  if (segments.includes("motion-safe"))
140
158
  return false;
141
- if (!segments.some((v) => INTERACTION_VARIANTS.has(v)))
159
+ if (!segments.some(isInteractionVariant))
142
160
  return false;
143
161
  return ANIMATE_MOTION_BASES.has(baseUtility(raw));
144
162
  });
@@ -171,7 +189,7 @@ export function checkReducedMotion(checks, strict = false) {
171
189
  // self-guarded at the same time, e.g. `hover:motion-safe:scale-110`).
172
190
  if (segments.includes("motion-safe"))
173
191
  return false;
174
- if (!segments.some((v) => INTERACTION_VARIANTS.has(v)))
192
+ if (!segments.some(isInteractionVariant))
175
193
  return false;
176
194
  return isNonIdentityMotionUtility(baseUtility(raw));
177
195
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tailwind-a11y",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
4
4
  "description": "Static analysis CLI that catches WCAG accessibility violations — color contrast, touch target size, and focus indicator removal/contrast — in Tailwind CSS class combinations before they ship.",
5
5
  "type": "module",
6
6
  "bin": {