├── .gitignore ├── .postcssrc ├── babel.config.js ├── src ├── index.html ├── useTheme.js ├── index.js └── s.module.scss ├── rollup.config.js ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | dist/ 3 | .cache/ 4 | node_modules/ -------------------------------------------------------------------------------- /.postcssrc: -------------------------------------------------------------------------------- 1 | { 2 | "modules": true, 3 | "plugins": { 4 | "autoprefixer": { 5 | "grid": true 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | 4 | const presets = ["@babel/preset-react", ["@babel/env", { modules: false }]]; 5 | 6 | return { 7 | presets 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Empty project 5 | 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from "rollup-plugin-node-resolve"; 2 | import babel from "rollup-plugin-babel"; 3 | import postcss from "rollup-plugin-postcss"; 4 | 5 | export default { 6 | input: "src/index.js", 7 | output: { 8 | file: "dist/bundle.js", 9 | format: "cjs" 10 | }, 11 | plugins: [ 12 | resolve({ 13 | // pass custom options to the resolve plugin 14 | customResolveOptions: { 15 | moduleDirectory: "node_modules" 16 | } 17 | }), 18 | babel({ 19 | exclude: "node_modules/**" // only transpile our source code 20 | }), 21 | postcss({ 22 | modules: true, 23 | plugins: [] 24 | }) 25 | ], 26 | // indicate which modules should be treated as external 27 | external: ["react"] 28 | }; 29 | -------------------------------------------------------------------------------- /src/useTheme.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | export const THEMES = ["sun", "moon"]; 4 | const getNewTheme = currentTheme => 5 | THEMES.indexOf(currentTheme) >= 0 6 | ? THEMES[1 - THEMES.indexOf(currentTheme)] 7 | : THEMES[0]; 8 | 9 | export const useTheme = () => { 10 | const themeFromLocalStorage = 11 | typeof window !== "undefined" ? window.localStorage.getItem("theme") : null; 12 | const [theme, setTheme] = useState(themeFromLocalStorage || THEMES[0]); 13 | const toggleTheme = currentTheme => { 14 | const newTheme = getNewTheme(currentTheme); 15 | setTheme(newTheme); 16 | }; 17 | useEffect(() => { 18 | typeof window !== "undefined" && 19 | window.localStorage.setItem("theme", theme); 20 | }, [theme]); 21 | return { theme, toggleTheme }; 22 | }; 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sun-moon-toggle", 3 | "version": "1.1.1", 4 | "main": "dist/bundle.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "clean": "rm dist/bundle.js", 8 | "start": "parcel src/index.html", 9 | "build": "rollup -c rollup.config.js" 10 | }, 11 | "peerDependencies": { 12 | "react": "^17.0.1", 13 | "react-dom": "^17.0.1" 14 | }, 15 | "devDependencies": { 16 | "@babel/core": "^7.4.5", 17 | "@babel/preset-env": "^7.4.5", 18 | "@babel/preset-react": "^7.0.0", 19 | "autoprefixer": "^9.5.1", 20 | "node-sass": "^4.12.0", 21 | "parcel-bundler": "^1.12.3", 22 | "postcss-modules": "^1.4.1", 23 | "rollup": "^1.12.3", 24 | "rollup-plugin-babel": "^4.3.2", 25 | "rollup-plugin-node-resolve": "^5.0.3", 26 | "rollup-plugin-postcss": "^2.0.3", 27 | "sass": "^1.20.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useTheme } from "./useTheme"; 3 | import s from "./s.module.scss"; 4 | 5 | const THEMES = ["sun", "moon"]; 6 | const getNewTheme = currentTheme => 7 | THEMES.indexOf(currentTheme) >= 0 8 | ? THEMES[1 - THEMES.indexOf(currentTheme)] 9 | : THEMES[0]; 10 | 11 | const TYPE = { 12 | DEFAULT: "default", 13 | HIPSTER: "hipster" 14 | }; 15 | 16 | const SunMoonToggle = ({ backgroundColor, type = TYPE.DEFAULT, ...props }) => { 17 | const { theme, toggleTheme } = useTheme(); 18 | 19 | return ( 20 | 21 |
26 |
33 | 34 | ); 35 | }; 36 | 37 | export default SunMoonToggle; 38 | -------------------------------------------------------------------------------- /src/s.module.scss: -------------------------------------------------------------------------------- 1 | :global { 2 | .emoji { 3 | isolation: isolate; 4 | display: inline-block; 5 | } 6 | img, 7 | iframe, 8 | video { 9 | isolation: isolate; 10 | } 11 | } 12 | :local { 13 | .blender { 14 | background-color: white; 15 | position: fixed; 16 | right: calc(50% - 40rem / 2 - 4rem); 17 | bottom: 1rem; 18 | mix-blend-mode: exclusion; 19 | pointer-events: none; 20 | transition: all ease 0.5s; 21 | @media screen and (max-width: 768px) { 22 | right: 1rem; 23 | } 24 | } 25 | .expansion { 26 | width: 3rem; 27 | height: 3rem; 28 | border-radius: 50%; 29 | } 30 | .expansion.moon { 31 | transform: scale(90); 32 | } 33 | .opacity { 34 | width: 100vw; 35 | height: 100vh; 36 | opacity: 0; 37 | top: 0; 38 | left: 0; 39 | } 40 | .opacity.moon { 41 | opacity: 1; 42 | } 43 | .toggle { 44 | background: #222; 45 | width: 3rem; 46 | height: 3rem; 47 | position: fixed; 48 | border-radius: 50%; 49 | right: calc(50% - 40rem / 2 - 4rem); 50 | bottom: 1rem; 51 | cursor: pointer; 52 | transition: all ease 0.5s; 53 | @media screen and (max-width: 768px) { 54 | right: 1rem; 55 | opacity: 0.8; 56 | } 57 | } 58 | .toggle.sun { 59 | transition: all ease 0.5s; 60 | z-index: 1; 61 | @media screen and (max-width: 768px) { 62 | opacity: 0.2; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🌚🌝 Sun Moon Toggle 2 | 3 | ![](https://i.imgur.com/vzexItp.gif) 4 | 5 | ## 🦄 How it works 6 | 7 | The CSS property `mix-blend-mode` ([browser support](https://caniuse.com/#feat=css-mixblendmode)) specifies how colors blend when graphics are stacked together. In brief, covering your site with a layer same as your background color and using `mix-blend-mode: difference` will automatically yield a black background while preserving the contrast with the foreground. 8 | 9 | We're using `mix-blend-mode: exclusion` which is a lower contrast version of `mix-blend-mode: difference`. 10 | 11 | Site that uses this: 12 | 13 | - [A Work in Progress](https://dev.wgao19.cc) 14 | 15 | You may read more about it in the following articles: 16 | 17 | - [Sun Moon Blending Mode](https://dev.wgao19.cc/2019-05-04__sun-moon-blending-mode/) 18 | - [Friends don't let friends implement dark mode alone](https://www.chenhuijing.com/blog/friends-dont-let-friends-implement-dark-mode-alone/#%F0%9F%8E%AE) 19 | 20 | ## 🛠 Installation 21 | 22 | ```shell 23 | $ yarn add sun-moon-toggle 24 | ``` 25 | 26 | ## 🦊 Example 27 | 28 | ```jsx 29 | import React from 'react' 30 | import SunMoonToggle from 'sun-moon-toggle' 31 | 32 | const Layout = () => ( 33 |
34 | {/* put it before other content */} 35 | 36 |
37 |

Hello, it's me

38 | 39 |