├── .eslintrc.json
├── .github
└── workflows
│ └── node.js.yml
├── .gitignore
├── .npmrc
├── .storybook
├── main.js
└── preview.js
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── docs
├── 0.7f12979c.iframe.bundle.js
├── 0.a050c77d9d5184a2984c.manager.bundle.js
├── 0.a050c77d9d5184a2984c.manager.bundle.js.LICENSE.txt
├── 1.12a002843cdd15ae748a.manager.bundle.js
├── 4.e60bf65e.iframe.bundle.js
├── 5.7f38562c1a9c59cb048f.manager.bundle.js
├── 5.7f38562c1a9c59cb048f.manager.bundle.js.LICENSE.txt
├── 6.5d0b5dbcc823408b3f75.manager.bundle.js
├── 7.e2782ab74fe8fdd3e210.manager.bundle.js
├── 8.f3dd91517cff71d9e5f0.manager.bundle.js
├── 8.f3dd91517cff71d9e5f0.manager.bundle.js.LICENSE.txt
├── 9.3dc9136cc3d4a723c52c.manager.bundle.js
├── favicon.ico
├── iframe.html
├── index.html
├── main.3de62775ef9963e7b0fb.manager.bundle.js
├── main.c2e2cf0f.iframe.bundle.js
├── project.json
├── runtime~main.00723566a33df1bda3c0.manager.bundle.js
├── runtime~main.695e7dde.iframe.bundle.js
├── vendors~main.c6e5cb4d563df4cc0ecd.manager.bundle.js
├── vendors~main.c6e5cb4d563df4cc0ecd.manager.bundle.js.LICENSE.txt
├── vendors~main.e65b8203.iframe.bundle.js
├── vendors~main.e65b8203.iframe.bundle.js.LICENSE.txt
└── vendors~main.e65b8203.iframe.bundle.js.map
├── jest.config.js
├── package-lock.json
├── package.json
├── prettier.config.js
├── react-snap-carousel.gif
├── src
├── use-isomorphic-layout-effect.tsx
├── use-snap-carousel.test.tsx
└── use-snap-carousel.tsx
├── stories
├── carousel.module.css
├── carousel.stories.tsx
├── carousel.tsx
├── infinite-carousel.module.css
├── infinite-carousel.stories.tsx
├── infinite-carousel.tsx
├── lib
│ ├── button.module.css
│ ├── button.tsx
│ ├── select.module.css
│ └── select.tsx
├── reset.css
├── slideshow.module.css
├── slideshow.stories.tsx
└── slideshow.tsx
└── tsconfig.json
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["react-app", "react-app/jest"]
3 | }
4 |
--------------------------------------------------------------------------------
/.github/workflows/node.js.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
3 |
4 | name: Node.js CI
5 |
6 | on:
7 | push:
8 | branches: [ "main" ]
9 | pull_request:
10 | branches: [ "main" ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | matrix:
19 | node-version: [16.x, 18.x]
20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
21 |
22 | steps:
23 | - uses: actions/checkout@v3
24 | - name: Use Node.js ${{ matrix.node-version }}
25 | uses: actions/setup-node@v3
26 | with:
27 | node-version: ${{ matrix.node-version }}
28 | cache: 'npm'
29 | - run: npm ci
30 | - run: npm run build --if-present
31 | - run: npm run lint
32 | - run: npm test
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | dist
4 | *.scratch
5 | .vscode
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | registry=https://registry.npmjs.com/
2 | legacy-peer-deps=true
3 |
--------------------------------------------------------------------------------
/.storybook/main.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | stories: [
3 | '../stories/**/*.stories.mdx',
4 | '../stories/**/*.stories.@(js|jsx|ts|tsx)'
5 | ],
6 | addons: [
7 | '@storybook/addon-links',
8 | {
9 | name: '@storybook/addon-essentials',
10 | options: {
11 | actions: false,
12 | controls: false,
13 | docs: false,
14 | toolbars: false
15 | }
16 | },
17 | 'storybook-css-modules',
18 | '@riscarrott/storybook-source-link'
19 | ],
20 | framework: '@storybook/react'
21 | };
22 |
--------------------------------------------------------------------------------
/.storybook/preview.js:
--------------------------------------------------------------------------------
1 | export const parameters = {
2 | actions: { argTypesRegex: '^on[A-Z].*' },
3 | controls: {
4 | matchers: {
5 | color: /(background|color)$/i,
6 | date: /Date$/
7 | }
8 | },
9 | sourceLink: 'https://github.com/richardscarrott/react-snap-carousel',
10 | sourceLinkIcon: 'github'
11 | };
12 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | https://github.com/richardscarrott/react-snap-carousel/releases
4 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | riscarrott@googlemail.com.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2023 Richard Scarrott
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Snap Carousel 🫰
2 |
3 | [](https://www.npmjs.com/package/react-snap-carousel)
4 | [](https://www.npmjs.com/package/react-snap-carousel)
5 | [](https://github.com/richardscarrott/react-snap-carousel/actions/workflows/node.js.yml)
6 | [](https://github.com/richardscarrott/react-snap-carousel/blob/main/LICENSE)
7 |
8 | DOM-first, headless carousel for React.
9 |
10 | React Snap Carousel leaves the DOM in charge of scrolling and simply computes derived state from the layout, allowing you to progressively enhance a scroll element with responsive carousel controls.
11 |
12 | 
13 |
14 | 🧈 Utilizes native browser scrolling & CSS scroll snap points for best performance and UX
15 |
16 | 📏 Computes responsive page state from DOM layout & scroll position
17 |
18 | 📲 Dynamic page-based CSS snap point rendering
19 |
20 | 🙈 Headless design, giving you full control over UI using React Hooks API
21 |
22 | 🖋️ Written in TypeScript
23 |
24 | 🪶 [Lightweight (~1kB)](https://bundlephobia.com/package/react-snap-carousel) + zero dependencies
25 |
26 | ## Install
27 |
28 | ```
29 | npm install react-snap-carousel
30 | ```
31 |
32 | ## Resources
33 |
34 | 🔥[StoryBook Examples](https://richardscarrott.github.io/react-snap-carousel/)🔥
35 |
36 | [CodeSandbox StarterKit](https://codesandbox.io/s/react-snap-carousel-49vu6p?file=/src/Carousel.tsx)
37 |
38 | [Beginners Tutorial](https://dev.to/richardscarrott/build-your-own-carousel-component-with-react-snap-carousel-1e11)
39 |
40 | ## Usage
41 |
42 | React Snap Carousel doesn't expose a ready-made `` component and instead offers a single export `useSnapCarousel` which provides the state & functions necessary to build your own carousel component.
43 |
44 | The following code snippet is a good starting point.
45 |
46 | > Inline styles are used for simplicity. You can use whichever CSS framework you prefer.
47 |
48 | > You can see it in action on [CodeSandbox](https://codesandbox.io/s/react-snap-carousel-49vu6p?file=/src/Carousel.tsx).
49 |
50 | ```tsx
51 | // Carousel.tsx
52 | import React, { CSSProperties } from 'react';
53 | import { useSnapCarousel } from 'react-snap-carousel';
54 |
55 | const styles = {
56 | root: {},
57 | scroll: {
58 | position: 'relative',
59 | display: 'flex',
60 | overflow: 'auto',
61 | scrollSnapType: 'x mandatory'
62 | },
63 | item: {
64 | width: '250px',
65 | height: '250px',
66 | flexShrink: 0
67 | },
68 | itemSnapPoint: {
69 | scrollSnapAlign: 'start'
70 | },
71 | controls: {
72 | display: 'flex',
73 | justifyContent: 'center',
74 | alignItems: 'center'
75 | },
76 | nextPrevButton: {},
77 | nextPrevButtonDisabled: { opacity: 0.3 },
78 | pagination: {
79 | display: 'flex'
80 | },
81 | paginationButton: {
82 | margin: '10px'
83 | },
84 | paginationButtonActive: { opacity: 0.3 },
85 | pageIndicator: {
86 | display: 'flex',
87 | justifyContent: 'center'
88 | }
89 | } satisfies Record;
90 |
91 | interface CarouselProps {
92 | readonly items: T[];
93 | readonly renderItem: (
94 | props: CarouselRenderItemProps
95 | ) => React.ReactElement;
96 | }
97 |
98 | interface CarouselRenderItemProps {
99 | readonly item: T;
100 | readonly isSnapPoint: boolean;
101 | }
102 |
103 | export const Carousel = ({
104 | items,
105 | renderItem
106 | }: CarouselProps) => {
107 | const {
108 | scrollRef,
109 | pages,
110 | activePageIndex,
111 | hasPrevPage,
112 | hasNextPage,
113 | prev,
114 | next,
115 | goTo,
116 | snapPointIndexes
117 | } = useSnapCarousel();
118 | return (
119 |
94 | {itemsToRender.map((item, index) =>
95 | renderItem({
96 | item,
97 | index,
98 | isSnapPoint: snapPointIndexes.has(index),
99 | // 4. Force snapping to the first item so that pages are always made up of the equivalent
100 | // items, even if the number of items isn't wholly divisible by the number of pages.
101 | // -- this simplifies the logic for rendering the controls.
102 | shouldSnap: item === itemsToRender[0]
103 | })
104 | )}
105 |