@present-day/carousel 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Present Day
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,171 @@
1
+ # `@present-day/carousel`
2
+
3
+ Headless React carousel built on native CSS scroll snap. The browser owns the physics; the library owns state, navigation, and accessibility.
4
+
5
+ ## Why this and not Swiper/Embla?
6
+
7
+ - **Native physics, zero JS in the scroll path.** Swiper/Embla reimplement momentum/snapping in JavaScript; this library lets CSS scroll snap do it, so swipes feel exactly like the platform because they ARE the platform.
8
+ - **Built on the 2026 primitives.** Active-slide state comes from the browser's own `scrollsnapchanging`/`scrollsnapchange` events (Chrome/Edge 129+, Safari 18.2+) with a tiny fallback — not from reverse-engineered scroll math as the primary path.
9
+ - **Tiny.** Zero runtime dependencies; a thin state/a11y layer over what the browser already does.
10
+ - **Headless.** No bundled CSS, no theme to fight — style everything via `classNames`, including fully custom markers via `renderMarker`.
11
+ - **Accessible by default.** WAI-ARIA carousel pattern: labeled slides, marker tablist with roving tabindex and keyboard nav, honest `prefers-reduced-motion` handling.
12
+
13
+ No infinite looping or autoplay. Looping requires scroll-teleport hacks to fake an infinite track; autoplay just needs a timer, but it's out of scope to keep this library tiny and headless. If you need either, use Embla.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ bun add @present-day/carousel
19
+ # or
20
+ npm i @present-day/carousel
21
+ ```
22
+
23
+ Peer dependencies: `react`, `react-dom` (v18 or v19).
24
+
25
+ ## Basic usage
26
+
27
+ ```tsx
28
+ import { useState } from 'react'
29
+ import { Carousel } from '@present-day/carousel'
30
+
31
+ const [current, setCurrent] = useState(0)
32
+
33
+ <Carousel aria-label="Featured products" onSlideChange={setCurrent}>
34
+ <Carousel.Slide className="slide">1</Carousel.Slide>
35
+ <Carousel.Slide className="slide">2</Carousel.Slide>
36
+ <Carousel.Slide className="slide">3</Carousel.Slide>
37
+ <Carousel.Markers />
38
+ <Carousel.Prev>←</Carousel.Prev>
39
+ <Carousel.Next>→</Carousel.Next>
40
+ </Carousel>
41
+ ```
42
+
43
+ `Carousel.Markers`, `Carousel.Prev`, and `Carousel.Next` are optional — mix in whichever controls you need, in any order, as siblings of the slides.
44
+
45
+ ## Slide sizing
46
+
47
+ Slides size themselves via CSS — the library never sets widths. `width: 100%` on your slide class gives one slide per page; `width: 50%` gives two.
48
+
49
+ ```css
50
+ .slide { width: 100%; } /* one per page */
51
+ .slide-half { width: 50%; } /* two per page */
52
+ ```
53
+
54
+ ## Props
55
+
56
+ ### `Carousel`
57
+
58
+ | Prop | Type | Default | Description |
59
+ | ---- | ---- | ------- | ----------- |
60
+ | `children` | `ReactNode` | — | `Carousel.Slide` children plus any controls (`Carousel.Markers`, etc.) |
61
+ | `defaultSlide` | `number` | `0` | Initial slide when uncontrolled |
62
+ | `slide` | `number` | — | Controlled active slide index |
63
+ | `onSlideChange` | `(index: number) => void` | — | Fires when the settled slide changes (after snap completes) |
64
+ | `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Scroll axis |
65
+ | `align` | `'start' \| 'center'` | `'start'` | `scroll-snap-align` of slides |
66
+ | `classNames` | `{ root?, track?, slide? }` | — | Class names for the root, track, and slides |
67
+ | `aria-label` | `string` | — | Accessible name for the carousel region |
68
+ | `className` | `string` | — | Added to the root element |
69
+ | `style` | `CSSProperties` | — | Added to the root element |
70
+
71
+ ### `Carousel.Slide`
72
+
73
+ | Prop | Type | Default | Description |
74
+ | ---- | ---- | ------- | ----------- |
75
+ | `children` | `ReactNode` | — | Slide content |
76
+ | `className` | `string` | — | Added to the slide element |
77
+ | `style` | `CSSProperties` | — | Added to the slide element |
78
+
79
+ ### `Carousel.Markers`
80
+
81
+ | Prop | Type | Default | Description |
82
+ | ---- | ---- | ------- | ----------- |
83
+ | `classNames` | `{ list?, marker?, markerActive? }` | — | Class names for the tablist, each marker, and the active marker |
84
+ | `renderMarker` | `(index: number, active: boolean) => ReactNode` | — | Fully custom marker content |
85
+ | `aria-label` | `string` | `'Slides'` | Accessible name for the marker tablist |
86
+
87
+ Renders nothing when there's one slide or fewer.
88
+
89
+ ### `Carousel.Prev` / `Carousel.Next`
90
+
91
+ | Prop | Type | Default | Description |
92
+ | ---- | ---- | ------- | ----------- |
93
+ | `children` | `ReactNode` | — | Button content |
94
+ | `className` | `string` | — | Added to the button |
95
+ | `style` | `CSSProperties` | — | Added to the button |
96
+ | `aria-label` | `string` | `'Previous slide'` / `'Next slide'` | Accessible name |
97
+
98
+ Both buttons `disabled` at their respective end of the carousel.
99
+
100
+ ## Vertical orientation
101
+
102
+ ```tsx
103
+ <Carousel
104
+ aria-label="Story"
105
+ orientation="vertical"
106
+ classNames={{ track: 'vertical-track' }}
107
+ >
108
+ <Carousel.Slide className="slide">1</Carousel.Slide>
109
+ <Carousel.Slide className="slide">2</Carousel.Slide>
110
+ </Carousel>
111
+ ```
112
+
113
+ ```css
114
+ .vertical-track { height: 240px; } /* required: constrains the scroll axis */
115
+ ```
116
+
117
+ `classNames.track` must constrain the track's height, or there's nothing to scroll within. The track also sets `overscroll-behavior` on its scroll axis so a vertical carousel doesn't fight the page for scroll.
118
+
119
+ ## How it works
120
+
121
+ Three tiers of progressive enhancement, each falling back gracefully:
122
+
123
+ | Tier | Mechanism | Support |
124
+ | ---- | --------- | ------- |
125
+ | Physics | CSS scroll snap (`scroll-snap-type`, `scroll-snap-align`) | All browsers |
126
+ | State | `scrollsnapchanging` / `scrollsnapchange` events | Chrome/Edge 129+, Safari 18.2+ — rAF scroll-math fallback elsewhere (Firefox, older Safari) |
127
+ | Navigation | `scrollTo({ behavior: 'smooth' })` | All browsers — instant under `prefers-reduced-motion` |
128
+
129
+ ## Constraints
130
+
131
+ `Carousel.Slide` must be a direct child of `Carousel`. Mapping an array of slides is fine; wrapping slides in your own components or fragments is not supported — the root partitions children by `child.type === Slide`.
132
+
133
+ ## Zero-JS dots
134
+
135
+ Supporting browsers can skip `Carousel.Markers` entirely and render dots with pure CSS:
136
+
137
+ ```css
138
+ /* Chrome/Edge 135+ — no React markers needed. `::scroll-marker` support is
139
+ still landing across browsers; check current support before relying on it
140
+ as your only navigation (caniuse: css-scroll-marker). */
141
+ .my-track {
142
+ scroll-marker-group: after;
143
+ }
144
+ .my-track::scroll-marker-group {
145
+ display: flex;
146
+ gap: 8px;
147
+ justify-content: center;
148
+ }
149
+ .my-slide::scroll-marker {
150
+ content: '';
151
+ width: 10px;
152
+ height: 10px;
153
+ border-radius: 50%;
154
+ background: #ccc;
155
+ }
156
+ .my-slide::scroll-marker:target-current {
157
+ background: #333;
158
+ }
159
+ ```
160
+
161
+ ## Styling hooks
162
+
163
+ The root element carries `data-active-slide={index}` at all times, so you can target the active state from CSS without touching React:
164
+
165
+ ```css
166
+ [data-active-slide='0'] .progress { width: 20%; }
167
+ ```
168
+
169
+ ## License
170
+
171
+ MIT
@@ -0,0 +1,77 @@
1
+ import * as react from 'react';
2
+ import { CSSProperties, ReactNode } from 'react';
3
+
4
+ type CarouselButtonProps = {
5
+ className?: string;
6
+ style?: CSSProperties;
7
+ children?: ReactNode;
8
+ 'aria-label'?: string;
9
+ };
10
+ declare function Prev({ className, style, children, 'aria-label': ariaLabel, }: CarouselButtonProps): react.JSX.Element;
11
+ declare function Next({ className, style, children, 'aria-label': ariaLabel, }: CarouselButtonProps): react.JSX.Element;
12
+
13
+ declare const CAROUSEL_ORIENTATION: {
14
+ readonly horizontal: "horizontal";
15
+ readonly vertical: "vertical";
16
+ };
17
+ type CarouselOrientation = (typeof CAROUSEL_ORIENTATION)[keyof typeof CAROUSEL_ORIENTATION];
18
+ declare const CAROUSEL_ALIGN: {
19
+ readonly start: "start";
20
+ readonly center: "center";
21
+ };
22
+ type CarouselAlign = (typeof CAROUSEL_ALIGN)[keyof typeof CAROUSEL_ALIGN];
23
+ type CarouselClassNames = {
24
+ root?: string;
25
+ track?: string;
26
+ slide?: string;
27
+ };
28
+ type CarouselMarkersClassNames = {
29
+ list?: string;
30
+ marker?: string;
31
+ markerActive?: string;
32
+ };
33
+ type CarouselProps = {
34
+ /** `Carousel.Slide` children plus any controls (`Carousel.Markers`, etc.). */
35
+ children: ReactNode;
36
+ /** Initial slide when uncontrolled. @default 0 */
37
+ defaultSlide?: number;
38
+ /** Controlled active slide index. */
39
+ slide?: number;
40
+ /** Fires when the settled slide changes (after snap completes). */
41
+ onSlideChange?: (index: number) => void;
42
+ /** Scroll axis. @default 'horizontal' */
43
+ orientation?: CarouselOrientation;
44
+ /** scroll-snap-align of slides. @default 'start' */
45
+ align?: CarouselAlign;
46
+ classNames?: CarouselClassNames;
47
+ /** Accessible name for the carousel region. */
48
+ 'aria-label'?: string;
49
+ className?: string;
50
+ style?: CSSProperties;
51
+ };
52
+
53
+ declare function Carousel$1({ children, defaultSlide, slide, onSlideChange, orientation, align, classNames, className, style, 'aria-label': ariaLabel, }: CarouselProps): react.JSX.Element;
54
+
55
+ type CarouselMarkersProps = {
56
+ classNames?: CarouselMarkersClassNames;
57
+ renderMarker?: (index: number, active: boolean) => ReactNode;
58
+ /** Accessible name for the marker list. @default 'Slides' */
59
+ 'aria-label'?: string;
60
+ };
61
+ declare function Markers({ classNames, renderMarker, 'aria-label': ariaLabel, }: CarouselMarkersProps): react.JSX.Element | null;
62
+
63
+ type CarouselSlideProps = {
64
+ children: ReactNode;
65
+ className?: string;
66
+ style?: CSSProperties;
67
+ };
68
+ declare function Slide({ children, className, style }: CarouselSlideProps): react.JSX.Element;
69
+
70
+ declare const Carousel: typeof Carousel$1 & {
71
+ Slide: typeof Slide;
72
+ Markers: typeof Markers;
73
+ Prev: typeof Prev;
74
+ Next: typeof Next;
75
+ };
76
+
77
+ export { CAROUSEL_ALIGN, CAROUSEL_ORIENTATION, Carousel, type CarouselAlign, type CarouselButtonProps, type CarouselClassNames, type CarouselMarkersClassNames, type CarouselMarkersProps, type CarouselOrientation, type CarouselProps, type CarouselSlideProps };
@@ -0,0 +1,77 @@
1
+ import * as react from 'react';
2
+ import { CSSProperties, ReactNode } from 'react';
3
+
4
+ type CarouselButtonProps = {
5
+ className?: string;
6
+ style?: CSSProperties;
7
+ children?: ReactNode;
8
+ 'aria-label'?: string;
9
+ };
10
+ declare function Prev({ className, style, children, 'aria-label': ariaLabel, }: CarouselButtonProps): react.JSX.Element;
11
+ declare function Next({ className, style, children, 'aria-label': ariaLabel, }: CarouselButtonProps): react.JSX.Element;
12
+
13
+ declare const CAROUSEL_ORIENTATION: {
14
+ readonly horizontal: "horizontal";
15
+ readonly vertical: "vertical";
16
+ };
17
+ type CarouselOrientation = (typeof CAROUSEL_ORIENTATION)[keyof typeof CAROUSEL_ORIENTATION];
18
+ declare const CAROUSEL_ALIGN: {
19
+ readonly start: "start";
20
+ readonly center: "center";
21
+ };
22
+ type CarouselAlign = (typeof CAROUSEL_ALIGN)[keyof typeof CAROUSEL_ALIGN];
23
+ type CarouselClassNames = {
24
+ root?: string;
25
+ track?: string;
26
+ slide?: string;
27
+ };
28
+ type CarouselMarkersClassNames = {
29
+ list?: string;
30
+ marker?: string;
31
+ markerActive?: string;
32
+ };
33
+ type CarouselProps = {
34
+ /** `Carousel.Slide` children plus any controls (`Carousel.Markers`, etc.). */
35
+ children: ReactNode;
36
+ /** Initial slide when uncontrolled. @default 0 */
37
+ defaultSlide?: number;
38
+ /** Controlled active slide index. */
39
+ slide?: number;
40
+ /** Fires when the settled slide changes (after snap completes). */
41
+ onSlideChange?: (index: number) => void;
42
+ /** Scroll axis. @default 'horizontal' */
43
+ orientation?: CarouselOrientation;
44
+ /** scroll-snap-align of slides. @default 'start' */
45
+ align?: CarouselAlign;
46
+ classNames?: CarouselClassNames;
47
+ /** Accessible name for the carousel region. */
48
+ 'aria-label'?: string;
49
+ className?: string;
50
+ style?: CSSProperties;
51
+ };
52
+
53
+ declare function Carousel$1({ children, defaultSlide, slide, onSlideChange, orientation, align, classNames, className, style, 'aria-label': ariaLabel, }: CarouselProps): react.JSX.Element;
54
+
55
+ type CarouselMarkersProps = {
56
+ classNames?: CarouselMarkersClassNames;
57
+ renderMarker?: (index: number, active: boolean) => ReactNode;
58
+ /** Accessible name for the marker list. @default 'Slides' */
59
+ 'aria-label'?: string;
60
+ };
61
+ declare function Markers({ classNames, renderMarker, 'aria-label': ariaLabel, }: CarouselMarkersProps): react.JSX.Element | null;
62
+
63
+ type CarouselSlideProps = {
64
+ children: ReactNode;
65
+ className?: string;
66
+ style?: CSSProperties;
67
+ };
68
+ declare function Slide({ children, className, style }: CarouselSlideProps): react.JSX.Element;
69
+
70
+ declare const Carousel: typeof Carousel$1 & {
71
+ Slide: typeof Slide;
72
+ Markers: typeof Markers;
73
+ Prev: typeof Prev;
74
+ Next: typeof Next;
75
+ };
76
+
77
+ export { CAROUSEL_ALIGN, CAROUSEL_ORIENTATION, Carousel, type CarouselAlign, type CarouselButtonProps, type CarouselClassNames, type CarouselMarkersClassNames, type CarouselMarkersProps, type CarouselOrientation, type CarouselProps, type CarouselSlideProps };