@nordwerk/scroll-carousel 0.1.1 → 0.1.3

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/AGENTS.md CHANGED
@@ -39,6 +39,18 @@ JavaScript, never move slides with transforms, never clone slides.
39
39
  9. Text: pass `labels` for dots and the live region in the page's language, and give the arrow
40
40
  buttons their own `aria-label`s ("Previous products", "Next products").
41
41
  10. Do not add `disabled` to the arrows yourself; the script manages `aria-disabled`.
42
+ 11. Tailwind CSS: `@import "@nordwerk/scroll-carousel/carousel.css" layer(components);`, layout as
43
+ arbitrary properties (`[--sc-per-view:2]`, `@3xl:[--sc-per-view:4]` on `.sc-track`), own
44
+ controls styled from the state attributes (`group-data-[sc-overflow]/row:grid`,
45
+ `aria-disabled:opacity-30`). Copyable example: the `#tailwind` section of the docs site.
46
+ 12. Next.js App Router: import `<Carousel>` from `@nordwerk/scroll-carousel/react` in a Server
47
+ Component as is; the entry carries `'use client'`.
48
+ 13. shadcn/ui projects: `npx shadcn@latest add studio-nordwerk/scroll-carousel/scroll-carousel`, or a
49
+ block (`product-row`, `brand-teasers`, `hero-autoplay`, `image-gallery`, `logo-belt`). Compose
50
+ `ScrollCarousel` > `ScrollCarouselContent` > `ScrollCarouselItem`, with `ScrollCarouselPrevious`,
51
+ `ScrollCarouselNext`, `ScrollCarouselDots` and `ScrollCarouselPlay` inside `ScrollCarousel`;
52
+ layout as custom properties on the content. Pass plugins made outside the component, and mark
53
+ the file `"use client"` when it creates them.
42
54
 
43
55
  ### Recipes (CSS on the root or track, options in attach)
44
56
 
@@ -77,11 +89,23 @@ JavaScript, never move slides with transforms, never clone slides.
77
89
  - `src/adapter.ts`: the React and Preact adapter, written once against a small `Framework`
78
90
  interface; `src/react.ts` and `src/preact.ts` only bind it.
79
91
  - `src/astro/Carousel.astro`: shipped as source.
80
- - `src/carousel.css`: all layout and the default controls.
92
+ - `src/carousel.css`: all layout and the default controls. The build also writes
93
+ `carousel.layer.css`, the same rules inside Tailwind's components layer.
94
+ - `registry.json` and `registry/`: the shadcn registry, read by the shadcn CLI straight from this
95
+ public repository (`studio-nordwerk/scroll-carousel/<item>`). `registry/ui` is the component,
96
+ `registry/blocks` the blocks, `registry/shims` stand-ins that only exist for the type check.
97
+ `node scripts/shadcn-smoke.mjs` installs everything into a fresh shadcn project, builds and
98
+ renders it; CI runs it on every push.
81
99
  - `site/`: the documentation site (`generate.mjs` writes `_site/`), including the wireframe
82
100
  patterns in `wireframes.mjs`. Content there is fictional. CI publishes it to GitHub Pages;
83
101
  www.nordwerk.studio/oss/scroll-carousel serves the same files through a proxy that rewrites
84
- `<base href>`, so every link in the site must stay relative to `<base>`.
102
+ `<base href>`, so every link in the site must stay relative to `<base>`. The proxy also puts the
103
+ studio site's own header and footer in place of `<!--nw:header-->` (first child of `.sheet`)
104
+ and `<!--nw:footer-->` (right after `.sheet`); keep both, keep `<html lang="en">` and `<body>`
105
+ free of other attributes, keep `data-hero` on the introduction, and never style the studio's
106
+ class names (band, pill, brand, brand-mark, links, link, cta, foot, grid, brand-col, tag, label,
107
+ base) or set `--wrap` in the site CSS. Locally and on GitHub Pages the site has no header or
108
+ footer.
85
109
  - `test/unit/`: node's test runner on the TypeScript sources.
86
110
  - `test/e2e/`: Playwright against `_site/` and the adapter fixtures from `scripts/fixtures.mjs`.
87
111
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.3 (2026-09-22)
4
+
5
+ - `carousel.layer.css`: the stylesheet inside Tailwind's components layer, with Tailwind's layer
6
+ order declared, so utilities override it and preflight never does, whatever the load order.
7
+ - shadcn/ui registry in the repository: the `scroll-carousel` component (the structure of shadcn's
8
+ Carousel, on native scrolling) and five blocks, `product-row`, `brand-teasers`,
9
+ `hero-autoplay`, `image-gallery` and `logo-belt`. Install with
10
+ `npx shadcn@latest add studio-nordwerk/scroll-carousel/<item>`.
11
+
12
+ ## 0.1.2 (2026-09-22)
13
+
14
+ - React entry marked `'use client'`: `<Carousel>` now works in Next.js App Router Server
15
+ Components (it failed to prerender before, as the hooks are not available on the server).
16
+ - Documentation: a copy-and-paste Tailwind CSS v4 example with a live preview compiled by
17
+ Tailwind, and notes for Tailwind and Next.js.
18
+
3
19
  ## 0.1.1 (2026-09-21)
4
20
 
5
21
  - Astro and `templateLabels`: `statusSingle` template for when only one slide is visible, so a
package/README.md CHANGED
@@ -102,6 +102,53 @@ import Carousel from '@nordwerk/scroll-carousel/astro';
102
102
  Slides go in the default slot, one element each. Drag and autoplay are loaded only on pages that
103
103
  use them. Labels take templates: `labels={{ page: 'Slide {n} of {count}' }}`.
104
104
 
105
+ ### Tailwind CSS
106
+
107
+ Import the stylesheet into the components layer, so utilities on your markup can override it, and
108
+ set the layout with arbitrary properties and container-query variants:
109
+
110
+ ```css
111
+ @import "tailwindcss";
112
+ @import "@nordwerk/scroll-carousel/carousel.css" layer(components);
113
+ ```
114
+
115
+ ```html
116
+ <div class="sc group/row relative [--sc-gap:1rem]">
117
+ <ul data-sc-track tabindex="0" aria-label="New arrivals"
118
+ class="sc-track [--sc-group:page] [--sc-per-view:1.3] @md:[--sc-per-view:2] @3xl:[--sc-per-view:4]">
119
+ <li>…</li>
120
+ </ul>
121
+ <button data-sc-next aria-label="Next products"
122
+ class="absolute end-3 top-1/3 hidden group-data-[sc-overflow]/row:grid aria-disabled:opacity-30">›</button>
123
+ </div>
124
+ ```
125
+
126
+ The complete example to copy, with a live preview compiled by Tailwind, is on the
127
+ [documentation site](https://www.nordwerk.studio/oss/scroll-carousel#tailwind).
128
+
129
+ ### shadcn/ui
130
+
131
+ A component with the structure of shadcn's Carousel, on native scrolling, and five blocks
132
+ (product row, brand teasers, hero with autoplay, image gallery, logo belt), straight from this
133
+ repository:
134
+
135
+ ```sh
136
+ npx shadcn@latest add studio-nordwerk/scroll-carousel/scroll-carousel
137
+ npx shadcn@latest add studio-nordwerk/scroll-carousel/product-row
138
+ ```
139
+
140
+ `ScrollCarousel`, `ScrollCarouselContent`, `ScrollCarouselItem`, `ScrollCarouselPrevious` and
141
+ `ScrollCarouselNext` match shadcn's names with the `Scroll` prefix, plus `ScrollCarouselDots`,
142
+ `ScrollCarouselPlay` and `useScrollCarousel()`. Size slides with `[--sc-per-view:3]` on the content
143
+ (or `basis-*` on items); spacing comes from `--sc-gap`, so the `-ml-4`/`pl-4` pair of the Embla
144
+ version is not needed. The component imports `@nordwerk/scroll-carousel/carousel.layer.css`, the
145
+ stylesheet inside Tailwind's components layer.
146
+
147
+ ### Next.js
148
+
149
+ The React entry is marked `'use client'`, so `<Carousel>` can be used directly in Server
150
+ Components of the App Router; it renders on the server and attaches in the browser.
151
+
105
152
  ## Markup
106
153
 
107
154
  | Element | Required | Purpose |
package/dist/carousel.css CHANGED
@@ -1,5 +1,5 @@
1
1
  /*
2
- * scroll-carousel, prototype stylesheet.
2
+ * scroll-carousel stylesheet.
3
3
  *
4
4
  * The layout is complete without JavaScript: the server-rendered markup is already the final
5
5
  * layout, and the script only adds controls. Everything is configured with custom properties,
@@ -0,0 +1,263 @@
1
+ @layer theme, base, components, utilities;
2
+
3
+ @layer components {
4
+ /*
5
+ * scroll-carousel stylesheet.
6
+ *
7
+ * The layout is complete without JavaScript: the server-rendered markup is already the final
8
+ * layout, and the script only adds controls. Everything is configured with custom properties,
9
+ * which can be set anywhere above the track and changed in media or container queries. The root
10
+ * is the query container, so inside `@container sc (...)` set them on .sc-track.
11
+ *
12
+ * --sc-per-view slides per view; a fraction shows part of the next slide. Default 1.
13
+ * --sc-slide-size a fixed slide size, or auto for the content width. Wins over per-view.
14
+ * --sc-gap space between slides. Default 1rem.
15
+ * --sc-offset-before space before the first slide; snapped slides line up with it. Default 0.
16
+ * --sc-offset-after space after the last slide. Default 0.
17
+ * --sc-snap mandatory, proximity or none. Default mandatory.
18
+ * --sc-align start or center. Default start.
19
+ * --sc-centered 1 pads both ends so the first and last slide can reach the centre.
20
+ * Derived from --sc-per-view, so it does not combine with --sc-slide-size.
21
+ * --sc-count the number of slides, when known; per-view never exceeds it.
22
+ * --sc-group slides per step of next and previous: a number, or page. Read by the script.
23
+ * --sc-controls none hides the arrows, dots and play button, e.g. for free mode on phones.
24
+ *
25
+ * Theme, all optional: --sc-control-bg, --sc-control-fg, --sc-control-border,
26
+ * --sc-control-size, --sc-control-radius, --sc-control-shadow, --sc-nav-top, --sc-nav-inset,
27
+ * --sc-dot, --sc-dot-size, --sc-dot-height, --sc-dot-active-size, --sc-dot-radius, --sc-dot-idle,
28
+ * --sc-focus.
29
+ */
30
+
31
+ .sc {
32
+ container: sc / inline-size;
33
+ position: relative;
34
+ }
35
+
36
+ .sc-track {
37
+ --_n: min(var(--sc-per-view, 1), var(--sc-count, 1000));
38
+ --_gap: var(--sc-gap, 1rem);
39
+ --_size: calc(
40
+ (100cqi - var(--sc-offset-before, 0px) - var(--sc-offset-after, 0px) - (var(--_n) - 1) * var(--_gap)) /
41
+ var(--_n)
42
+ );
43
+ --_edge: calc((100cqi - var(--_size)) / 2 * var(--sc-centered, 0));
44
+ /* Also the containing block for anything absolutely positioned inside the slides, which the
45
+ scroller then clips instead of letting it widen the page. */
46
+ position: relative;
47
+ display: flex;
48
+ gap: var(--_gap);
49
+ margin: 0;
50
+ padding-block: 0;
51
+ padding-inline: calc(var(--sc-offset-before, 0px) + var(--_edge)) calc(var(--sc-offset-after, 0px) + var(--_edge));
52
+ list-style: none;
53
+ overflow-x: auto;
54
+ overflow-y: hidden;
55
+ overscroll-behavior-x: contain;
56
+ /* `x none` is invalid, which makes the property fall back to its initial value, none. */
57
+ scroll-snap-type: x var(--sc-snap, mandatory);
58
+ scroll-padding-inline: var(--sc-offset-before, 0px) var(--sc-offset-after, 0px);
59
+ scrollbar-width: none;
60
+ transition: opacity 0.16s ease;
61
+ }
62
+
63
+ .sc-track::-webkit-scrollbar {
64
+ display: none;
65
+ }
66
+
67
+ /* Zero specificity, so a host can size its own slides without a fight. */
68
+ :where(.sc-track > *) {
69
+ flex: 0 0 var(--sc-slide-size, var(--_size));
70
+ min-width: 0;
71
+ scroll-snap-align: var(--sc-align, start);
72
+ }
73
+
74
+ :where(.sc-track > [data-sc-snap-off]) {
75
+ scroll-snap-align: none;
76
+ }
77
+
78
+ /* A row too short to scroll sits in the middle; auto margins collapse to 0 once it overflows. */
79
+ .sc--center-few :where(.sc-track > :first-child) {
80
+ margin-inline-start: auto;
81
+ }
82
+
83
+ .sc--center-few :where(.sc-track > :last-child) {
84
+ margin-inline-end: auto;
85
+ }
86
+
87
+ .sc-track:focus-visible {
88
+ outline: 2px solid var(--sc-focus, Highlight);
89
+ outline-offset: -2px;
90
+ }
91
+
92
+ .sc[data-sc-drag][data-sc-overflow] .sc-track {
93
+ cursor: grab;
94
+ }
95
+
96
+ .sc[data-sc-drag] .sc-track :is(img, a) {
97
+ -webkit-user-drag: none;
98
+ }
99
+
100
+ .sc-track[data-sc-dragging] {
101
+ cursor: grabbing;
102
+ scroll-snap-type: none;
103
+ -webkit-user-select: none;
104
+ user-select: none;
105
+ }
106
+
107
+ .sc-track[data-sc-fading] {
108
+ opacity: 0;
109
+ }
110
+
111
+ /* Default controls. All optional: a host can bring its own and bind them to the API. */
112
+
113
+ .sc-nav,
114
+ .sc-play {
115
+ display: var(--sc-controls, grid);
116
+ place-items: center;
117
+ box-sizing: border-box;
118
+ inline-size: var(--sc-control-size, 2.75rem);
119
+ block-size: var(--sc-control-size, 2.75rem);
120
+ padding: 0;
121
+ border: 1px solid var(--sc-control-border, color-mix(in srgb, currentColor 22%, transparent));
122
+ border-radius: var(--sc-control-radius, 50%);
123
+ background: var(--sc-control-bg, Canvas);
124
+ box-shadow: var(--sc-control-shadow, none);
125
+ color: var(--sc-control-fg, CanvasText);
126
+ font: inherit;
127
+ cursor: pointer;
128
+ }
129
+
130
+ .sc-nav {
131
+ position: absolute;
132
+ z-index: 1;
133
+ inset-block-start: var(--sc-nav-top, 50%);
134
+ translate: 0 -50%;
135
+ }
136
+
137
+ .sc-prev {
138
+ inset-inline-start: var(--sc-nav-inset, 0.5rem);
139
+ }
140
+
141
+ .sc-next {
142
+ inset-inline-end: var(--sc-nav-inset, 0.5rem);
143
+ }
144
+
145
+ .sc-nav svg,
146
+ .sc-play svg {
147
+ inline-size: 1.25rem;
148
+ block-size: 1.25rem;
149
+ fill: none;
150
+ stroke: currentColor;
151
+ stroke-width: 2;
152
+ stroke-linecap: round;
153
+ stroke-linejoin: round;
154
+ }
155
+
156
+ .sc-nav:dir(rtl) svg {
157
+ scale: -1 1;
158
+ }
159
+
160
+ .sc-nav[aria-disabled='true'] {
161
+ opacity: 0.35;
162
+ cursor: default;
163
+ }
164
+
165
+ .sc-play .sc-icon-pause,
166
+ .sc[data-sc-playing] .sc-play .sc-icon-play {
167
+ display: none;
168
+ }
169
+
170
+ .sc[data-sc-playing] .sc-play .sc-icon-pause {
171
+ display: block;
172
+ }
173
+
174
+ .sc-dots {
175
+ display: var(--sc-controls, flex);
176
+ flex-wrap: wrap;
177
+ justify-content: center;
178
+ min-block-size: 1.5rem;
179
+ }
180
+
181
+ .sc-dot {
182
+ display: grid;
183
+ place-items: center;
184
+ inline-size: 1.5rem;
185
+ block-size: 1.5rem;
186
+ padding: 0;
187
+ border: 0;
188
+ background: none;
189
+ color: var(--sc-dot, currentColor);
190
+ cursor: pointer;
191
+ }
192
+
193
+ .sc-dot::before {
194
+ content: '';
195
+ inline-size: var(--sc-dot-size, 0.5rem);
196
+ block-size: var(--sc-dot-height, var(--sc-dot-size, 0.5rem));
197
+ border-radius: var(--sc-dot-radius, 1rem);
198
+ background: currentColor;
199
+ opacity: var(--sc-dot-idle, 0.35);
200
+ transition: inline-size 0.2s ease, opacity 0.2s ease;
201
+ }
202
+
203
+ .sc-dot[aria-current]::before {
204
+ inline-size: var(--sc-dot-active-size, 1.25rem);
205
+ opacity: 1;
206
+ }
207
+
208
+ /* While autoplay runs, the current dot fills up until the next move. */
209
+ .sc[data-sc-playing] .sc-dot[aria-current]::before {
210
+ background:
211
+ linear-gradient(currentColor, currentColor) 0 0 / 0 100% no-repeat,
212
+ color-mix(in srgb, currentColor 35%, transparent);
213
+ animation: sc-progress var(--sc-autoplay-delay, 5s) linear forwards;
214
+ }
215
+
216
+ .sc[data-sc-playing] .sc-dot[aria-current]:dir(rtl)::before {
217
+ background-position: 100% 0;
218
+ }
219
+
220
+ .sc[data-sc-paused] .sc-dot::before {
221
+ animation-play-state: paused;
222
+ }
223
+
224
+ @keyframes sc-progress {
225
+ to {
226
+ background-size: 100% 100%;
227
+ }
228
+ }
229
+
230
+ :is(.sc-nav, .sc-play, .sc-dot):focus-visible {
231
+ outline: 2px solid var(--sc-focus, Highlight);
232
+ outline-offset: 2px;
233
+ }
234
+
235
+ /* Without the script, or without overflow, controls are absent: never focusable and dead.
236
+ Arrows float over the track and can simply go; dots and the play button keep their space,
237
+ so attaching the script cannot shift the layout around them. */
238
+ .sc:not([data-sc-ready][data-sc-overflow]) .sc-nav {
239
+ display: none;
240
+ }
241
+
242
+ .sc:not([data-sc-ready][data-sc-overflow]) :is(.sc-dots, .sc-play) {
243
+ visibility: hidden;
244
+ }
245
+
246
+ .sc-status,
247
+ .sc-skip:not(:focus) {
248
+ position: absolute;
249
+ inline-size: 1px;
250
+ block-size: 1px;
251
+ overflow: hidden;
252
+ clip-path: inset(50%);
253
+ white-space: nowrap;
254
+ }
255
+
256
+ @media (prefers-reduced-motion: reduce) {
257
+ .sc-track,
258
+ .sc-dot::before {
259
+ transition: none;
260
+ }
261
+ }
262
+
263
+ }
package/dist/react.js CHANGED
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import {
2
3
  createAdapter
3
4
  } from "./shared/chunk-I3QEUR7L.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nordwerk/scroll-carousel",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "A dependency-free carousel on native scrolling: CSS scroll snap first, arrows, dots, paging and an API as progressive enhancement.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -49,6 +49,7 @@
49
49
  },
50
50
  "./astro": "./src/astro/Carousel.astro",
51
51
  "./carousel.css": "./dist/carousel.css",
52
+ "./carousel.layer.css": "./dist/carousel.layer.css",
52
53
  "./package.json": "./package.json"
53
54
  },
54
55
  "files": [
@@ -63,7 +64,7 @@
63
64
  },
64
65
  "scripts": {
65
66
  "build": "node scripts/build.mjs",
66
- "typecheck": "tsc -p tsconfig.json --noEmit",
67
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p registry/tsconfig.json",
67
68
  "test": "node --test \"test/unit/*.test.ts\"",
68
69
  "size": "node scripts/size.mjs",
69
70
  "site": "node scripts/build.mjs && node site/generate.mjs",
@@ -89,6 +90,7 @@
89
90
  "@axe-core/playwright": "4.13.0",
90
91
  "@fontsource-variable/geist": "^5.3.0",
91
92
  "@playwright/test": "1.61.1",
93
+ "@tailwindcss/cli": "4.3.3",
92
94
  "@types/node": "^24.0.0",
93
95
  "@types/react": "^19.3.0",
94
96
  "@types/react-dom": "^19.3.0",
@@ -98,6 +100,8 @@
98
100
  "preact-render-to-string": "6.7.0",
99
101
  "react": "^19.3.0",
100
102
  "react-dom": "^19.3.0",
103
+ "shadcn": "4.21.0",
104
+ "tailwindcss": "4.3.3",
101
105
  "typescript": "^7.0.2"
102
106
  },
103
107
  "engines": {