@mitchellslager/carousel 0.0.0 → 0.1.0
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/LICENSE +21 -0
- package/README.md +121 -0
- package/package.json +10 -10
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mitchell Slager
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# @mitchellslager/carousel
|
|
2
|
+
|
|
3
|
+
A headless, accessible image carousel for React 19, driven by native CSS scroll snapping.
|
|
4
|
+
|
|
5
|
+
- Thumbnail navigation, synchronised with the carousel
|
|
6
|
+
- A "3 / 10" counter
|
|
7
|
+
- The WAI-ARIA carousel pattern, with translatable labels
|
|
8
|
+
- Respects `prefers-reduced-motion`
|
|
9
|
+
|
|
10
|
+
[Demo](https://mitchellslager.github.io/carousel/) · [Design decisions](https://github.com/mitchellslager/carousel/blob/main/DESIGN.md)
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pnpm add @mitchellslager/carousel
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
import { Carousel } from "@mitchellslager/carousel"
|
|
20
|
+
import "@mitchellslager/carousel/styles.css"
|
|
21
|
+
|
|
22
|
+
export function ProductGallery({
|
|
23
|
+
images,
|
|
24
|
+
}: {
|
|
25
|
+
images: { src: string; alt: string }[]
|
|
26
|
+
}) {
|
|
27
|
+
return (
|
|
28
|
+
<Carousel.Root aria-label="Product images">
|
|
29
|
+
<Carousel.Viewport>
|
|
30
|
+
{images.map((image) => (
|
|
31
|
+
<Carousel.Slide key={image.src}>
|
|
32
|
+
<img src={image.src} alt={image.alt} />
|
|
33
|
+
</Carousel.Slide>
|
|
34
|
+
))}
|
|
35
|
+
</Carousel.Viewport>
|
|
36
|
+
<Carousel.Prev>←</Carousel.Prev>
|
|
37
|
+
<Carousel.Counter />
|
|
38
|
+
<Carousel.Next>→</Carousel.Next>
|
|
39
|
+
<Carousel.Thumbnails>
|
|
40
|
+
{images.map((image, index) => (
|
|
41
|
+
<Carousel.Thumbnail key={image.src} index={index}>
|
|
42
|
+
<img src={image.src} alt="" />
|
|
43
|
+
</Carousel.Thumbnail>
|
|
44
|
+
))}
|
|
45
|
+
</Carousel.Thumbnails>
|
|
46
|
+
</Carousel.Root>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Give `Root` an accessible name with `aria-label` or `aria-labelledby`. Every part is optional and can be placed anywhere inside `Root`.
|
|
52
|
+
|
|
53
|
+
### Parts
|
|
54
|
+
|
|
55
|
+
| Part | Renders | Notes |
|
|
56
|
+
| -------------- | -------- | ---------------------------------------------------------------- |
|
|
57
|
+
| `Root` | `div` | `index` / `defaultIndex` / `onIndexChange`, `labels`, `motion`. |
|
|
58
|
+
| `Viewport` | `div` | The scroll-snap container. Its `Slide` children are the slides. |
|
|
59
|
+
| `Slide` | `div` | `inert` unless active. `data-active` when active. |
|
|
60
|
+
| `Prev`, `Next` | `button` | `aria-disabled` and `data-disabled` at the edges. `render` prop. |
|
|
61
|
+
| `Counter` | `span` | "3 / 10" by default; pass a function child to change it. |
|
|
62
|
+
| `Thumbnails` | `div` | Tab list; `orientation` is `"horizontal"` or `"vertical"`. |
|
|
63
|
+
| `Thumbnail` | `button` | Tab for slide `index`. `data-active` when active. `render` prop. |
|
|
64
|
+
|
|
65
|
+
### State
|
|
66
|
+
|
|
67
|
+
`onIndexChange` fires once scrolling settles on a new slide. Pass `index` to request a slide from outside: changing it scrolls there, but a swipe always wins and reports itself through `onIndexChange`. To reset the carousel, for example when a product variant changes its images, give `Root` a new `key`.
|
|
68
|
+
|
|
69
|
+
### Labels
|
|
70
|
+
|
|
71
|
+
All accessible text is English by default. Translate it with `labels`:
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
<Carousel.Root
|
|
75
|
+
aria-label="Productafbeeldingen"
|
|
76
|
+
labels={{
|
|
77
|
+
carousel: "carrousel",
|
|
78
|
+
slideLabel: (index, total) => `${index + 1} van ${total}`,
|
|
79
|
+
previous: "Vorige afbeelding",
|
|
80
|
+
next: "Volgende afbeelding",
|
|
81
|
+
thumbnails: "Afbeeldingen",
|
|
82
|
+
thumbnailLabel: (index) => `Afbeelding ${index + 1}`,
|
|
83
|
+
announcement: (index, total) => `Afbeelding ${index + 1} van ${total}`,
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Motion
|
|
89
|
+
|
|
90
|
+
The carousel scrolls smoothly, and jumps instead when the user prefers reduced motion. Pass `motion="reduce"` to always jump.
|
|
91
|
+
|
|
92
|
+
### Custom elements
|
|
93
|
+
|
|
94
|
+
`Prev`, `Next` and `Thumbnail` take a `render` prop to render as your own component. Props are merged: class names are joined, and your event handlers run first (call `event.preventDefault()` to skip the carousel's own).
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
<Carousel.Next render={<Button variant="ghost" />}>Next</Carousel.Next>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Styling
|
|
101
|
+
|
|
102
|
+
Import the base styles once:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import "@mitchellslager/carousel/styles.css"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
They only handle layout, snapping and reduced motion, inside `@layer carousel`. Unlayered CSS of your own always wins. If you use Tailwind or other cascade layers, declare the carousel layer first so your utilities override it:
|
|
109
|
+
|
|
110
|
+
```css
|
|
111
|
+
@layer carousel;
|
|
112
|
+
@import "tailwindcss";
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Contributing
|
|
116
|
+
|
|
117
|
+
Development happens in the [GitHub repository](https://github.com/mitchellslager/carousel).
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mitchellslager/carousel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "A headless, accessible image carousel for React, driven by native CSS scroll snapping.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Mitchell Slager",
|
|
@@ -42,14 +42,6 @@
|
|
|
42
42
|
"access": "public",
|
|
43
43
|
"provenance": true
|
|
44
44
|
},
|
|
45
|
-
"scripts": {
|
|
46
|
-
"build": "tsdown",
|
|
47
|
-
"test": "vitest run",
|
|
48
|
-
"test:watch": "vitest",
|
|
49
|
-
"typecheck": "tsc",
|
|
50
|
-
"check:package": "publint --strict --pack npm && attw --pack . --profile esm-only --exclude-entrypoints styles.css",
|
|
51
|
-
"size": "size-limit"
|
|
52
|
-
},
|
|
53
45
|
"peerDependencies": {
|
|
54
46
|
"react": ">=19",
|
|
55
47
|
"react-dom": ">=19"
|
|
@@ -76,5 +68,13 @@
|
|
|
76
68
|
"tsdown": "^0.23.5",
|
|
77
69
|
"vitest": "^5.1.0",
|
|
78
70
|
"vitest-browser-react": "^2.3.0"
|
|
71
|
+
},
|
|
72
|
+
"scripts": {
|
|
73
|
+
"build": "tsdown",
|
|
74
|
+
"test": "vitest run",
|
|
75
|
+
"test:watch": "vitest",
|
|
76
|
+
"typecheck": "tsc",
|
|
77
|
+
"check:package": "publint --strict --pack npm && attw --pack . --profile esm-only --exclude-entrypoints styles.css",
|
|
78
|
+
"size": "size-limit"
|
|
79
79
|
}
|
|
80
|
-
}
|
|
80
|
+
}
|