├── .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 | 
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 |
40 |
41 | Wrap emojis in a class
42 | 🌝🌚
43 |
44 |
45 |
46 | )
47 | ```
48 |
49 | ## 🍱 Props
50 |
51 | | Props | Type | Optional | Default | What it does |
52 | | :-- | --- | --- | --- | --- |
53 | | `backgroundColor` | `string` | Yes | `'white'` | Background color of your site, used to compute the dark color while preserving the contrast to your foreground |
54 | | `type` | `'default'`, `'hipster'` | Yes | `'default'` | `'default'` uses opacity (see gif [here](https://i.imgur.com/CsEehnx.gif)), `'hipster'` uses the expansion effect (gif above) |
55 | | `style` | `object` | Yes | `undefined` | In case you need to move the toggle, use this prop to set position on the screen |
56 |
--------------------------------------------------------------------------------