srcdev-nuxt-components 9.6.11 → 9.6.12

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.
@@ -204,6 +204,11 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
204
204
  `aria-hidden` (`ToggleSwitch`, 2026-10-16).
205
205
  - Live-updating or auto-advancing content (carousels, marquees, auto-dismissing toasts) has a
206
206
  way to pause it, per WCAG 2.2.2 — a visible control, or pause-on-hover/focus at minimum.
207
+ - Pause-on-focus listens for `focusin`/`focusout` on the root (with a `relatedTarget`
208
+ containment check), not `focus`/`blur`, which don't bubble: focusable content inside an
209
+ `aria-hidden` moving track otherwise gets focus while still hidden and moving. A looping
210
+ duplicate copy gets `inert`, and a focus-caused pause must not undo a pause the user chose
211
+ (`MarqueeScroller`, 2026-10-18).
207
212
  - `prefers-reduced-motion` is respected for any animation that isn't purely decorative.
208
213
  - A mode that hides a native control's own indicator (a range thumb, a checkbox box, a caret)
209
214
  draws its own visible state for the current value, and its interactive area isn't shrunk to a
@@ -57,10 +57,25 @@ Full token table in `app/components/05.forms/form-field/CONSUMER-STYLING.md`. Pu
57
57
  Target state via `.form-field[data-width="wide"]`, `.form-field[data-has-gutter]`,
58
58
  `.form-field[data-invalid]`.
59
59
 
60
+ FormField doesn't change `overflow-wrap` or `min-inline-size` on its slot content: wrapping long
61
+ copy is the slotted control's job (every `*Field` component handles its own). Plain text slotted
62
+ directly needs its own wrapping rule.
63
+
64
+ ---
65
+
66
+ ## Storybook
67
+
68
+ `Components/Forms/Form Field/FormField`: `Default`, `AllWidths`, `NoGutter`, and `StressTest`
69
+ (each width, with and without gutter and error state, around an `InputTextField` with hostile
70
+ copy). Stories use the `CanvasSwitcher` decorator.
71
+
60
72
  ---
61
73
 
62
74
  ## History
63
75
 
76
+ **2026-10-17**: added the `StressTest` story, `CanvasSwitcher` decorator and Docs-page
77
+ descriptions; nothing in the component broke under it.
78
+
64
79
  **Migrated 2026-09-26** (0/5 → full compliance):
65
80
 
66
81
  - Width and gutter modifiers moved from bare classes (`.narrow`, `.medium`, `.wide`, `.has-gutter`)
@@ -21,8 +21,8 @@ ARIA semantics.
21
21
  | `:legend` | `string` | `""` | Legend text. The legend renders when this or the `legend` slot is given. |
22
22
  | `group-role` | `"group" \| "radiogroup"` | `"group"` | `"group"` renders no `role` attribute (a fieldset's native role is already `group`). `"radiogroup"` sets `role="radiogroup"`: use it for radio buttons only. |
23
23
  | `:required` | `boolean` | `false` | Sets `aria-required="true"` **only** when `group-role="radiogroup"`. `aria-required` isn't allowed on `role="group"`, so checkbox groups get nothing; put "(required)" in the legend text instead. |
24
- | `:field-has-error` | `boolean` | `false` | Sets `aria-invalid` on the fieldset. |
25
- | `:data-testid` | `string` | `""` | Passed to the fieldset. |
24
+ | `:field-has-error` | `boolean` | `false` | Sets `aria-invalid="true"` on the fieldset; the attribute is absent when `false`. |
25
+ | `:data-testid` | `string` | `""` | Passed to the fieldset; omitted when empty. |
26
26
  | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra classes on the fieldset, via `useStyleClassPassthrough` (reacts to prop changes). |
27
27
 
28
28
  ## Slots
@@ -54,10 +54,26 @@ content `--form-fieldset-content-margin-block|margin-inline`, legend
54
54
  `--form-fieldset-legend-color|font-size|font-weight|line-height|margin-block|margin-inline`. Error state hook:
55
55
  `.form-fieldset[aria-invalid="true"]`.
56
56
 
57
+ The fieldset sets `min-inline-size: 0` and the legend `overflow-wrap: anywhere`, so neither
58
+ overflows a narrow column. Slotted controls handle their own wrapping.
59
+
60
+ ---
61
+
62
+ ## Storybook
63
+
64
+ `Components/Forms/Form Fieldset/FormFieldset`: `Default`, `CheckboxGroup`, `WithError`, and
65
+ `StressTest` (hostile legend copy with the error state on). Stories use the `CanvasSwitcher`
66
+ decorator and link to the three components that render FormFieldset.
67
+
57
68
  ---
58
69
 
59
70
  ## History
60
71
 
72
+ **2026-10-17**: StressTest story, CanvasSwitcher and Docs-page descriptions added. Fixed: a
73
+ valid fieldset rendered `aria-invalid="false"`; an empty `dataTestid` rendered `data-testid=""`;
74
+ the fieldset had no `min-inline-size: 0` (each wrapping component set it on its own class), and
75
+ the legend didn't wrap unbroken copy.
76
+
61
77
  **Migrated 2026-09-26** (0/5 → full compliance):
62
78
 
63
79
  - **Role bug fixed:** `role="radiogroup"` used to be hardcoded, so screen readers announced
@@ -10,8 +10,9 @@ type: reference
10
10
 
11
11
  `MarqueeScroller` renders an infinite horizontal scroller for small repeating items — client
12
12
  logos, badges, testimonial avatars. The track is duplicated once so the loop is seamless
13
- (`marquee-group` renders twice: once real, once `aria-hidden="true"`), and the whole thing pauses
14
- automatically on hover, on keyboard focus, and when the user has `prefers-reduced-motion` set.
13
+ (`.marquee-scroller-group` renders twice: once real, once `aria-hidden="true"` and `inert`), and the
14
+ whole thing pauses automatically on hover, whenever focus is inside it, and when the user has
15
+ `prefers-reduced-motion` set.
15
16
 
16
17
  ## Props
17
18
 
@@ -24,11 +25,12 @@ automatically on hover, on keyboard focus, and when the user has `prefers-reduce
24
25
  | `ariaLabel` | `string` | `"Scrolling content"` | `aria-label` on the root region — override for localisation. |
25
26
  | `ariaDescription` | `string` | `"Use spacebar to pause or play the animation."` | Screen-reader-only instructions rendered inside the region — override for localisation. |
26
27
  | `showControls` | `boolean` | `false` | Shows a visible pause/play button. Recommended (WCAG 2.2.2, Pause/Stop/Hide) whenever content scrolls automatically and indefinitely. |
27
- | `respectReducedMotion` | `boolean` | `true` | Auto-pauses and disables the animation for users with `prefers-reduced-motion: reduce`, and pauses on keyboard focus. |
28
+ | `respectReducedMotion` | `boolean` | `true` | Auto-pauses and disables the animation for users with `prefers-reduced-motion: reduce` (and keeps it paused after focus leaves). Focus-pausing no longer depends on it. |
28
29
  | `playIcon` | `string` | `"mdi:play"` | Iconify icon name shown on the control button while paused. Ignored if the `toggle-icon` slot is used. |
29
30
  | `pauseIcon` | `string` | `"mdi:pause"` | Iconify icon name shown on the control button while playing. Ignored if the `toggle-icon` slot is used. |
30
- | `playLabel` | `string` | `"Play animation"` | Control button `aria-label` while paused — override for localisation. |
31
- | `pauseLabel` | `string` | `"Pause animation"` | Control button `aria-label` while playing — override for localisation. |
31
+ | `playLabel` | `string` | `"Play animation"` | Control button text (visually hidden, its accessible name) while paused — override for localisation. |
32
+ | `pauseLabel` | `string` | `"Pause animation"` | Control button text (visually hidden, its accessible name) while playing — override for localisation. |
33
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes on the root `.marquee-scroller`, via `useStyleClassPassthrough` (reacts to prop changes). |
32
34
 
33
35
  Import the item types from the library's type exports when building `marqueeData`/`itemConfig`
34
36
  outside the component:
@@ -52,8 +54,10 @@ One dynamically-named slot per `marqueeData` entry, named after that item's `id`
52
54
  </MarqueeScroller>
53
55
  ```
54
56
 
55
- Each slot is rendered twice (once in the visible group, once in the `aria-hidden` duplicate group)
56
- so the loop wraps seamlessly — don't put anything with side effects or unique ids inside a slot.
57
+ Each slot is rendered twice (once in the visible group, once in the `aria-hidden`, `inert`
58
+ duplicate group) so the loop wraps seamlessly. Don't put anything with side effects or unique ids
59
+ inside a slot. Links are fine: only the real copy is focusable. Items are fixed-size boxes; text
60
+ wraps inside them (`overflow-wrap: anywhere`) but content taller than `itemConfig.height` overflows.
57
61
 
58
62
  `toggle-icon` (scoped with `{ isPaused: boolean }`) replaces the control button's icon entirely —
59
63
  use this instead of `playIcon`/`pauseIcon` when an Iconify name isn't enough (custom SVG, a
@@ -70,16 +74,25 @@ different icon set):
70
74
  ## Accessibility behaviour
71
75
 
72
76
  - The root is `role="region"` with `tabindex="0"` so it's independently focusable.
73
- - Spacebar toggles pause/play while the region is focused. Arrow keys do nothing — there is no
77
+ - Spacebar toggles pause/play while the region itself is focused (keydown from inside, e.g. on the
78
+ control button or a link, is left to that element). Arrow keys do nothing — there is no
74
79
  manual-stepping behaviour, so the default `ariaDescription` doesn't claim one (fixed 2026-09-08;
75
80
  it previously announced "arrow keys ... for manual control" with no implementation behind it).
76
- - Focusing the region pauses the animation when `respectReducedMotion` is `true` (the default);
77
- blurring resumes it unless the user prefers reduced motion.
81
+ - Focus anywhere inside the region (the region itself, or a link in an item) pauses the animation
82
+ and removes `aria-hidden` from the track, so the focused content is announced. Focus moving between
83
+ elements inside keeps it paused. When focus leaves, it resumes only if focus caused the pause and
84
+ the user doesn't prefer reduced motion: a pause chosen with the button or spacebar sticks.
85
+ Focus landing on the control button doesn't auto-pause, so one click pauses. (Changed 2026-10-17:
86
+ this used to listen for `focus`/`blur` on the region only, gated on `respectReducedMotion`, so
87
+ tabbing to a link in an item left it moving and `aria-hidden`.)
78
88
  - Hovering anywhere over the scroller pauses the track and applies
79
89
  `--marquee-scroller-group-hover-filter` (default `grayscale(1)`) to every item; hovering an
80
90
  individual item restores it via `--marquee-scroller-item-hover-filter` (default `grayscale(0)`).
81
91
  - `aria-live` switches to `"polite"` while paused so screen readers announce the paused content.
82
- - The duplicate track group is always `aria-hidden="true"`.
92
+ - The track is `aria-hidden="true"` while playing (attribute removed when paused). The duplicate
93
+ group is always `aria-hidden="true"` and `inert`, so its copies of any links can't be focused.
94
+ - The control button is an `InputButton` (`variant="inline"`, icon-only): `playLabel`/`pauseLabel`
95
+ is its visually hidden text rather than an `aria-label`.
83
96
  - All user-visible copy (`ariaLabel`, `ariaDescription`, `playLabel`, `pauseLabel`) is a plain
84
97
  string prop with an English default — there's no i18n framework dependency in this library, so
85
98
  pass translated strings from the consumer's own i18n solution.
@@ -110,11 +123,11 @@ theme); only its border width is (`--marquee-scroller-control-border-width`).
110
123
 
111
124
  ## Seamless loop math
112
125
 
113
- The track duplicates `marqueeData` into two `.marquee-group` children so the loop can wrap without
126
+ The track duplicates `marqueeData` into two `.marquee-scroller-group` children so the loop can wrap without
114
127
  a visible cut. The animation must shift the track by exactly one group's width plus its connecting
115
128
  gap — not `translateX(-50%)`, which overshoots short of the seam once the track's own `gap`
116
129
  between the two groups is added to its total width. This is computed as a private
117
- `--_track-shift: calc(itemCount * (itemWidth + gap))` token and used in the `marqueeMove`
130
+ `--_track-shift: calc(itemCount * (itemWidth + gap))` token and used in the `marquee-scroller-move`
118
131
  keyframe. Fixed 2026-09-08 — the original `-50%` version produced a small but visible gap/overlap
119
132
  at the loop point once more than a couple of items were in `marqueeData`.
120
133
 
@@ -125,6 +138,30 @@ section on a wide viewport), the loop briefly shows empty space before snapping
125
138
  the visible track only spans as wide as the content, not the container. On mount (and on
126
139
  container resize, via `ResizeObserver`), the component measures one rendered group's `scrollWidth`
127
140
  against the root element's `offsetWidth` and repeats `marqueeData` internally (`repeatCount`)
128
- enough times per `.marquee-group` so a single group's width always covers the container. This is
141
+ enough times per `.marquee-scroller-group` so a single group's width always covers the container. This is
129
142
  transparent to consumers — it doesn't change `marqueeData` or any prop, only how many times each
130
143
  item's slot is rendered internally. Fixed 2026-09-08.
144
+
145
+ The repeat count is capped at 50 copies per group, so tiny items in a huge container can't flood
146
+ the DOM (added 2026-10-17).
147
+
148
+ ## Storybook
149
+
150
+ `Atoms/Effects/MarqueeScroller`: `WithControls`, `FocusableItems` (links in items, for the focus
151
+ behaviour), and `StressTest` (24 items of hostile copy, a broken image, long localised labels, a
152
+ single item, tiny items hitting the repeat cap, and empty data). Stories use the `CanvasSwitcher`
153
+ decorator.
154
+
155
+ ## History
156
+
157
+ **2026-10-17** (stress-test pass, breaking class renames):
158
+
159
+ - Structural classes prefixed (`.marquee-scroller-item`, `-control`, `-track`, `-group`,
160
+ `-description`); bare `.reverse`/`.paused`/`.reduced-motion` became `data-*` attributes; keyframes
161
+ renamed `marquee-scroller-move`. See CONSUMER-STYLING.md for the full map.
162
+ - Raw `<button>` replaced with `InputButton variant="inline"` (icon-only slot).
163
+ - Focus-pausing reworked (focusin/focusout, see Accessibility behaviour); duplicate group `inert`.
164
+ - `prefers-reduced-motion` change listener is now removed on unmount.
165
+ - New `--marquee-scroller-focus-outline-colour` token (was bare `--theme-ring`).
166
+ - Added `styleClassPassthrough`. Removed the redundant `aspect-ratio: 1 / 1` on items (width and
167
+ height are both set, and it mis-centres on old Safari). Items wrap unbroken text.
@@ -3,7 +3,7 @@
3
3
  "description": "FormField layout wrapper around a single form control",
4
4
  "scope": "vue,html",
5
5
  "body": [
6
- "<FormField width=\"${1|wide,medium,narrow|}\" :has-gutter=\"${2|false,true|}\" :field-has-error=\"!!$3errors.fieldName\">",
6
+ "<FormField width=\"${1|wide,medium,narrow|}\" :has-gutter=\"${2|false,true|}\" :field-has-error=\"!!${3:errors.fieldName}\">",
7
7
  " $0",
8
8
  "</FormField>"
9
9
  ]
@@ -12,12 +12,13 @@
12
12
  "description": "CSS override scaffold for FormField tokens",
13
13
  "scope": "css",
14
14
  "body": [
15
- ".$1my-page {",
15
+ ".${1:my-page} {",
16
16
  " .form-field {",
17
- " --form-field-max-width-narrow: $2;",
18
- " --form-field-gutter-width: $3;",
19
- " --form-field-margin-block: $4;",
20
- " --form-field-background-color: $5;",
17
+ " --form-field-max-width-narrow: ${2:400px};",
18
+ " --form-field-gutter-width: ${3:1.6rem};",
19
+ " --form-field-margin-block: ${4:0 1.6rem};",
20
+ " --form-field-background-color: ${5:transparent};",
21
+ " --form-field-border-radius: ${6:0.4rem};",
21
22
  " }",
22
23
  "}"
23
24
  ]
@@ -4,11 +4,11 @@
4
4
  "scope": "vue,html",
5
5
  "body": [
6
6
  "<FormFieldset",
7
- " id=\"$1fieldId\"",
8
- " name=\"$2fieldName\"",
9
- " legend=\"$3Legend\"",
7
+ " id=\"${1:fieldId}\"",
8
+ " name=\"${2:fieldName}\"",
9
+ " legend=\"${3:Legend}\"",
10
10
  " group-role=\"${4|group,radiogroup|}\"",
11
- " :field-has-error=\"!!$5errors.fieldName\"",
11
+ " :field-has-error=\"!!${5:errors.fieldName}\"",
12
12
  ">",
13
13
  " <template #content>",
14
14
  " $0",
@@ -20,15 +20,15 @@
20
20
  "description": "CSS override scaffold for FormFieldset tokens",
21
21
  "scope": "css",
22
22
  "body": [
23
- ".$1my-page {",
23
+ ".${1:my-page} {",
24
24
  " .form-fieldset {",
25
- " --form-fieldset-padding-block: $6;",
26
- " --form-fieldset-padding-inline: $7;",
27
- " --form-fieldset-border: $8;",
28
- " --form-fieldset-legend-color: $2;",
29
- " --form-fieldset-legend-font-size: $3;",
30
- " --form-fieldset-legend-font-weight: $4;",
31
- " --form-fieldset-legend-margin-block: $5;",
25
+ " --form-fieldset-padding-block: ${2:0};",
26
+ " --form-fieldset-padding-inline: ${3:0};",
27
+ " --form-fieldset-border: ${4:0};",
28
+ " --form-fieldset-legend-color: ${5:inherit};",
29
+ " --form-fieldset-legend-font-size: ${6:var(--step-5)};",
30
+ " --form-fieldset-legend-font-weight: ${7:normal};",
31
+ " --form-fieldset-legend-margin-block: ${8:0.8rem};",
32
32
  " }",
33
33
  "}"
34
34
  ]
@@ -5,16 +5,17 @@
5
5
  "body": [
6
6
  "<MarqueeScroller",
7
7
  " :marquee-data=\"[",
8
- " { id: 1, content: '$1logo-a' },",
9
- " { id: 2, content: '$2logo-b' },",
8
+ " { id: 1, content: '${1:logo-a}' },",
9
+ " { id: 2, content: '${2:logo-b}' },",
10
10
  " ]\"",
11
11
  " :item-config=\"{ width: '${3:120px}', height: '${4:60px}', gap: '${5:24px}' }\"",
12
+ " :style-class-passthrough=\"[${6:}]\"",
12
13
  ">",
13
14
  " <template #1>",
14
- " $6",
15
+ " $7",
15
16
  " </template>",
16
17
  " <template #2>",
17
- " $7",
18
+ " $8",
18
19
  " </template>",
19
20
  "</MarqueeScroller>"
20
21
  ]
@@ -24,11 +25,11 @@
24
25
  "scope": "vue,html",
25
26
  "body": [
26
27
  "<MarqueeScroller",
27
- " :marquee-data=\"$1items\"",
28
+ " :marquee-data=\"${1:items}\"",
28
29
  " show-controls",
29
- " aria-label=\"$2Scrolling client logos\"",
30
+ " aria-label=\"${2:Scrolling client logos}\"",
30
31
  ">",
31
- " <template v-for=\"item in $1items\" #[item.id] :key=\"item.id\">",
32
+ " <template v-for=\"item in ${1:items}\" #[item.id] :key=\"item.id\">",
32
33
  " $3",
33
34
  " </template>",
34
35
  "</MarqueeScroller>"
@@ -39,14 +40,14 @@
39
40
  "scope": "vue,html",
40
41
  "body": [
41
42
  "<MarqueeScroller",
42
- " :marquee-data=\"$1items\"",
43
+ " :marquee-data=\"${1:items}\"",
43
44
  " show-controls",
44
45
  " play-icon=\"${2:mdi:play-circle}\"",
45
46
  " pause-icon=\"${3:mdi:pause-circle}\"",
46
47
  " play-label=\"${4:Play}\"",
47
48
  " pause-label=\"${5:Pause}\"",
48
49
  ">",
49
- " <template v-for=\"item in $1items\" #[item.id] :key=\"item.id\">",
50
+ " <template v-for=\"item in ${1:items}\" #[item.id] :key=\"item.id\">",
50
51
  " $6",
51
52
  " </template>",
52
53
  "</MarqueeScroller>"
@@ -56,7 +57,7 @@
56
57
  "description": "MarqueeScroller with the toggle-icon slot fully replacing the control button's icon",
57
58
  "scope": "vue,html",
58
59
  "body": [
59
- "<MarqueeScroller :marquee-data=\"$1items\" show-controls>",
60
+ "<MarqueeScroller :marquee-data=\"${1:items}\" show-controls>",
60
61
  " <template #toggle-icon=\"{ isPaused }\">",
61
62
  " $2",
62
63
  " </template>",
@@ -67,25 +68,27 @@
67
68
  "description": "CSS override scaffold for MarqueeScroller — scope to a page or section class",
68
69
  "scope": "css",
69
70
  "body": [
70
- ".$1my-page {",
71
+ ".${1:my-page} {",
71
72
  " .marquee-scroller {",
72
- " --marquee-scroller-fade-width: ;",
73
- " --marquee-scroller-focus-outline-width: ;",
74
- " --marquee-scroller-focus-outline-offset: ;",
75
- " --marquee-scroller-item-border-colour: ;",
76
- " --marquee-scroller-item-border-width: ;",
77
- " --marquee-scroller-item-border-radius: ;",
78
- " --marquee-scroller-item-transition-duration: ;",
79
- " --marquee-scroller-group-hover-filter: ;",
80
- " --marquee-scroller-item-hover-filter: ;",
81
- " --marquee-scroller-control-offset: ;",
82
- " --marquee-scroller-control-background-colour: ;",
83
- " --marquee-scroller-control-background-colour-hover: ;",
84
- " --marquee-scroller-control-text-colour: ;",
85
- " --marquee-scroller-control-border-radius: ;",
86
- " --marquee-scroller-control-border-width: ;",
87
- " --marquee-scroller-control-padding: ;",
88
- " --marquee-scroller-control-font-size: ;",
73
+ " --marquee-scroller-fade-width: ${2:10%};",
74
+ " --marquee-scroller-focus-outline-width: ${3:2px};",
75
+ " --marquee-scroller-focus-outline-colour: ${4:var(--theme-ring)};",
76
+ " --marquee-scroller-focus-outline-offset: ${5:2px};",
77
+ " --marquee-scroller-item-border-colour: ${6:var(--slate-10)};",
78
+ " --marquee-scroller-item-border-width: ${7:1px};",
79
+ " --marquee-scroller-item-border-radius: ${8:4px};",
80
+ " --marquee-scroller-item-transition-duration: ${9:0.5s};",
81
+ " --marquee-scroller-group-hover-filter: ${10:grayscale(1)};",
82
+ " --marquee-scroller-item-hover-filter: ${11:grayscale(0)};",
83
+ " --marquee-scroller-control-offset: ${12:8px};",
84
+ " --marquee-scroller-control-background-colour: ${13:rgba(0, 0, 0, 0.7)};",
85
+ " --marquee-scroller-control-background-colour-hover: ${14:rgba(0, 0, 0, 0.9)};",
86
+ " --marquee-scroller-control-text-colour: ${15:white};",
87
+ " --marquee-scroller-control-border-radius: ${16:4px};",
88
+ " --marquee-scroller-control-border-width: ${17:1px};",
89
+ " --marquee-scroller-control-padding: ${18:8px};",
90
+ " --marquee-scroller-control-font-size: ${19:14px};",
91
+ " --marquee-scroller-control-transition-duration: ${20:0.2s};",
89
92
  " }",
90
93
  "}"
91
94
  ]
@@ -2,17 +2,30 @@
2
2
 
3
3
  ## Public token API
4
4
 
5
+ ### Region
6
+
5
7
  | Token | Default | Controls |
6
8
  |---|---|---|
7
9
  | `--marquee-scroller-fade-width` | `10%` | Width of the transparent fade at each edge of the track (`mask-image`) |
8
- | `--marquee-scroller-focus-outline-width` | `2px` | Outline width on the root region and the control button when keyboard-focused (colour is `--theme-ring`) |
10
+ | `--marquee-scroller-focus-outline-width` | `2px` | Outline width on the root region and the control button when keyboard-focused |
11
+ | `--marquee-scroller-focus-outline-colour` | `var(--theme-ring)` | Outline colour on the same two focus states |
9
12
  | `--marquee-scroller-focus-outline-offset` | `2px` | Outline offset on the same two focus states |
13
+
14
+ ### Items
15
+
16
+ | Token | Default | Controls |
17
+ |---|---|---|
10
18
  | `--marquee-scroller-group-hover-filter` | `grayscale(1)` | Filter applied to every item when the pointer hovers anywhere over the scroller |
11
19
  | `--marquee-scroller-item-hover-filter` | `grayscale(0)` | Filter applied to a single item when it is individually hovered (overrides the group filter for that item) |
12
20
  | `--marquee-scroller-item-transition-duration` | `0.5s` | Transition duration for the item hover filter change |
13
21
  | `--marquee-scroller-item-border-width` | `1px` | Item border width |
14
22
  | `--marquee-scroller-item-border-colour` | `var(--slate-10)` | Item border colour |
15
23
  | `--marquee-scroller-item-border-radius` | `4px` | Item border radius |
24
+
25
+ ### Control button
26
+
27
+ | Token | Default | Controls |
28
+ |---|---|---|
16
29
  | `--marquee-scroller-control-offset` | `8px` | Distance of the pause/play button from the top and end edges |
17
30
  | `--marquee-scroller-control-background-colour` | `rgba(0, 0, 0, 0.7)` | Pause/play button background |
18
31
  | `--marquee-scroller-control-background-colour-hover` | `rgba(0, 0, 0, 0.9)` | Pause/play button background on hover |
@@ -21,15 +34,35 @@
21
34
  | `--marquee-scroller-control-padding` | `8px` | Pause/play button padding |
22
35
  | `--marquee-scroller-control-font-size` | `14px` | Pause/play button icon font size |
23
36
  | `--marquee-scroller-control-transition-duration` | `0.2s` | Pause/play button background-colour transition duration |
24
- | `--marquee-scroller-control-border-width` | `1px` | Pause/play button border width in forced-colors/high-contrast mode (`prefers-contrast: high`). The border colour itself stays the system `ButtonText` keyword — see note below. |
37
+ | `--marquee-scroller-control-border-width` | `1px` | Pause/play button border width under `prefers-contrast: high`. The border colour itself stays the system `ButtonText` keyword, see below. |
25
38
 
26
- ```css
27
- .my-page {
28
- --marquee-scroller-item-border-colour: #b5651d;
29
- --marquee-scroller-control-background-colour: #1a1a1a;
30
- --marquee-scroller-fade-width: 20%;
31
- }
32
- ```
39
+ Private tokens (not public API): `--_fade-width`, `--_focus-outline` (width + colour, shared by
40
+ both focus states) and `--_track-shift` (the loop distance, computed from `itemConfig`).
41
+
42
+ ---
43
+
44
+ ## State hooks
45
+
46
+ The root `.marquee-scroller` carries:
47
+
48
+ | Attribute | When |
49
+ |---|---|
50
+ | `data-paused` | The track is paused (control button, spacebar, focus inside, or reduced motion) |
51
+ | `data-reverse` | `reverse` is `true` |
52
+ | `data-reduced-motion` | The user prefers reduced motion and `respect-reduced-motion` is on |
53
+
54
+ Inner elements: `.marquee-scroller-track` (the animated row), `.marquee-scroller-group` (one copy
55
+ of the items; the second is `aria-hidden` and `inert`), `.marquee-scroller-item` (one fixed-size
56
+ item box), `.marquee-scroller-control` (the pause/play `InputButton`) and
57
+ `.marquee-scroller-description` (the visually hidden instructions).
58
+
59
+ > **Changed 2026-10-17**: the generic class names were renamed so they can't collide with consumer
60
+ > CSS. `.item` → `.marquee-scroller-item`, `.control-btn` → `.marquee-scroller-control` (now an
61
+ > `InputButton`, so it also has `.input-button`), `.marquee-track` → `.marquee-scroller-track`,
62
+ > `.marquee-group` → `.marquee-scroller-group`, `.sr-only` → `.marquee-scroller-description`. The
63
+ > bare state classes became attributes: `.reverse` → `[data-reverse]`, `.paused` →
64
+ > `[data-paused]`, `.reduced-motion` → `[data-reduced-motion]`. The keyframes are now
65
+ > `marquee-scroller-move` (was `marqueeMove`).
33
66
 
34
67
  ---
35
68
 
@@ -37,10 +70,9 @@
37
70
 
38
71
  Under `prefers-contrast: high`, the control button switches to the system colour keywords
39
72
  `ButtonFace`/`ButtonText` rather than the component's own background/text tokens. These aren't
40
- exposed as overridable tokens on purpose — they're meant to track the user's OS-level high-contrast
41
- theme, which a component-level override would defeat. Only the border *width* in that mode is a
42
- token (`--marquee-scroller-control-border-width`), since it's a plain dimension with no such
43
- constraint.
73
+ exposed as overridable tokens on purpose: they track the user's OS-level high-contrast theme, which
74
+ a component-level override would defeat. Only the border *width* in that mode is a token
75
+ (`--marquee-scroller-control-border-width`), since it's a plain dimension with no such constraint.
44
76
 
45
77
  ---
46
78
 
@@ -48,7 +80,7 @@ constraint.
48
80
 
49
81
  Item `width`/`height`/`gap` and the animation duration are set via the `itemConfig` and
50
82
  `animationRuntime` props (not CSS custom properties), since they also drive the component's own
51
- layout math (`aspect-ratio`, track width). Override those through props:
83
+ loop math (`--_track-shift`) and the repeat measurement. Override those through props:
52
84
 
53
85
  ```vue
54
86
  <MarqueeScroller
@@ -57,58 +89,49 @@ layout math (`aspect-ratio`, track width). Override those through props:
57
89
  />
58
90
  ```
59
91
 
60
- ---
92
+ Item boxes are a fixed size. Text in an item wraps (`overflow-wrap: anywhere`), but content taller
93
+ than `itemConfig.height` overflows, so size slot content to the box.
61
94
 
62
- ## Item content via slots
95
+ ---
63
96
 
64
- Each entry in `marqueeData` (`{ id, content }`) renders a slot named after its `id`. Provide the
65
- markup for each item via that dynamically-named slot:
97
+ ## Global theming
66
98
 
67
- ```vue
68
- <MarqueeScroller :marquee-data="[{ id: 1, content: 'logo-a' }, { id: 2, content: 'logo-b' }]">
69
- <template #1>
70
- <img src="/logos/a.svg" alt="Logo A" />
71
- </template>
72
- <template #2>
73
- <img src="/logos/b.svg" alt="Logo B" />
74
- </template>
75
- </MarqueeScroller>
99
+ ```css
100
+ :where(html) {
101
+ --marquee-scroller-fade-width: 15%;
102
+ --marquee-scroller-item-border-colour: var(--slate-04);
103
+ }
76
104
  ```
77
105
 
78
106
  ---
79
107
 
80
- ## Control button icon and copy
81
-
82
- The pause/play button's icon and its `aria-label` copy are all overridable — see `playIcon`/
83
- `pauseIcon`/`playLabel`/`pauseLabel` props, or the `toggle-icon` slot for a fully custom icon
84
- (scoped with `isPaused`):
108
+ ## Local overrides
85
109
 
86
- ```vue
87
- <MarqueeScroller
88
- show-controls
89
- play-icon="mdi:play-circle"
90
- pause-icon="mdi:pause-circle"
91
- play-label="Reproduire"
92
- pause-label="Suspendre"
93
- />
94
- ```
110
+ Set the tokens above on an element you own (a page or section class, or a class added with
111
+ `:style-class-passthrough`): they inherit down into the component. Keep the block **unlayered** (no
112
+ `@layer` wrapper) so it beats the library's `@layer components`. If your own file uses
113
+ `<style scoped>`, tokens set on your element still work, but selectors that reach inside the
114
+ component need `:deep()`. Patterns and examples: `.claude/skills/component-local-style-override.md`.
95
115
 
96
- ```vue
97
- <MarqueeScroller show-controls>
98
- <template #toggle-icon="{ isPaused }">
99
- <Icon :name="isPaused ? 'lucide:play' : 'lucide:pause'" />
100
- </template>
101
- </MarqueeScroller>
116
+ ```css
117
+ .partners-section {
118
+ --marquee-scroller-item-border-colour: #b5651d;
119
+ --marquee-scroller-control-background-colour: #1a1a1a;
120
+ --marquee-scroller-fade-width: 20%;
121
+ }
102
122
  ```
103
123
 
104
124
  ---
105
125
 
106
- ## Local overrides
126
+ ## Class passthrough
127
+
128
+ `:style-class-passthrough` (string or string array) adds classes to the root `.marquee-scroller`
129
+ region.
130
+
131
+ ---
107
132
 
108
- Set the tokens above on an element you own (a page or section class, or a plain `class` on the
109
- component (it falls through to the root element)): they inherit down into the component. Keep the
110
- block **unlayered** (no `@layer` wrapper) so it beats the library's `@layer components`. If your own
111
- file uses `<style scoped>`, tokens set on your element still work, but selectors that reach inside
112
- the component need `:deep()`. Patterns and examples:
113
- `.claude/skills/component-local-style-override.md`.
133
+ ## Notes
114
134
 
135
+ - Item content and the control button's icon and copy are covered in the skill doc
136
+ (`marquee-scroller.md`): one slot per `marqueeData` id, `toggle-icon` slot, and the
137
+ `playIcon`/`pauseIcon`/`playLabel`/`pauseLabel` props.