├── example ├── src │ ├── components │ │ ├── Wrapper.module.css │ │ ├── Header.module.css │ │ ├── Footer.module.css │ │ ├── Wrapper.jsx │ │ ├── Header.jsx │ │ └── Footer.jsx │ ├── main.jsx │ ├── index.css │ └── App.jsx ├── vite.config.js ├── .gitignore ├── index.html ├── README.md ├── .eslintrc.cjs ├── package.json ├── public │ └── vite.svg └── package-lock.json ├── src ├── main.jsx ├── icons │ ├── back.svg │ └── next.svg ├── App.css └── App.jsx ├── .gitignore ├── index.html ├── vite.config.js ├── .eslintrc.cjs ├── public └── vite.svg ├── package.json └── README.md /example/src/components/Wrapper.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | max-width: 1000px; 3 | margin: 0 auto; 4 | padding: 20px; 5 | } 6 | -------------------------------------------------------------------------------- /example/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /example/src/components/Header.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | text-align: center; 3 | margin-bottom: 20px; 4 | } 5 | 6 | .title { 7 | margin: 0; 8 | font-size: 40px; 9 | } 10 | 11 | .description { 12 | margin: 0; 13 | font-size: 18px; 14 | } 15 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import Slider from "./App.jsx"; 4 | 5 | ReactDOM.createRoot(document.getElementById("root")).render( 6 | 7 | 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /example/src/components/Footer.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | margin-top: 30px; 3 | text-align: center; 4 | color: #303030; 5 | padding: 10px 20px; 6 | font-size: 16px; 7 | } 8 | 9 | .wrapper p { 10 | margin: 10px; 11 | } 12 | 13 | .credits { 14 | font-weight: bold; 15 | } -------------------------------------------------------------------------------- /example/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.jsx' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /example/src/components/Wrapper.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styles from './Wrapper.module.css' 3 | 4 | const Wrapper = ({children}) => { 5 | return ( 6 |
7 | {children} 8 |
9 | ) 10 | } 11 | 12 | export default Wrapper 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Image Slider 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Image Slider Example 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | -------------------------------------------------------------------------------- /example/src/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./Header.module.css"; 3 | 4 | const Header = () => { 5 | return ( 6 |
7 |

Image Slider for your Pictures

8 |

9 | Node.js 18.x / 20+ is required. Tested on React 18.2.0 and NextJS 10 | 14.1.0. 11 |

12 |
13 | ); 14 | }; 15 | 16 | export default Header; 17 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { resolve } from "path"; 3 | 4 | export default defineConfig({ 5 | build: { 6 | lib: { 7 | entry: resolve(__dirname, "src/App.jsx"), 8 | name: "Image Slider", 9 | fileName: "image-slider", 10 | }, 11 | rollupOptions: { 12 | external: ["react", "react-dom"], 13 | output: { 14 | globals: { 15 | react: "React", 16 | "react-dom": "ReactDOM", 17 | }, 18 | }, 19 | }, 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react/jsx-no-target-blank': 'off', 16 | 'react-refresh/only-export-components': [ 17 | 'warn', 18 | { allowConstantExport: true }, 19 | ], 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /example/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react/jsx-no-target-blank': 'off', 16 | 'react-refresh/only-export-components': [ 17 | 'warn', 18 | { allowConstantExport: true }, 19 | ], 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "image-slider-example", 3 | "version": "2.0.1", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@madzadev/image-slider": "^2.0.1", 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0", 15 | "react-syntax-highlighter": "^15.5.0" 16 | }, 17 | "devDependencies": { 18 | "@types/react": "^18.2.55", 19 | "@types/react-dom": "^18.2.19", 20 | "@vitejs/plugin-react": "^4.2.1", 21 | "eslint": "^8.56.0", 22 | "eslint-plugin-react": "^7.33.2", 23 | "eslint-plugin-react-hooks": "^4.6.0", 24 | "eslint-plugin-react-refresh": "^0.4.5", 25 | "vite": "^5.1.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /example/src/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Montserrat&display=swap'); 2 | @import url('https://fonts.googleapis.com/css2?family=Nothing+You+Could+Do&display=swap'); 3 | 4 | body { 5 | margin: 0; 6 | padding: 0; 7 | font-family: 'Montserrat', sans-serif; 8 | background-color: #b8c6db; 9 | background-image: linear-gradient(315deg, #b8c6db 0%, #f5f7fa 74%); 10 | } 11 | 12 | h1, 13 | h2, 14 | h3, 15 | h4 { 16 | font-family: 'Montserrat', sans-serif; 17 | } 18 | 19 | .warning { 20 | padding-left: 10px; 21 | border-left: 5px solid tomato; 22 | } 23 | 24 | .note { 25 | padding-left: 10px; 26 | border-left: 5px solid rgb(255, 175, 55); 27 | } 28 | 29 | .link { 30 | color: rgb(103, 92, 253); 31 | } 32 | 33 | pre { 34 | font-family: 'Montserrat', sans-serif; 35 | font-size: 16px; 36 | } 37 | 38 | code { 39 | background-color: rgb(212, 212, 212); 40 | padding: 5px; 41 | } -------------------------------------------------------------------------------- /src/icons/back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/icons/next.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /example/src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./Footer.module.css"; 3 | 4 | const Footer = () => { 5 | return ( 6 |
7 |

8 | Placeholder images from{" "} 9 | 15 | Lorem picsum 16 | 17 | . Icons from{" "} 18 | 24 | FlatIcon. 25 | 26 |

27 |

28 | Have a suggestion?{" "} 29 | 35 | Contribute{" "} 36 | 37 | to project! 38 |

39 |
40 | ); 41 | }; 42 | 43 | export default Footer; 44 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | position: relative; 3 | margin: 0 auto; 4 | overflow: hidden; 5 | text-align: center; 6 | box-sizing: border-box; 7 | } 8 | 9 | .button { 10 | width: 40px; 11 | height: 40px; 12 | -webkit-filter: invert(100%); 13 | /* Safari/Chrome */ 14 | filter: invert(100%); 15 | } 16 | 17 | .leftClick { 18 | position: absolute; 19 | width: 40px; 20 | height: 40px; 21 | top: calc(50% - 20px); 22 | left: 20px; 23 | color: white; 24 | display: grid; 25 | place-items: center; 26 | transition: transform 0.2s; 27 | } 28 | 29 | .leftClick:hover { 30 | cursor: pointer; 31 | transform: scale(1.1); 32 | } 33 | 34 | .rightClick { 35 | position: absolute; 36 | width: 40px; 37 | height: 40px; 38 | top: calc(50% - 20px); 39 | right: 20px; 40 | color: white; 41 | display: grid; 42 | place-items: center; 43 | transition: transform 0.2s; 44 | } 45 | 46 | .rightClick:hover { 47 | cursor: pointer; 48 | transform: scale(1.3); 49 | } 50 | 51 | .dots { 52 | width: 100%; 53 | display: flex; 54 | justify-content: center; 55 | padding: 20px 0; 56 | } 57 | 58 | .dot { 59 | width: 15px; 60 | height: 15px; 61 | background-color: lightblue; 62 | border-radius: 50%; 63 | margin: 0 10px; 64 | } 65 | 66 | .dot:hover { 67 | cursor: pointer; 68 | background-color: rgb(76, 171, 202); 69 | } 70 | 71 | .activeDot { 72 | width: 15px; 73 | height: 15px; 74 | background-color: rgb(76, 171, 202); 75 | border-radius: 50%; 76 | margin: 0 10px; 77 | } 78 | 79 | .activeDot:hover { 80 | cursor: pointer; 81 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@madzadev/image-slider", 3 | "version": "2.0.1", 4 | "description": "React/NextJS image slider component to display images", 5 | "keywords": [ 6 | "react", 7 | "nextjs", 8 | "image", 9 | "slider" 10 | ], 11 | "author": "Madza (http://madza.dev)", 12 | "license": "MIT", 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/madzadev/image-slider.git" 16 | }, 17 | "homepage": "https://image-slider-madza.vercel.app/", 18 | "bugs": { 19 | "url": "https://github.com/madzadev/image-slider/issues" 20 | }, 21 | "type": "module", 22 | "files": [ 23 | "dist" 24 | ], 25 | "main": "./dist/image-slider.umd.cjs", 26 | "module": "./dist/image-slider.js", 27 | "exports": { 28 | ".": { 29 | "import": "./dist/image-slider.js", 30 | "require": "./dist/image-slider.umd.cjs" 31 | }, 32 | "./dist/index.css": "./dist/style.css" 33 | }, 34 | "engines": { 35 | "node": ">=18" 36 | }, 37 | "scripts": { 38 | "dev": "vite", 39 | "build": "vite build", 40 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 41 | "preview": "vite preview" 42 | }, 43 | "dependencies": { 44 | "react": "^18.2.0", 45 | "react-dom": "^18.2.0" 46 | }, 47 | "devDependencies": { 48 | "@types/react": "^18.2.55", 49 | "@types/react-dom": "^18.2.19", 50 | "@vitejs/plugin-react": "^4.2.1", 51 | "eslint": "^8.56.0", 52 | "eslint-plugin-react": "^7.33.2", 53 | "eslint-plugin-react-hooks": "^4.6.0", 54 | "eslint-plugin-react-refresh": "^0.4.5", 55 | "vite": "^5.1.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React/NextJS Image slider 2 | 3 | ![Slider Preview](https://images.ctfassets.net/zlsyc9paq6sa/6wX3r2sDGHSPjMEmIk8nSi/5f9e6581fe9b7bf9cf721ffb14aad6b2/image_slider_preview.gif) 4 | 5 | ### Demo: [https://image-slider-madza.vercel.app](https://image-slider-madza.vercel.app) 6 | 7 | --- 8 | 9 | ## Requirements 10 | 11 | Node.js 18.x / 20+ is required. 12 | 13 | Tested on React 18.2.0 and NextJS 14.1.0. 14 | 15 | ## Installation 16 | 17 | ```javascript 18 | npm install @madzadev/image-slider 19 | ``` 20 | 21 | ## Usage 22 | 23 | ```javascript 24 | import Slider from "@madzadev/image-slider"; 25 | import "@madzadev/image-slider/dist/index.css"; 26 | ``` 27 | 28 | ```javascript 29 | const images = [ 30 | { url: "https://picsum.photos/seed/a/1600/900" }, 31 | { url: "https://picsum.photos/seed/b/1920/1080" }, 32 | { url: "https://picsum.photos/seed/c/1366/768" }, 33 | ]; 34 | ``` 35 | 36 | ```javascript 37 | 38 | ``` 39 | 40 | `imageList` is the mandatory prop and requires to pass in 41 | an array consisting of objects with `url` keys. 42 | 43 | `width` and `height` are mandatory props that 44 | set the dimension of the images shown. 45 | 46 | ## Behavior 47 | 48 | The default values of available options props are displayed. 49 | 50 | ```javascript 51 | 56 | ``` 57 | 58 | ## Controls 59 | 60 | The default values of available props are displayed. 61 | 62 | ```javascript 63 | 64 | ``` 65 | 66 | ## Styling 67 | 68 | If set, background color is displayed to fill the background if images are smaller than the slider wrapper. 69 | 70 | ```javascript 71 | 72 | ``` 73 | 74 | ## Final notes 75 | 76 | The project is under MIT license, so be free to check it out and give 77 | contributions. 78 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import "./App.css"; 3 | 4 | import backButton from "./icons/back.svg"; 5 | import nextButton from "./icons/next.svg"; 6 | 7 | const Slider = ({ 8 | imageList, 9 | width, 10 | height, 11 | loop = true, 12 | autoPlay = true, 13 | autoPlayInterval = 3000, 14 | showArrowControls = true, 15 | showDotControls = true, 16 | bgColor = "none", 17 | }) => { 18 | let [active, setActive] = useState(0); 19 | 20 | const setPreviousImage = () => { 21 | if (active !== 0) { 22 | setActive((active -= 1)); 23 | } else { 24 | if (loop) { 25 | setActive((active = imageList.length - 1)); 26 | } 27 | } 28 | }; 29 | 30 | const setNextImage = () => { 31 | if (active !== imageList.length - 1) { 32 | setActive((active += 1)); 33 | } else { 34 | if (loop) { 35 | setActive((active = 0)); 36 | } 37 | } 38 | }; 39 | 40 | const leftClickHandle = () => { 41 | setPreviousImage(); 42 | }; 43 | 44 | const rightClickHandle = () => { 45 | setNextImage(); 46 | }; 47 | 48 | const dotClickHandler = (e) => { 49 | const dotNum = e.target.getAttribute("data-key"); 50 | setActive((active = parseInt(dotNum))); 51 | }; 52 | 53 | useEffect(() => { 54 | if (autoPlay) { 55 | let autoSlider = setInterval(setNextImage, autoPlayInterval); 56 | return () => clearInterval(autoSlider); 57 | } 58 | }, [active]); 59 | 60 | return ( 61 |
62 |
63 | {((showArrowControls && !loop && active !== 0) || 64 | (showArrowControls && loop)) && ( 65 |
66 | back 67 |
68 | )} 69 | image 78 | {((showArrowControls && !loop && active !== imageList.length - 1) || 79 | (showArrowControls && loop)) && ( 80 |
81 | next 82 |
83 | )} 84 |
85 | {showDotControls && ( 86 |
87 | {imageList.map((el, index) => { 88 | if (index !== active) { 89 | return ( 90 |
96 | ); 97 | } else { 98 | return
; 99 | } 100 | })} 101 |
102 | )} 103 |
104 | ); 105 | }; 106 | 107 | export default Slider; 108 | -------------------------------------------------------------------------------- /example/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Wrapper from "./components/Wrapper"; 3 | import Header from "./components/Header"; 4 | import Footer from "./components/Footer"; 5 | 6 | import Slider from "@madzadev/image-slider"; 7 | import "@madzadev/image-slider/dist/index.css"; 8 | 9 | import { PrismLight as SyntaxHighlighter } from "react-syntax-highlighter"; 10 | import { coldarkDark } from "react-syntax-highlighter/dist/esm/styles/prism"; 11 | 12 | import bash from "react-syntax-highlighter/dist/esm/languages/prism/bash"; 13 | import jsx from "react-syntax-highlighter/dist/esm/languages/prism/jsx"; 14 | import javascript from "react-syntax-highlighter/dist/esm/languages/prism/javascript"; 15 | SyntaxHighlighter.registerLanguage("bash", bash); 16 | SyntaxHighlighter.registerLanguage("jsx", jsx); 17 | SyntaxHighlighter.registerLanguage("javascript", javascript); 18 | 19 | const images = [ 20 | { 21 | url: "https://picsum.photos/seed/1/1000/300", 22 | }, 23 | { 24 | url: "https://picsum.photos/seed/36/1000/300", 25 | }, 26 | { 27 | url: "https://picsum.photos/seed/47/1000/300", 28 | }, 29 | { 30 | url: "https://picsum.photos/seed/35/1000/300", 31 | }, 32 | { 33 | url: "https://picsum.photos/seed/19/1000/300", 34 | }, 35 | { 36 | url: "https://picsum.photos/seed/22/1000/300", 37 | }, 38 | { 39 | url: "https://picsum.photos/seed/33/1000/300", 40 | }, 41 | { 42 | url: "https://picsum.photos/seed/8/1000/300", 43 | }, 44 | ]; 45 | 46 | const App = () => { 47 | return ( 48 | 49 |
50 | 51 |

Installation

52 | 53 | {`npm install @madzadev/image-slider`} 54 | 55 | 56 |

Usage

57 | 58 | {`import Slider from '@madzadev/image-slider' 59 | import "image-slider/dist/index.css"`} 60 | 61 | 62 | {`const images = [ 63 | {url: 'https://picsum.photos/seed/a/1600/900'}, 64 | {url: 'https://picsum.photos/seed/b/1920/1080'}, 65 | {url: 'https://picsum.photos/seed/c/1366/768'} 66 | ]`} 67 | 68 | 69 | {``} 70 | 71 |

72 | 'imageList' is the mandatory prop and requires to pass in 73 | an array consisting of objects with url keys. 74 |

75 |

76 | 'width' and 'height' are mandatory props that 77 | set the dimension of the images shown. 78 |

79 | 80 |

Behavior

81 |

82 | The default values of available props are displayed. 83 |

84 | 85 | {``} 90 | 91 | 92 |

Controls

93 |

94 | The default values of available props are displayed. 95 |

96 | 97 | {``} 101 | 102 | 103 |

Styling

104 |

105 | If set, this prop fills the background if images are smaller than the 106 | slider wrapper. 107 |

108 | 109 | {``} 110 | 111 | 112 |

Final notes

113 |

114 | The project is under MIT license, so be free to check it out and give 115 | contributions. 116 |

117 |