├── 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 | Add toggle
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 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | > 🌃 Animated dark mode toggle as seen in blogs!
19 |
20 | 
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 |
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 |
--------------------------------------------------------------------------------