19 | A React Carousel supporting different slides sizes, responsive, custom styling, accesible by default, SSR compatible, and tested.
20 |
21 |
22 | Stars are welcome 😊
23 |
24 |
25 | ## ✨ Features
26 |
27 | - Automagically responsive:
28 | - Any size: no need to set a specific size via props
29 | - Multiple items: no need to set the number of items per "page"
30 | - Supports images, videos, everything: each direct child is a slide
31 | - Scroll based: works on mobile or trackpad
32 | - Control buttons
33 | - Custom styling
34 | - Accessible by default
35 | - Show next/previous items partially
36 | - Works with server-side rendering
37 |
38 | ## 👀 Demos
39 |
40 | View the full [Storybook documentation](https://react-carousel.codely.com/).
41 |
42 | - [Standard Carousel](https://react-carousel.codely.com/?path=/story/carousel--default)
43 | - [Variable Slides](https://react-carousel.codely.com/?path=/story/carousel--variable-slides)
44 | - [With Gap](https://react-carousel.codely.com/?path=/story/carousel--with-gap)
45 | - [With Custom Button Content](https://react-carousel.codely.com/?path=/story/carousel--with-custom-button-content)
46 | - [With Custom Aria Labels](https://react-carousel.codely.com/?path=/story/carousel--with-custom-aria-labels)
47 | - [Button Styling](https://react-carousel.codely.com/?path=/story/carousel--button-styling)
48 |
49 | ## ⚙️ How to use
50 |
51 | 1. Install the dependency
52 | ```sh
53 | npm install @codelytv/react-carousel
54 | ```
55 | or
56 | ```sh
57 | yarn add @codelytv/react-carousel
58 | ```
59 | 2. Import and use:
60 | ```javascript
61 | import { Carousel } from "@codelytv/react-carousel"
62 | ```
63 | ```jsx
64 |
65 |
A simple slide
66 |
67 |
68 |
It can be any tag
69 |
and contain any number of items
70 |
71 |
72 | ```
73 | The carousel automatically detects the size of each slide and when navigating via buttons, it will scroll smoothly until the first not visible slide is in view.
74 |
75 | ### 🎛️ Props
76 |
77 | | Name | Value | Default | Description |
78 | | ------------------- | ------------------- | --------------------------- | --------------------------- |
79 | | `prevButtonContent` | `React.ReactNode` | [``](https://github.com/CodelyTV/react-carousel/tree/main/src/components/ArrowLeft.tsx) | The HTML content of the previous navigation button |
80 | | `nextButtonContent` | `React.ReactNode` | [``](https://github.com/CodelyTV/react-carousel/tree/main/src/components/ArrowRight.tsx) | The HTML content of the next navigation button |
81 | | `prevAriaLabel` | `string` | "Previous" | Defines the previous navigation button `aria-label` attribute. Useful when the button content is an element without accessible text. |
82 | | `nextAriaLabel` | `string` | "Next" | Defines the previous navigation button `aria-label` attribute. Useful when the button content is an element without accessible text. |
83 |
84 | ### 🎨 Styling
85 | There are some CSS Variables that will help you style the carousel:
86 |
87 | | Name | Default | Description |
88 | | ------------------------- | ------------------- | --------------------------------------------- |
89 | | `--slider-gap` | `0` | Sets the gap between slides |
90 | | `--slider-nav-margin-top` | `0.5rem` | Sets the top margin of the navigation buttons |
91 | | `--slider-button-width` | `2.5rem` | Sets the navigation buttons width |
92 | | `--slider-button-height` | `2.5rem` | Sets the navigation buttons height |
93 | | `--slider-button-padding` | `0.2rem` | Sets the padding of the navigation buttons |
94 |
95 | If this is not enough, you can always style via CSS classes. They all have low specificity so they are easy to overwrite, but be careful, changing this elements could cause the carousel to break. Try to limit the changes to colors, background, etc. to prevent unexpected results.
96 |
97 | | Class | Description |
98 | | ------------------- | -------------------------------------- |
99 | | `.carousel` | The main carousel wrapper |
100 | | `.carousel__slider` | The carousel scroller |
101 | | `.carousel__slide` | The wrapper for each slide |
102 | | `.carousel__nav` | The wrapper for the navigation buttons |
103 | | `.carousel__button` | The navigation buttons |
104 |
105 |
106 | ## 🤝 Contributing
107 |
108 | ### 📚 Run
109 |
110 | - `npm run build`: Compiles the Carousel package
111 | - `npm run storybook`: Opens Storybook documentation with all of the Carousel demos
112 |
113 | ### ✅ Testing
114 |
115 | `npm run test`: Run unit tests with Jest and React Testing Library
116 |
117 | ### 🔦 Linting
118 |
119 | - `npm run lint`: Run linter
120 | - `npm run lint:fix`: Fix lint issues
121 |
122 | ## 👌 Codely Code Quality Standards
123 |
124 | Publishing this package we are committing ourselves to the following code quality standards:
125 |
126 | - 🤝 Respect **Semantic Versioning**: No breaking changes in patch or minor versions
127 | - 🤏 No surprises in transitive dependencies: Use the **bare minimum dependencies** needed to meet the purpose
128 | - 🎯 **One specific purpose** to meet without having to carry a bunch of unnecessary other utilities
129 | - ✅ **Tests** as documentation and usage examples
130 | - 📖 **Well documented ReadMe** showing how to install and use
131 | - ⚖️ **License favoring Open Source** and collaboration
132 |
--------------------------------------------------------------------------------
/cypress.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "cypress";
2 |
3 | export default defineConfig({
4 | video: false,
5 | component: {
6 | specPattern: "tests/**/*.cy.{js,jsx,ts,tsx}",
7 | supportFile: "tests/tests-config/cypress/support/component.ts",
8 | indexHtmlFile: "tests/tests-config/cypress/support/component-index.html",
9 | devServer: {
10 | framework: "react",
11 | bundler: "vite",
12 | },
13 | viewportWidth: 1000,
14 | viewportHeight: 700,
15 | },
16 | });
17 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | <⚡⚛️> Vite React Best Practices Template (by Codely)
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/jest.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testEnvironment: "jsdom",
3 | setupFilesAfterEnv: ["/tests/tests-config/jest/setupTests.ts"],
4 | testMatch: ["/tests/**/*.(spec).(ts|tsx)"],
5 | transform: {
6 | "^.+\\.(js|jsx|ts|tsx)$": [
7 | "@swc/jest",
8 | {
9 | sourceMaps: true,
10 | jsc: {
11 | parser: {
12 | syntax: "typescript",
13 | tsx: true,
14 | },
15 | transform: {
16 | react: {
17 | runtime: "automatic",
18 | },
19 | },
20 | },
21 | },
22 | ],
23 | },
24 | moduleNameMapper: {
25 | "\\.(css|less|scss|sass)$": "identity-obj-proxy",
26 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
27 | "jest-transform-stub",
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@codelytv/react-carousel",
3 | "author": "codelytv",
4 | "license": "MIT",
5 | "version": "1.0.3",
6 | "type": "module",
7 | "files": [
8 | "dist"
9 | ],
10 | "main": "./dist/react-carousel.umd.cjs",
11 | "module": "./dist/react-carousel.js",
12 | "types": "./dist/index.d.ts",
13 | "peerDependencies": {
14 | "react": "^18.2.0",
15 | "react-dom": "^18.2.0"
16 | },
17 | "scripts": {
18 | "build": "vite build",
19 | "lint": "eslint --ignore-path .gitignore . && stylelint **/*.scss",
20 | "lint:fix": "eslint --fix --ignore-path .gitignore . && stylelint --fix **/*.scss",
21 | "docs": "storybook dev -p 6006",
22 | "build:docs": "storybook build",
23 | "cy:open": "cypress open",
24 | "cy:run": "cypress run --component"
25 | },
26 | "browserslist": {
27 | "production": [
28 | ">0.2%",
29 | "not dead",
30 | "not op_mini all"
31 | ],
32 | "development": [
33 | "last 1 chrome version",
34 | "last 1 firefox version",
35 | "last 1 safari version"
36 | ]
37 | },
38 | "devDependencies": {
39 | "@storybook/addon-essentials": "^7.0.17",
40 | "@storybook/addon-interactions": "^7.0.17",
41 | "@storybook/addon-links": "^7.0.17",
42 | "@storybook/blocks": "^7.0.17",
43 | "@storybook/react": "^7.0.17",
44 | "@storybook/react-vite": "^7.0.17",
45 | "@storybook/testing-library": "^0.1.0",
46 | "@swc/core": "^1.3.42",
47 | "@testing-library/cypress": "^9.0.0",
48 | "@types/node": "^16.18.21",
49 | "@types/react": "^18.0.30",
50 | "@types/react-dom": "^18.0.11",
51 | "@vitejs/plugin-react": "^3.1.0",
52 | "cypress": "^12.9.0",
53 | "eslint": "^8.36.0",
54 | "eslint-config-codely": "^2.1.3",
55 | "eslint-config-react-app": "^7.0.1",
56 | "eslint-plugin-storybook": "^0.6.12",
57 | "identity-obj-proxy": "^3.0.0",
58 | "prop-types": "^15.8.1",
59 | "react": "^18.2.0",
60 | "react-dom": "^18.2.0",
61 | "sass": "^1.60.0",
62 | "storybook": "^7.0.17",
63 | "stylelint": "^14.16.1",
64 | "stylelint-config-rational-order": "^0.0.4",
65 | "stylelint-config-standard-scss": "^3.0.0",
66 | "stylelint-order": "^5.0.0",
67 | "typescript": "^4.9.5",
68 | "vite": "^4.3.8",
69 | "vite-plugin-css-injected-by-js": "^3.1.1",
70 | "vite-plugin-dts": "^2.3.0"
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/public/brand.svg:
--------------------------------------------------------------------------------
1 |
19 |
--------------------------------------------------------------------------------
/public/img/georgi-kalaydzhiev-neF7gKk9708-unsplash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodelyTV/react-carousel/3c659bca2732f38fe45d11eaa789ed3b9617c40d/public/img/georgi-kalaydzhiev-neF7gKk9708-unsplash.jpg
--------------------------------------------------------------------------------
/public/img/josh-hild-zWCKJjPCl0s-unsplash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodelyTV/react-carousel/3c659bca2732f38fe45d11eaa789ed3b9617c40d/public/img/josh-hild-zWCKJjPCl0s-unsplash.jpg
--------------------------------------------------------------------------------
/public/img/lena-polishko-EM_thLGU0yw-unsplash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodelyTV/react-carousel/3c659bca2732f38fe45d11eaa789ed3b9617c40d/public/img/lena-polishko-EM_thLGU0yw-unsplash.jpg
--------------------------------------------------------------------------------
/public/img/lena-polishko-EYrLkNNOyhM-unsplash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodelyTV/react-carousel/3c659bca2732f38fe45d11eaa789ed3b9617c40d/public/img/lena-polishko-EYrLkNNOyhM-unsplash.jpg
--------------------------------------------------------------------------------
/public/img/luka-verc-2iBUuz4z3B8-unsplash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodelyTV/react-carousel/3c659bca2732f38fe45d11eaa789ed3b9617c40d/public/img/luka-verc-2iBUuz4z3B8-unsplash.jpg
--------------------------------------------------------------------------------
/public/img/nika-tchokhonelidze-Ojsyw6_5NDw-unsplash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodelyTV/react-carousel/3c659bca2732f38fe45d11eaa789ed3b9617c40d/public/img/nika-tchokhonelidze-Ojsyw6_5NDw-unsplash.jpg
--------------------------------------------------------------------------------
/public/img/yuliya-matuzava-28WVjNXuFV8-unsplash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodelyTV/react-carousel/3c659bca2732f38fe45d11eaa789ed3b9617c40d/public/img/yuliya-matuzava-28WVjNXuFV8-unsplash.jpg
--------------------------------------------------------------------------------
/src/ArrowLeft.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export function ArrowLeft() {
4 | return (
5 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/src/ArrowRight.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export function ArrowRight() {
4 | return (
5 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/src/Carousel.scss:
--------------------------------------------------------------------------------
1 | .carousel {
2 | width: 100%;
3 | overflow: hidden;
4 |
5 | &__slider {
6 | position: relative;
7 | display: flex;
8 | gap: var(--slider-gap, 0);
9 | overflow-x: scroll;
10 | overflow-y: hidden;
11 | scrollbar-width: none;
12 | scroll-behavior: smooth;
13 |
14 | &::-webkit-scrollbar {
15 | display: none;
16 | }
17 | }
18 |
19 | &__nav {
20 | margin-top: var(--slider-nav-margin-top, 0.5rem);
21 | text-align: left;
22 |
23 | @media (hover: none) {
24 | display: none;
25 | }
26 | }
27 |
28 | &__button {
29 | display: inline-flex;
30 | justify-content: center;
31 | align-items: center;
32 | width: var(--slider-button-width, 2.5rem);
33 | height: var(--slider-button-height, 2.5rem);
34 | padding: var(--slider-button-padding, 0.2rem);
35 | color: inherit;
36 | background: transparent;
37 | border: none;
38 | transition: all 0.2s;
39 |
40 | &:hover {
41 | opacity: 0.7;
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Carousel.stories.tsx:
--------------------------------------------------------------------------------
1 | import "./assets/docs.css";
2 |
3 | import type { Meta, StoryObj } from "@storybook/react";
4 |
5 | import { Carousel } from "./Carousel";
6 |
7 | const meta = {
8 | title: "Carousel",
9 | component: Carousel,
10 | tags: ["autodocs"],
11 | } satisfies Meta;
12 |
13 | export default meta;
14 | type Story = StoryObj;
15 |
16 | export const Default: Story = {
17 | parameters: {
18 | docs: {
19 | description: {
20 | story:
21 | "The carousel will automatically fit the width of its container, it's responsive by default and paginates according to the slides size, so there is no need to specify a number of slides per page or different breakpoints to make the carousel responsive. You can also scroll horizontally on mobile or trackpad (or pressing `shift` while scrolling with a mouse). You only need to import the `Carousel` component and each child will be considered a slide. Click the 'show code' below for a usage example.",
22 | },
23 | },
24 | },
25 | args: {
26 | children: [
27 |
28 |
32 |
33 | Photo by{" "}
34 |
35 | Georgi Kalaydzhiev
36 |
37 |
38 | ,
39 |
40 |
44 |
45 | Photo by{" "}
46 |
47 | Josh Hild
48 |
49 |
50 | ,
51 |
52 |
56 |
57 | Photo by{" "}
58 |
59 | Lena Polishko
60 |
61 |
62 | ,
63 |
64 |
68 |
69 | Photo by{" "}
70 |
71 | Lena Polishko
72 |
73 |
74 | ,
75 |
76 |
80 |
81 | Photo by{" "}
82 |
83 | Luka Verč
84 |
85 |
86 | ,
87 |
88 |
92 |
93 | Photo by{" "}
94 |
95 | nika tchokhonelidze
96 |
97 |
98 | ,
99 |
100 |
104 |
105 | Photo by{" "}
106 |
107 | Yuliya Matuzava
108 |
109 |
110 | ,
111 | ],
112 | },
113 | render: (args) => ,
114 | };
115 |
116 | export const VariableSlides: Story = {
117 | parameters: {
118 | docs: {
119 | description: {
120 | story:
121 | "The carousel supports any kind of content, as well as variable width and height for the slides.",
122 | },
123 | },
124 | },
125 | args: {
126 | children: [
127 |
A simple slide
,
128 |
129 |
130 |
,
131 |
134 |
It can be any tag
135 |
and contain any number of items
136 | ,
137 |
138 |
145 |
,
146 | ],
147 | },
148 | };
149 |
150 | export const WithGap: Story = {
151 | parameters: {
152 | docs: {
153 | description: {
154 | story:
155 | "To add gap between slides, you must specify a `--slider-gap` custom property via CSS. You can specify it in a wrapping div (see 'show code' below for an example), or in any other parent element in your CSS. [Read more about CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties).",
156 | },
157 | },
158 | },
159 | args: Default.args,
160 | render: (args) => (
161 |
162 |
163 |
164 | ),
165 | };
166 |
167 | export const WithCustomButtonContent: Story = {
168 | parameters: {
169 | docs: {
170 | description: {
171 | story:
172 | "By default the buttons display an arrow SVG. With the `nextButtonContent` and `prevButtonContent` props you can pass any string, SVG or HTML to be placed inside the previous and next button. Keep in mind that this will be inside the `button` tag, so you should not pass another button. Passing `` would be invalid.",
173 | },
174 | },
175 | },
176 | args: {
177 | prevButtonContent: 👈,
178 | nextButtonContent: 👉,
179 | ...Default.args,
180 | },
181 | };
182 |
183 | export const WithCustomAriaLabels: Story = {
184 | parameters: {
185 | docs: {
186 | description: {
187 | story:
188 | "The buttons by default have a 'Previous' and 'Next' aria-label for accessibility. With the `nextAriaLabel` and `prevAriaLabel` props you can pass a custom one if for example your website is in another language. You can also pass an empty string if you already passed a buttonContent with accessible text.",
189 | },
190 | },
191 | },
192 | args: {
193 | prevAriaLabel: "Anterior",
194 | nextAriaLabel: "Siguiente",
195 | ...Default.args,
196 | },
197 | };
198 |
199 | export const ButtonStyling: Story = {
200 | parameters: {
201 | docs: {
202 | description: {
203 | story:
204 | "If passing a custom content is not enough, you can adjust the button style with CSS custom properties. You can adjust `--slider-button-width`, `--slider-button-height` and `--slider-button-padding`, as well as the buttons top margin with `--slider-nav-margin-top`. You can specify this variables in a wrapping div (see 'show code' below for an example), or in any other parent element in your CSS. If the variables are not enough, you can also style the carousel's classes. [Check the Readme for the full documentation](https://github.com/CodelyTV/react-carousel#readme).",
205 | },
206 | },
207 | },
208 | args: Default.args,
209 | render: (args) => (
210 |