├── .babelrc.json ├── .gitignore ├── .storybook ├── main.js └── preview.js ├── LICENSE ├── README.md ├── dist ├── index.es.js └── index.js ├── dummyackage.json ├── package.json ├── pnpm-lock.yaml ├── rollup.config.mjs └── src ├── components ├── CircleCursor │ ├── index.jsx │ └── styles.module.css ├── EmojiCursor │ ├── index.jsx │ └── styles.module.css ├── ImageCursor │ ├── index.jsx │ └── styles.module.css ├── SquareOrRectangleCursors │ ├── index.jsx │ └── styles.module.css └── index.jsx ├── index.js └── stories ├── Button.jsx ├── Button.stories.jsx ├── Cursors.jsx ├── Cursors.stories.jsx ├── Header.jsx ├── Header.stories.js ├── Introduction.mdx ├── Page.jsx ├── Page.stories.js ├── assets ├── code-brackets.svg ├── colors.svg ├── comments.svg ├── direction.svg ├── flow.svg ├── plugin.svg ├── repo.svg └── stackalt.svg ├── button.css ├── cursors.css ├── header.css └── page.css /.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/env"] 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | /** @type { import('@storybook/react-vite').StorybookConfig } */ 2 | const config = { 3 | stories: ['../src/**/*.stories.@(js|jsx)'], 4 | addons: [ 5 | "@storybook/addon-links", 6 | "@storybook/addon-essentials", 7 | "@storybook/addon-interactions", 8 | '@storybook/addon-styling', 9 | '@storybook/addon-interactions', 10 | ], 11 | framework: { 12 | name: "@storybook/react-vite", 13 | options: {}, 14 | }, 15 | core: { 16 | disableTelemetry: true, // 👈 Disables telemetry 17 | }, 18 | 19 | docs: { 20 | autodocs: "tag", 21 | }, 22 | }; 23 | export default config; 24 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | /** @type { import('@storybook/react').Preview } */ 2 | const preview = { 3 | parameters: { 4 | actions: { argTypesRegex: "^on[A-Z].*" }, 5 | controls: { 6 | matchers: { 7 | color: /(background|color)$/i, 8 | date: /Date$/, 9 | }, 10 | }, 11 | }, 12 | }; 13 | 14 | export default preview; 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Shridhar Kamat 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](https://img.shields.io/npm/v/react-cursors)](https://www.npmjs.com/package/react-cursors) 2 | [![](https://img.shields.io/npm/dm/react-cursors)](https://www.npmjs.com/package/react-cursors) 3 | ![react' (1) 1](https://user-images.githubusercontent.com/65373279/137585010-15a171b1-9839-4c17-b177-79232d099d2d.png) 4 | 5 | 6 |

React Cursors

7 |

Collection of highly customizable cursors for react!

8 | 9 | > Currently we have a few cursors ready for you to use, you may also customize them to your liking and contribute to this repo ✨ 10 | 11 | ## Installation 12 | ``` 13 | npm i react-cursors 14 | ``` 15 | 16 | ## Usage 17 | 18 | Import the component 19 | 20 | ``` js 21 | import { CircleCursor } from 'react-cursors' 22 | ``` 23 | 24 | Use it 25 | 26 | ``` js 27 | 50 | 51 | ``` 52 | 53 | Props provided in the initial will be the initial styles of the cursor, and the props provided in the hover will be the styles of cursor when the cursor hovers over an element with class 'hover-detect'. 54 | 55 |
56 | 57 | If you want to use default styling, or no hover elements then you will have to keep the component as : 58 | 59 | ```js 60 | 61 | 62 | 63 | ``` 64 |
65 | 66 | Further, it is always recommeded to use lazy loading while importing the component, to avoid any DOM related problems. 67 | 68 | Problems faced without lazy loading can lead to bugs like, hover styles not getting activated when cursors hovers over elements having class 'hover-detect' 69 | 70 | ``` 71 | 72 | import React, { Suspense } from "react"; // we also need to import suspense 73 | 74 | const CircleCursor = React.lazy(() => import('react-cursors').then(module => ({ default: module.CircleCursor }))) ; 75 | 76 | ``` 77 | 78 | And using it like : 79 | 80 | ```html 81 | 82 |
83 | 84 | Loading ...
}> 85 | 86 | 87 | 88 | 89 | ``` 90 | 91 | 92 | 93 | 94 | ## Element attributes 95 | All the attributes are optional, incase values are not provided, the default values will be used 96 | 97 |

98 | 99 | ### Common for all 100 |
101 | 102 | | Attribute | Values | Description | 103 | | ----------------------- | ------------------------ | ---------------------------------------------------------------------------------------- | 104 | | `safeBoundaryX` | `string` | Used to set the boundary of the cursor along the x-axis, helpful inorder to prevent overflow 105 | | `safeBoundaryY` | `string` | Used to set the boundary of the cursor along the y-axis, helpful inorder to prevent overflow 106 | 107 |

108 | 109 | ### Circle Cursor 110 |
111 | 112 | | Attribute | Values | Description | 113 | | ----------------------- | ------------------------ | ---------------------------------------------------------------------------------------- | 114 | | `dotSize` | `string` | Used to set the size of the inner dot | 115 | | `circleSize` | `string` | Used to set the size of the outer circle | 116 | | `borderStyle` | `string` | Used to set the border style of the outer circle | 117 | | `color` | `string` | Used to set the color of the outer circle and inner dot | 118 | | `borderWidth` | `string` | Used to set the border width of the outer circle | 119 | | `circleDelay` | `string` | Used to set the delay of the outer circle | 120 | | `dotDelay` | `string` | Used to set the delay of the inner circle | 121 | 122 |

123 | 124 | ### Emoji Cursor 125 |
126 | 127 | | Attribute | Values | Description | 128 | | ----------------------- | ------------------------ | ---------------------------------------------------------------------------------------- | 129 | | `size` | `string` | Used to set the size of the emoji | 130 | | `delay` | `string` | Used to set the delay of the emoji | 131 | | `emoji` | `string` | Used to set the emoji | 132 | 133 |

134 | 135 | ### Image Cursor 136 | 137 | #### Experimental ⚠ 138 |
139 | 140 | | Attribute | Values | Description | 141 | | ----------------------- | ------------------------ | ---------------------------------------------------------------------------------------- | 142 | | `width` | `string` | Used to set the width of the image | 143 | | `height` | `string` | Used to set the height of the image | 144 | | `delay` | `string` | Used to set the delay of the image | 145 | | `url` | `string` | Url of the image | 146 | 147 |

148 | 149 | ### Square Rectangle Cursor 150 |
151 | 152 | | Attribute | Values | Description | 153 | | ----------------------- | ------------------------ | ---------------------------------------------------------------------------------------- | 154 | | `innerShapeSize` | `string` | Used to set the size of the inner shape | 155 | | `shapeSize` | `string` | Used to set the size of the outer shape | 156 | | `borderStyle` | `string` | Used to set the border style of the outer circle | 157 | | `color` | `string` | Used to set the color of the outer shape and inner shape | 158 | | `borderWidth` | `string` | Used to set the border width of the outer circle | 159 | | `shapeDelay` | `string` | Used to set the delay of the outer circle | 160 | | `innerShapeDelay` | `string` | Used to set the delay of the inner circle | 161 | 162 | 163 |
164 | 165 | --- 166 | 167 |

Join our Community and feel free to drop your questions on

168 |

169 | 170 | Discord 171 |

172 | 173 | --- 174 | 175 | ## Project Maintainers 🛠 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 |

Devraj Chatribin

💻

Shridhar Kamat

💻
-------------------------------------------------------------------------------- /dist/index.es.js: -------------------------------------------------------------------------------- 1 | import e,{useState as t}from"react";function r(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var r=null==e?null:"undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(null!=r){var n,o,i,a,s=[],c=!0,l=!1;try{if(i=(r=r.call(e)).next,0===t){if(Object(r)!==r)return;c=!1}else for(;!(c=(n=i.call(r)).done)&&(s.push(n.value),s.length!==t);c=!0);}catch(e){l=!0,o=e}finally{try{if(!c&&null!=r.return&&(a=r.return(),Object(a)!==a))return}finally{if(l)throw o}}return s}}(e,t)||function(e,t){if(!e)return;if("string"==typeof e)return n(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);"Object"===r&&e.constructor&&(r=e.constructor.name);if("Map"===r||"Set"===r)return Array.from(e);if("Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return n(e,t)}(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function n(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);re.length)&&(t=e.length);for(var r=0,n=new Array(t);r0.2%", 48 | "not dead", 49 | "not op_mini all" 50 | ], 51 | "development": [ 52 | "last 1 chrome version", 53 | "last 1 firefox version", 54 | "last 1 safari version" 55 | ] 56 | }, 57 | "devDependencies": { 58 | "@babel/cli": "^7.22.6", 59 | "@babel/core": "^7.22.6", 60 | "@babel/preset-env": "^7.22.6" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-cursors", 3 | "description": "Cursor components for React", 4 | "author": "Shridhar Kamat", 5 | "keywords": [ 6 | "react", 7 | "components", 8 | "ui", 9 | "cursors" 10 | ], 11 | "version": "2.1.0", 12 | "private": false, 13 | "main": "dist/index.js", 14 | "scripts": { 15 | "test": "echo \"Error: no test specified\" && exit 1", 16 | "storybook": "storybook dev -p 6006", 17 | "build-storybook": "storybook build", 18 | "build-lib": "rollup -c" 19 | }, 20 | "files": [ 21 | "dist", 22 | "README.md" 23 | ], 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/Shridhar-dev/react-cursors" 27 | }, 28 | "license": "ISC", 29 | "devDependencies": { 30 | "@babel/preset-react": "^7.22.5", 31 | "@rollup/plugin-babel": "^6.0.3", 32 | "@rollup/plugin-node-resolve": "^15.1.0", 33 | "@rollup/plugin-terser": "^0.4.3", 34 | "@storybook/addon-essentials": "^7.0.25", 35 | "@storybook/addon-interactions": "^7.0.25", 36 | "@storybook/addon-links": "^7.0.25", 37 | "@storybook/addon-styling": "^1.3.2", 38 | "@storybook/blocks": "^7.0.25", 39 | "@storybook/react": "^7.0.25", 40 | "@storybook/react-vite": "^7.0.25", 41 | "@storybook/testing-library": "^0.0.14-next.2", 42 | "prop-types": "^15.8.1", 43 | "react": "^18.2.0", 44 | "react-dom": "^18.2.0", 45 | "rollup": "^3.26.2", 46 | "rollup-plugin-peer-deps-external": "^2.2.4", 47 | "rollup-plugin-postcss": "^4.0.2", 48 | "storybook": "^7.0.25" 49 | }, 50 | "peerDependencies": { 51 | "react": "^18.2.0", 52 | "react-dom": "^18.2.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | import resolve from "@rollup/plugin-node-resolve"; 3 | import external from 'rollup-plugin-peer-deps-external' 4 | import postcss from 'rollup-plugin-postcss'; 5 | import terser from '@rollup/plugin-terser'; 6 | export default { 7 | 8 | input: './src/index.js', 9 | output: [ 10 | { 11 | file: 'dist/index.js', 12 | format: 'cjs' 13 | }, 14 | { 15 | file: 'dist/index.es.js', 16 | format: 'es', 17 | exports: 'named', 18 | } 19 | ], 20 | plugins: [ 21 | resolve({ 22 | extensions: [".js", ".jsx"] 23 | }), 24 | postcss({ 25 | plugins: [], 26 | minimize: true, 27 | }), 28 | babel({ 29 | babelHelpers: 'bundled', 30 | exclude: 'node_modules/**', 31 | presets: ['@babel/preset-react'] 32 | }), 33 | external(), 34 | terser(), 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /src/components/CircleCursor/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import styles from './styles.module.css' 3 | 4 | function CircleCursor(props) { 5 | const [newProps, setNewProps] = useState(props.initial) 6 | 7 | 8 | window.addEventListener('mousemove', (e) => { 9 | if ((e.clientX < (window.outerWidth - (props.safeBoundaryX ? props.safeBoundaryX : 30))) 10 | && 11 | e.clientY < (window.outerHeight - (props.safeBoundaryY ? props.safeBoundaryY : 40))) { 12 | document.getElementById('circle__cursor').style.top = (e.clientY) + 'px' 13 | document.getElementById('circle__cursor').style.left = (e.clientX) + 'px' 14 | document.getElementById('circle__cursor__dot').style.top = (e.clientY) + 'px' 15 | document.getElementById('circle__cursor__dot').style.left = (e.clientX) + 'px' 16 | } 17 | }) 18 | 19 | let hoverElement = document.querySelectorAll('.hover-detect') 20 | hoverElement = Array.from(hoverElement) 21 | 22 | hoverElement.forEach((element) => { 23 | 24 | element.addEventListener('mouseenter', () => { 25 | setNewProps(props.hover) 26 | }) 27 | 28 | element.addEventListener('mouseleave', () => { 29 | setNewProps(props.initial) 30 | }) 31 | 32 | }) 33 | 34 | return ( 35 | <> 36 |
44 |
45 |
51 |
52 | 53 | ) 54 | } 55 | 56 | export default CircleCursor 57 | 58 | -------------------------------------------------------------------------------- /src/components/CircleCursor/styles.module.css: -------------------------------------------------------------------------------- 1 | .circle__cursor{ 2 | cursor:none; 3 | pointer-events: none; 4 | position:fixed; 5 | z-index:999; 6 | height:3rem; 7 | width:3rem; 8 | transform:translate(-50%,-50%); 9 | border:2px solid black; 10 | border-radius:50%; 11 | transition: all 0.03s ease 0s, width 0.2s ease 0s, height 0.2s ease 0s 12 | } 13 | .circle__cursor__dot{ 14 | cursor:none; 15 | pointer-events: none; 16 | position:fixed; 17 | z-index:999; 18 | height:1rem; 19 | width:1rem; 20 | transform:translate(-50%,-50%); 21 | background:black; 22 | border-radius:50%; 23 | transition:all 0.03s ease 0s, width 0.2s ease 0s, height 0.2s ease 0s; 24 | } -------------------------------------------------------------------------------- /src/components/EmojiCursor/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import styles from './styles.module.css' 3 | 4 | function EmojiCursor(props) { 5 | const [newProps, setNewProps] = useState(props.initial) 6 | 7 | window.addEventListener('mousemove',(e)=>{ 8 | if((e.clientX < (window.outerWidth - (props.safeBoundaryX ? props.safeBoundaryX : 30))) 9 | && 10 | e.clientY < (window.outerHeight - (props.safeBoundaryY ? props.safeBoundaryY : 40)) ){ 11 | document.getElementById('emoji__cursor').style.top = (e.clientY) + 'px' 12 | document.getElementById('emoji__cursor').style.left = (e.clientX) + 'px' 13 | } 14 | }) 15 | 16 | 17 | let hoverElement = document.querySelectorAll('.hover-detect') 18 | hoverElement = Array.from(hoverElement) 19 | 20 | 21 | hoverElement.forEach((element)=>{ 22 | 23 | 24 | element.addEventListener('mouseenter',()=>{ 25 | setNewProps(props.hover) 26 | }) 27 | 28 | element.addEventListener('mouseleave',()=>{ 29 | setNewProps(props.initial) 30 | }) 31 | 32 | }) 33 | 34 | return ( 35 | <> 36 |
40 | {newProps.emoji ? newProps.emoji : '🤚'} 41 |
42 | 43 | ) 44 | } 45 | 46 | export default EmojiCursor 47 | -------------------------------------------------------------------------------- /src/components/EmojiCursor/styles.module.css: -------------------------------------------------------------------------------- 1 | .emoji__cursor{ 2 | cursor:none; 3 | pointer-events: none; 4 | position:fixed; 5 | z-index:999; 6 | transition:all 0.03s ease 0s, width 0.2s ease 0s, height 0.2s ease 0s; 7 | transform:translate(-50%,-50%); 8 | } -------------------------------------------------------------------------------- /src/components/ImageCursor/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import styles from './styles.module.css' 3 | 4 | function ImageCursor(props) { 5 | 6 | const [newProps, setNewProps] = useState(props.initial) 7 | 8 | window.addEventListener('mousemove', (e) => { 9 | if ((e.clientX < (window.outerWidth - (props.safeBoundaryX ? props.safeBoundaryX : 30))) 10 | && 11 | e.clientY < (window.outerHeight - (props.safeBoundaryY ? props.safeBoundaryY : 40))) { 12 | document.getElementById('image__cursor').style.top = (e.clientY) + 'px' 13 | document.getElementById('image__cursor').style.left = (e.clientX) + 'px' 14 | } 15 | }) 16 | 17 | let hoverElement = document.querySelectorAll('.hover-detect') 18 | hoverElement = Array.from(hoverElement) 19 | 20 | 21 | hoverElement.forEach((element) => { 22 | 23 | 24 | element.addEventListener('mouseenter', () => { 25 | setNewProps(props.hover) 26 | }) 27 | 28 | element.addEventListener('mouseleave', () => { 29 | setNewProps(props.initial) 30 | }) 31 | 32 | }) 33 | 34 | return ( 35 | <> 36 |
44 | 45 |
46 | 47 | ) 48 | } 49 | 50 | export default ImageCursor -------------------------------------------------------------------------------- /src/components/ImageCursor/styles.module.css: -------------------------------------------------------------------------------- 1 | .image__cursor{ 2 | cursor:none; 3 | pointer-events: none; 4 | position:fixed; 5 | z-index:999; 6 | transition:all 0.03s ease 0s, width 0.2s ease 0s, height 0.2s ease 0s; 7 | transform:translate(0%,0%); 8 | } -------------------------------------------------------------------------------- /src/components/SquareOrRectangleCursors/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import styles from './styles.module.css' 3 | 4 | function SquareOrRectangleCursors(props) { 5 | const [newProps, setNewProps] = useState(props.initial) 6 | 7 | 8 | window.addEventListener('mousemove', (e) => { 9 | if ((e.clientX < (window.outerWidth - (props.safeBoundaryX ? props.safeBoundaryX : 30))) 10 | && 11 | e.clientY < (window.outerHeight - (props.safeBoundaryY ? props.safeBoundaryY : 40))) { 12 | document.getElementById('SquareOrRectangleCursorsMain').style.top = (e.clientY) + 'px' 13 | document.getElementById('SquareOrRectangleCursorsMain').style.left = (e.clientX) + 'px' 14 | document.getElementById('SquareOrRectangleCursorsInner').style.top = (e.clientY) + 'px' 15 | document.getElementById('SquareOrRectangleCursorsInner').style.left = (e.clientX) + 'px' 16 | } 17 | }) 18 | 19 | let hoverElement = document.querySelectorAll('.hover-detect') 20 | hoverElement = Array.from(hoverElement) 21 | 22 | hoverElement.forEach((element) => { 23 | 24 | element.addEventListener('mouseenter', () => { 25 | setNewProps(props.hover) 26 | }) 27 | 28 | element.addEventListener('mouseleave', () => { 29 | setNewProps(props.initial) 30 | }) 31 | 32 | }) 33 | 34 | return ( 35 | <> 36 |
44 |
45 |
51 |
52 | 53 | ) 54 | } 55 | 56 | export default SquareOrRectangleCursors 57 | 58 | -------------------------------------------------------------------------------- /src/components/SquareOrRectangleCursors/styles.module.css: -------------------------------------------------------------------------------- 1 | .SquareOrRectangleCursorsMain ,.SquareOrRectangleCursorsMain::before { 2 | cursor: none; 3 | pointer-events: none; 4 | position: fixed; 5 | z-index: 999; 6 | height: 3rem; 7 | width: 3rem; 8 | transform: translate(-50%, -50%); 9 | transition:all 0.03s ease 0s, width 0.2s ease 0s, height 0.2s ease 0s; 10 | } 11 | 12 | .SquareOrRectangleCursorsInner { 13 | cursor: none; 14 | pointer-events: none; 15 | position: fixed; 16 | content: '⬢'; 17 | z-index: 999; 18 | height: 1rem; 19 | width: 1rem; 20 | transform: translate(-50%, -50%); 21 | background: black; 22 | transition:all 0.03s ease 0s, width 0.2s ease 0s, height 0.2s ease 0s; 23 | } -------------------------------------------------------------------------------- /src/components/index.jsx: -------------------------------------------------------------------------------- 1 | import CircleCursor from "./CircleCursor"; 2 | import EmojiCursor from "./EmojiCursor"; 3 | import SquareOrRectangleCursors from "./SquareOrRectangleCursors"; 4 | import ImageCursor from "./ImageCursor"; 5 | 6 | export { CircleCursor, EmojiCursor, ImageCursor, SquareOrRectangleCursors }; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export * from './components'; -------------------------------------------------------------------------------- /src/stories/Button.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import './button.css'; 4 | 5 | /** 6 | * Primary UI component for user interaction 7 | */ 8 | export const Button = ({ primary, backgroundColor, size, label, ...props }) => { 9 | const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary'; 10 | return ( 11 | 19 | ); 20 | }; 21 | 22 | Button.propTypes = { 23 | /** 24 | * Is this the principal call to action on the page? 25 | */ 26 | primary: PropTypes.bool, 27 | /** 28 | * What background color to use 29 | */ 30 | backgroundColor: PropTypes.string, 31 | /** 32 | * How large should the button be? 33 | */ 34 | size: PropTypes.oneOf(['small', 'medium', 'large']), 35 | /** 36 | * Button contents 37 | */ 38 | label: PropTypes.string.isRequired, 39 | /** 40 | * Optional click handler 41 | */ 42 | onClick: PropTypes.func, 43 | }; 44 | 45 | Button.defaultProps = { 46 | backgroundColor: null, 47 | primary: false, 48 | size: 'medium', 49 | onClick: undefined, 50 | }; 51 | -------------------------------------------------------------------------------- /src/stories/Button.stories.jsx: -------------------------------------------------------------------------------- 1 | import { Button } from './Button'; 2 | 3 | // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction 4 | export default { 5 | title: 'Example/Button', 6 | component: Button, 7 | tags: ['autodocs'], 8 | argTypes: { 9 | backgroundColor: { control: 'color' }, 10 | }, 11 | }; 12 | 13 | // More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args 14 | export const Primary = { 15 | args: { 16 | primary: true, 17 | label: 'Button', 18 | }, 19 | }; 20 | 21 | export const Secondary = { 22 | args: { 23 | label: 'Button', 24 | }, 25 | }; 26 | 27 | export const Large = { 28 | args: { 29 | size: 'large', 30 | label: 'Button', 31 | }, 32 | }; 33 | 34 | export const Small = { 35 | args: { 36 | size: 'small', 37 | label: 'Button', 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /src/stories/Cursors.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { CircleCursor, EmojiCursor, ImageCursor, SquareOrRectangleCursors } from "../components" 3 | import './cursors.css'; 4 | 5 | export const Cursors = (args) => { 6 | const [isloaded, setIsloaded] = useState(false) 7 | 8 | useEffect(() => { 9 | setIsloaded(true) 10 | }, []) 11 | console.log(args) 12 | return ( 13 | <> 14 |
15 |

Hover me

18 |
19 |
20 |

Hover me

23 |
24 | {!isloaded ?
Loading...
: } 25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/stories/Cursors.stories.jsx: -------------------------------------------------------------------------------- 1 | import { Cursors } from "./Cursors"; 2 | import { CircleCursor, EmojiCursor, ImageCursor, SquareOrRectangleCursors } from "../components"; 3 | import { render } from "react-dom"; 4 | 5 | export default { 6 | title: 'Cursors', 7 | component: Cursors, 8 | }; 9 | 10 | /* 11 | *👇 Render functions are a framework specific feature to allow you control on how the component renders. 12 | * See https://storybook.js.org/docs/react/api/csf 13 | * to learn how to use render functions. 14 | */ 15 | export const Primary = { 16 | args: {} 17 | }; 18 | export const Image = { 19 | render: () => { 20 | return ( 21 | <> 22 | ) 23 | } 24 | } 25 | export const Emoji = { 26 | render: () => { 27 | return ( 28 | <> 29 | ) 30 | } 31 | } 32 | export const SquareOrRectangle = { 33 | render: () => { 34 | return ( 35 | <> 36 | ) 37 | } 38 | } -------------------------------------------------------------------------------- /src/stories/Header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { Button } from './Button'; 5 | import './header.css'; 6 | 7 | export const Header = ({ user, onLogin, onLogout, onCreateAccount }) => ( 8 |
9 |
10 |
11 | 12 | 13 | 17 | 21 | 25 | 26 | 27 |

Acme

28 |
29 |
30 | {user ? ( 31 | <> 32 | 33 | Welcome, {user.name}! 34 | 35 |
44 |
45 |
46 | ); 47 | 48 | Header.propTypes = { 49 | user: PropTypes.shape({ 50 | name: PropTypes.string.isRequired, 51 | }), 52 | onLogin: PropTypes.func.isRequired, 53 | onLogout: PropTypes.func.isRequired, 54 | onCreateAccount: PropTypes.func.isRequired, 55 | }; 56 | 57 | Header.defaultProps = { 58 | user: null, 59 | }; 60 | -------------------------------------------------------------------------------- /src/stories/Header.stories.js: -------------------------------------------------------------------------------- 1 | import { Header } from './Header'; 2 | 3 | export default { 4 | title: 'Example/Header', 5 | component: Header, 6 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs 7 | tags: ['autodocs'], 8 | parameters: { 9 | // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout 10 | layout: 'fullscreen', 11 | }, 12 | }; 13 | 14 | export const LoggedIn = { 15 | args: { 16 | user: { 17 | name: 'Jane Doe', 18 | }, 19 | }, 20 | }; 21 | 22 | export const LoggedOut = {}; 23 | -------------------------------------------------------------------------------- /src/stories/Introduction.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/blocks'; 2 | import Code from './assets/code-brackets.svg'; 3 | import Colors from './assets/colors.svg'; 4 | import Comments from './assets/comments.svg'; 5 | import Direction from './assets/direction.svg'; 6 | import Flow from './assets/flow.svg'; 7 | import Plugin from './assets/plugin.svg'; 8 | import Repo from './assets/repo.svg'; 9 | import StackAlt from './assets/stackalt.svg'; 10 | 11 | 12 | 13 | 118 | 119 | # Welcome to Storybook 120 | 121 | Storybook helps you build UI components in isolation from your app's business logic, data, and context. 122 | That makes it easy to develop hard-to-reach states. Save these UI states as **stories** to revisit during development, testing, or QA. 123 | 124 | Browse example stories now by navigating to them in the sidebar. 125 | View their code in the `stories` directory to learn how they work. 126 | We recommend building UIs with a [**component-driven**](https://componentdriven.org) process starting with atomic components and ending with pages. 127 | 128 |
Configure
129 | 130 | 176 | 177 |
Learn
178 | 179 | 209 | 210 |
211 | TipEdit the Markdown in{' '} 212 | stories/Introduction.stories.mdx 213 |
214 | -------------------------------------------------------------------------------- /src/stories/Page.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Header } from './Header'; 4 | import './page.css'; 5 | 6 | export const Page = () => { 7 | const [user, setUser] = React.useState(); 8 | 9 | return ( 10 |
11 |
setUser({ name: 'Jane Doe' })} 14 | onLogout={() => setUser(undefined)} 15 | onCreateAccount={() => setUser({ name: 'Jane Doe' })} 16 | /> 17 | 18 |
19 |

Pages in Storybook

20 |

21 | We recommend building UIs with a{' '} 22 | 23 | component-driven 24 | {' '} 25 | process starting with atomic components and ending with pages. 26 |

27 |

28 | Render pages with mock data. This makes it easy to build and review page states without 29 | needing to navigate to them in your app. Here are some handy patterns for managing page 30 | data in Storybook: 31 |

32 |
    33 |
  • 34 | Use a higher-level connected component. Storybook helps you compose such data from the 35 | "args" of child component stories 36 |
  • 37 |
  • 38 | Assemble data in the page component from your services. You can mock these services out 39 | using Storybook. 40 |
  • 41 |
42 |

43 | Get a guided tutorial on component-driven development at{' '} 44 | 45 | Storybook tutorials 46 | 47 | . Read more in the{' '} 48 | 49 | docs 50 | 51 | . 52 |

53 |
54 | Tip Adjust the width of the canvas with the{' '} 55 | 56 | 57 | 62 | 63 | 64 | Viewports addon in the toolbar 65 |
66 |
67 |
68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /src/stories/Page.stories.js: -------------------------------------------------------------------------------- 1 | import { within, userEvent } from '@storybook/testing-library'; 2 | 3 | import { Page } from './Page'; 4 | 5 | export default { 6 | title: 'Example/Page', 7 | component: Page, 8 | parameters: { 9 | // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout 10 | layout: 'fullscreen', 11 | }, 12 | }; 13 | 14 | export const LoggedOut = {}; 15 | 16 | // More on interaction testing: https://storybook.js.org/docs/react/writing-tests/interaction-testing 17 | export const LoggedIn = { 18 | play: async ({ canvasElement }) => { 19 | const canvas = within(canvasElement); 20 | const loginButton = await canvas.getByRole('button', { 21 | name: /Log in/i, 22 | }); 23 | await userEvent.click(loginButton); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /src/stories/assets/code-brackets.svg: -------------------------------------------------------------------------------- 1 | illustration/code-brackets -------------------------------------------------------------------------------- /src/stories/assets/colors.svg: -------------------------------------------------------------------------------- 1 | illustration/colors -------------------------------------------------------------------------------- /src/stories/assets/comments.svg: -------------------------------------------------------------------------------- 1 | illustration/comments -------------------------------------------------------------------------------- /src/stories/assets/direction.svg: -------------------------------------------------------------------------------- 1 | illustration/direction -------------------------------------------------------------------------------- /src/stories/assets/flow.svg: -------------------------------------------------------------------------------- 1 | illustration/flow -------------------------------------------------------------------------------- /src/stories/assets/plugin.svg: -------------------------------------------------------------------------------- 1 | illustration/plugin -------------------------------------------------------------------------------- /src/stories/assets/repo.svg: -------------------------------------------------------------------------------- 1 | illustration/repo -------------------------------------------------------------------------------- /src/stories/assets/stackalt.svg: -------------------------------------------------------------------------------- 1 | illustration/stackalt -------------------------------------------------------------------------------- /src/stories/button.css: -------------------------------------------------------------------------------- 1 | .storybook-button { 2 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | font-weight: 700; 4 | border: 0; 5 | border-radius: 3em; 6 | cursor: pointer; 7 | display: inline-block; 8 | line-height: 1; 9 | } 10 | .storybook-button--primary { 11 | color: white; 12 | background-color: #1ea7fd; 13 | } 14 | .storybook-button--secondary { 15 | color: #333; 16 | background-color: transparent; 17 | box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset; 18 | } 19 | .storybook-button--small { 20 | font-size: 12px; 21 | padding: 10px 16px; 22 | } 23 | .storybook-button--medium { 24 | font-size: 14px; 25 | padding: 11px 20px; 26 | } 27 | .storybook-button--large { 28 | font-size: 16px; 29 | padding: 12px 24px; 30 | } 31 | -------------------------------------------------------------------------------- /src/stories/cursors.css: -------------------------------------------------------------------------------- 1 | .App-header { 2 | background-color: #282c34; 3 | width: 300px; 4 | height: 300px; 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-content: center; 9 | font-size: calc(10px + 2vmin); 10 | color: white; 11 | } 12 | 13 | .App-link { 14 | color: #61dafb; 15 | } -------------------------------------------------------------------------------- /src/stories/header.css: -------------------------------------------------------------------------------- 1 | .storybook-header { 2 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 4 | padding: 15px 20px; 5 | display: flex; 6 | align-items: center; 7 | justify-content: space-between; 8 | } 9 | 10 | .storybook-header svg { 11 | display: inline-block; 12 | vertical-align: top; 13 | } 14 | 15 | .storybook-header h1 { 16 | font-weight: 700; 17 | font-size: 20px; 18 | line-height: 1; 19 | margin: 6px 0 6px 10px; 20 | display: inline-block; 21 | vertical-align: top; 22 | } 23 | 24 | .storybook-header button + button { 25 | margin-left: 10px; 26 | } 27 | 28 | .storybook-header .welcome { 29 | color: #333; 30 | font-size: 14px; 31 | margin-right: 10px; 32 | } 33 | -------------------------------------------------------------------------------- /src/stories/page.css: -------------------------------------------------------------------------------- 1 | .storybook-page { 2 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | font-size: 14px; 4 | line-height: 24px; 5 | padding: 48px 20px; 6 | margin: 0 auto; 7 | max-width: 600px; 8 | color: #333; 9 | } 10 | 11 | .storybook-page h2 { 12 | font-weight: 700; 13 | font-size: 32px; 14 | line-height: 1; 15 | margin: 0 0 4px; 16 | display: inline-block; 17 | vertical-align: top; 18 | } 19 | 20 | .storybook-page p { 21 | margin: 1em 0; 22 | } 23 | 24 | .storybook-page a { 25 | text-decoration: none; 26 | color: #1ea7fd; 27 | } 28 | 29 | .storybook-page ul { 30 | padding-left: 30px; 31 | margin: 1em 0; 32 | } 33 | 34 | .storybook-page li { 35 | margin-bottom: 8px; 36 | } 37 | 38 | .storybook-page .tip { 39 | display: inline-block; 40 | border-radius: 1em; 41 | font-size: 11px; 42 | line-height: 12px; 43 | font-weight: 700; 44 | background: #e7fdd8; 45 | color: #66bf3c; 46 | padding: 4px 12px; 47 | margin-right: 10px; 48 | vertical-align: top; 49 | } 50 | 51 | .storybook-page .tip-wrapper { 52 | font-size: 13px; 53 | line-height: 20px; 54 | margin-top: 40px; 55 | margin-bottom: 40px; 56 | } 57 | 58 | .storybook-page .tip-wrapper svg { 59 | display: inline-block; 60 | height: 12px; 61 | width: 12px; 62 | margin-right: 4px; 63 | vertical-align: top; 64 | margin-top: 3px; 65 | } 66 | 67 | .storybook-page .tip-wrapper svg path { 68 | fill: #1ea7fd; 69 | } 70 | --------------------------------------------------------------------------------