├── example ├── .npmignore ├── index.html ├── tsconfig.json ├── package.json └── index.tsx ├── .gitignore ├── docs └── demo.gif ├── test └── index.test.tsx ├── tsconfig.json ├── .github └── workflows │ └── main.yml ├── LICENSE ├── package.json ├── src └── index.tsx └── README.md /example/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | dist -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .cache 5 | dist 6 | -------------------------------------------------------------------------------- /docs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoseRFelix/react-toggle-dark-mode/HEAD/docs/demo.gif -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Playground 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/index.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { DarkModeSwitch } from '../src'; 4 | 5 | describe('it renders', () => { 6 | it('renders without crashing', () => { 7 | const div = document.createElement('div'); 8 | ReactDOM.render( 9 | {}} checked={false} />, 10 | div 11 | ); 12 | ReactDOM.unmountComponentAtNode(div); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": false, 4 | "target": "es5", 5 | "module": "commonjs", 6 | "jsx": "react", 7 | "moduleResolution": "node", 8 | "noImplicitAny": false, 9 | "noUnusedLocals": false, 10 | "noUnusedParameters": false, 11 | "removeComments": true, 12 | "strictNullChecks": true, 13 | "preserveConstEnums": true, 14 | "sourceMap": true, 15 | "lib": ["es2015", "es2016", "dom"], 16 | "baseUrl": ".", 17 | "types": ["node"] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "types"], 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "lib": ["dom", "esnext"], 6 | "importHelpers": true, 7 | "declaration": true, 8 | "sourceMap": true, 9 | "rootDir": "./src", 10 | "strict": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "moduleResolution": "node", 16 | "baseUrl": "./", 17 | "paths": { 18 | "*": ["src/*", "node_modules/*"] 19 | }, 20 | "jsx": "react", 21 | "esModuleInterop": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "parcel index.html", 8 | "build": "parcel build index.html" 9 | }, 10 | "dependencies": { 11 | "react-app-polyfill": "^1.0.0" 12 | }, 13 | "alias": { 14 | "react": "../node_modules/react", 15 | "react-dom": "../node_modules/react-dom/profiling", 16 | "scheduler/tracing": "../node_modules/scheduler/tracing-profiling" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^16.9.11", 20 | "@types/react-dom": "^16.8.4", 21 | "parcel": "^1.12.3", 22 | "typescript": "^3.4.5" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | 7 | steps: 8 | - name: Begin CI... 9 | uses: actions/checkout@v2 10 | 11 | - name: Use Node 12 12 | uses: actions/setup-node@v1 13 | with: 14 | node-version: 12.x 15 | 16 | - name: Use cached node_modules 17 | uses: actions/cache@v1 18 | with: 19 | path: node_modules 20 | key: nodeModules-${{ hashFiles('**/yarn.lock') }} 21 | restore-keys: | 22 | nodeModules- 23 | 24 | - name: Install dependencies 25 | run: yarn install --frozen-lockfile 26 | env: 27 | CI: true 28 | 29 | - name: Lint 30 | run: yarn lint 31 | env: 32 | CI: true 33 | 34 | - name: Test 35 | run: yarn test --ci --coverage --maxWorkers=2 36 | env: 37 | CI: true 38 | 39 | - name: Build 40 | run: yarn build 41 | env: 42 | CI: true 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jose Felix 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.1.1", 3 | "license": "MIT", 4 | "main": "dist/index.js", 5 | "typings": "dist/index.d.ts", 6 | "files": [ 7 | "dist", 8 | "src" 9 | ], 10 | "engines": { 11 | "node": ">=10" 12 | }, 13 | "scripts": { 14 | "start": "tsdx watch", 15 | "build": "tsdx build", 16 | "test": "tsdx test --passWithNoTests", 17 | "lint": "tsdx lint", 18 | "prepare": "tsdx build" 19 | }, 20 | "peerDependencies": { 21 | "react": ">=16" 22 | }, 23 | "husky": { 24 | "hooks": { 25 | "pre-commit": "tsdx lint" 26 | } 27 | }, 28 | "prettier": { 29 | "printWidth": 80, 30 | "semi": true, 31 | "singleQuote": true, 32 | "trailingComma": "es5" 33 | }, 34 | "name": "react-toggle-dark-mode", 35 | "description": "Animated dark mode toggle as seen in blogs!", 36 | "author": "Jose R. Felix (https://jfelix.info)", 37 | "module": "dist/react-toggle-dark-mode.esm.js", 38 | "repository": { 39 | "type": "git", 40 | "url": "https://github.com/JoseRFelix/react-toggle-dark-mode" 41 | }, 42 | "bugs": { 43 | "url": "https://github.com/JoseRFelix/react-toggle-dark-mode/issues" 44 | }, 45 | "homepage": "https://github.com/JoseRFelix/react-toggle-dark-mode#readme", 46 | "devDependencies": { 47 | "@types/react": "^16.9.43", 48 | "@types/react-dom": "^16.9.8", 49 | "husky": "^4.2.5", 50 | "react": "^16.13.1", 51 | "react-dom": "^16.13.1", 52 | "tsdx": "^0.13.2", 53 | "tslib": "^2.0.0", 54 | "typescript": "^3.9.6" 55 | }, 56 | "dependencies": { 57 | "react-spring": "^9.0.0-rc.3" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /example/index.tsx: -------------------------------------------------------------------------------- 1 | import 'react-app-polyfill/ie11'; 2 | import * as React from 'react'; 3 | import * as ReactDOM from 'react-dom'; 4 | import { DarkModeSwitch } from '../.'; 5 | 6 | function arrayN(size: number) { 7 | return new Array(size).fill(undefined); 8 | } 9 | 10 | const App = () => { 11 | const [isDarkMode, setDarkMode] = React.useState(false); 12 | const [toggleAmount, setToggleAmount] = React.useState(0); 13 | 14 | const toggleDarkMode = (checked: boolean) => { 15 | setDarkMode(checked); 16 | }; 17 | 18 | const addToggle = () => { 19 | setToggleAmount(prevValue => prevValue + 1); 20 | }; 21 | 22 | return ( 23 |
34 | 40 | 46 | 53 | {arrayN(toggleAmount).map(() => ( 54 | 60 | ))} 61 | 62 |
63 | ); 64 | }; 65 | 66 | ReactDOM.render(, document.getElementById('root')); 67 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useSpring, animated } from 'react-spring'; 3 | 4 | export const defaultProperties = { 5 | dark: { 6 | circle: { 7 | r: 9, 8 | }, 9 | mask: { 10 | cx: '50%', 11 | cy: '23%', 12 | }, 13 | svg: { 14 | transform: 'rotate(40deg)', 15 | }, 16 | lines: { 17 | opacity: 0, 18 | }, 19 | }, 20 | light: { 21 | circle: { 22 | r: 5, 23 | }, 24 | mask: { 25 | cx: '100%', 26 | cy: '0%', 27 | }, 28 | svg: { 29 | transform: 'rotate(90deg)', 30 | }, 31 | lines: { 32 | opacity: 1, 33 | }, 34 | }, 35 | springConfig: { mass: 4, tension: 250, friction: 35 }, 36 | }; 37 | 38 | let REACT_TOGGLE_DARK_MODE_GLOBAL_ID = 0; 39 | 40 | type SVGProps = Omit, 'onChange'>; 41 | export interface Props extends SVGProps { 42 | onChange: (checked: boolean) => void; 43 | checked: boolean; 44 | style?: React.CSSProperties; 45 | size?: number | string; 46 | animationProperties?: typeof defaultProperties; 47 | moonColor?: string; 48 | sunColor?: string; 49 | } 50 | 51 | export const DarkModeSwitch: React.FC = ({ 52 | onChange, 53 | children, 54 | checked = false, 55 | size = 24, 56 | animationProperties = defaultProperties, 57 | moonColor = 'white', 58 | sunColor = 'black', 59 | style, 60 | ...rest 61 | }) => { 62 | const [id, setId] = React.useState(0); 63 | 64 | React.useEffect(() => { 65 | REACT_TOGGLE_DARK_MODE_GLOBAL_ID += 1; 66 | setId(REACT_TOGGLE_DARK_MODE_GLOBAL_ID); 67 | }, [setId]); 68 | 69 | const properties = React.useMemo(() => { 70 | if (animationProperties !== defaultProperties) { 71 | return Object.assign(defaultProperties, animationProperties); 72 | } 73 | 74 | return animationProperties; 75 | }, [animationProperties]); 76 | 77 | const { circle, svg, lines, mask } = properties[checked ? 'dark' : 'light']; 78 | 79 | const svgContainerProps = useSpring({ 80 | ...svg, 81 | config: animationProperties.springConfig, 82 | }); 83 | const centerCircleProps = useSpring({ 84 | ...circle, 85 | config: animationProperties.springConfig, 86 | }); 87 | const maskedCircleProps = useSpring({ 88 | ...mask, 89 | config: animationProperties.springConfig, 90 | }); 91 | const linesProps = useSpring({ 92 | ...lines, 93 | config: animationProperties.springConfig, 94 | }); 95 | 96 | const toggle = () => onChange(!checked); 97 | 98 | const uniqueMaskId = `circle-mask-${id}`; 99 | 100 | return ( 101 | 120 | 121 | 122 | 128 | 129 | 130 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | ); 150 | }; 151 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

React Toggle Dark Mode

3 |
4 |

5 | 6 | Version 7 | 8 | 9 | 10 | License: MIT 11 | 12 | 13 | PRs Welcome 14 | 15 | Bundle size 16 |

17 | 18 | > 🌃 Animated dark mode toggle as seen in blogs! 19 | 20 | ![Interactive sun and moon transition](./docs/demo.gif) 21 | 22 | ## Prerequisites 23 | 24 | - node >=10 25 | 26 | ## Installation 27 | 28 | ```shell 29 | npm i react-toggle-dark-mode 30 | ``` 31 | 32 | or with Yarn: 33 | 34 | ```shell 35 | yarn add react-toggle-dark-mode 36 | ``` 37 | 38 | ## Usage 39 | 40 | ```jsx 41 | import * as React from 'react'; 42 | import * as ReactDOM from 'react-dom'; 43 | import { DarkModeSwitch } from 'react-toggle-dark-mode'; 44 | 45 | const App = () => { 46 | const [isDarkMode, setDarkMode] = React.useState(false); 47 | 48 | const toggleDarkMode = (checked: boolean) => { 49 | setDarkMode(checked); 50 | }; 51 | 52 | return ( 53 | 59 | ); 60 | }; 61 | ``` 62 | 63 | ## API 64 | 65 | ### DarkModeSwitch 66 | 67 | #### Props 68 | 69 | | Name | Type | Default Value | Description | 70 | | ------------------- | ---------------------------- | ------------------------------- | ----------------------------------------- | 71 | | onChange | \(checked: boolean\) => void | | Event that triggers when icon is clicked. | 72 | | checked | boolean | false | Current icon state. | 73 | | style | Object | | CSS properties object. | 74 | | size | number | 24 | SVG size. | 75 | | animationProperties | Object | defaultProperties \(see below\) | Override default animation properties. | 76 | | moonColor | string | white | Color of the moon. | 77 | | sunColor | string | black | Color of the sun. | 78 | 79 | ### Default Animation Properties 80 | 81 | ```javascript 82 | const defaultProperties = { 83 | dark: { 84 | circle: { 85 | r: 9, 86 | }, 87 | mask: { 88 | cx: '50%', 89 | cy: '23%', 90 | }, 91 | svg: { 92 | transform: 'rotate(40deg)', 93 | }, 94 | lines: { 95 | opacity: 0, 96 | }, 97 | }, 98 | light: { 99 | circle: { 100 | r: 5, 101 | }, 102 | mask: { 103 | cx: '100%', 104 | cy: '0%', 105 | }, 106 | svg: { 107 | transform: 'rotate(90deg)', 108 | }, 109 | lines: { 110 | opacity: 1, 111 | }, 112 | }, 113 | springConfig: { mass: 4, tension: 250, friction: 35 }, 114 | }; 115 | ``` 116 | 117 | ## Contributors 118 | 119 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 |

Jose Felix

💻 📖 ⚠️
129 | 130 | 131 | 132 | 133 | 134 | 135 | This project follows the [all-contributors](https://allcontributors.org) specification. 136 | Contributions of any kind are welcome! 137 | 138 | ## Show your support 139 | 140 | Give a ⭐️ if this project helped you! 141 | --------------------------------------------------------------------------------