`:
32 |
33 | ```html
34 |
35 |
36 |
37 | ```
38 |
39 | Use in [React](https://github.com/facebook/react):
40 |
41 | ```jsx
42 | import React from 'react';
43 | import '@wcj/dark-mode';
44 |
45 | function Demo() {
46 | return (
47 |
48 |
49 |
50 | );
51 | }
52 | ```
53 |
54 | Toggle in JavaScript:
55 |
56 | ```js
57 | const toggle = document.querySelector('dark-mode');
58 | const button = document.createElement('button');
59 | button.textContent = 'Change Theme';
60 | button.onclick = () => {
61 | const theme = document.documentElement.dataset.colorMode;
62 | // or => const theme = toggle.mode
63 | document.documentElement.setAttribute('data-color-mode', theme === 'light' ? 'dark' : 'light');
64 | }
65 | document.body.appendChild(button);
66 | // Listen for toggle changes
67 | // and toggle the `dark` class accordingly.
68 | document.addEventListener('colorschemechange', (e) => {
69 | console.log(`Color scheme changed to "${e.detail.colorScheme}" or "${toggle.mode}".`);
70 | button.textContent = toggle.mode === 'dark' ? 'Change Theme 🌞' : 'Change Theme 🌒';
71 | });
72 | ```
73 |
74 | Interacting with the custom element:
75 |
76 | ```js
77 | const darkMode = document.querySelector('dark-mode');
78 |
79 | // Set the mode to dark
80 | darkMode.mode = 'dark';
81 | // Set the mode to light
82 | darkMode.mode = 'light';
83 | // Set the mode to dark
84 | document.documentElement.setAttribute('data-color-mode', 'dark');
85 | // Set the mode to light
86 | document.documentElement.setAttribute('data-color-mode', 'light');
87 |
88 | // Set the light label to "off"
89 | darkMode.light = 'off';
90 | // Set the dark label to "on"
91 | darkMode.dark = 'on';
92 |
93 | // Set a "remember the last selected mode" label
94 | darkMode.permanent = true;
95 |
96 | // Remember the user's last color scheme choice
97 | darkModeToggle.setAttribute('permanent', false);
98 | // Forget the user's last color scheme choice
99 | darkModeToggle.removeAttribute('permanent');
100 | ```
101 |
102 | Reacting on color scheme changes:
103 |
104 | ```js
105 | /* On the page */
106 | document.addEventListener('colorschemechange', (e) => {
107 | console.log(`Color scheme changed to ${e.detail.colorScheme}.`);
108 | });
109 | ```
110 |
111 | Reacting on "remember the last selected mode" functionality changes:
112 |
113 | ```js
114 | /* On the page */
115 | document.addEventListener('permanentcolorscheme', (e) => {
116 | console.log(`${e.detail.permanent ? 'R' : 'Not r'}emembering the last selected mode.`);
117 | });
118 | ```
119 |
120 | ## Hide Text and Icons
121 |
122 | You can use the following CSS selectors to hide the text and icon parts of the `dark-mode` component:
123 |
124 | ```css
125 | dark-mode::part(text) {
126 | display: none;
127 | }
128 | dark-mode::part(icon) {
129 | display: none;
130 | }
131 | ```
132 |
133 | ## Properties
134 |
135 | Properties can be set directly on the custom element at creation time, or dynamically via JavaScript.
136 |
137 | ```typescript
138 | export type ColorScheme = 'light' | 'dark';
139 | export class DarkMode extends HTMLElement {
140 | mode?: ColorScheme;
141 | /**
142 | * Defaults to not remember the last choice.
143 | * If present remembers the last selected mode (`dark` or `light`),
144 | * which allows the user to permanently override their usual preferred color scheme.
145 | */
146 | permanent?: boolean;
147 | /**
148 | * Any string value that represents the label for the "dark" mode.
149 | */
150 | dark?: string;
151 | /**
152 | * Any string value that represents the label for the "light" mode.
153 | */
154 | light?: string;
155 | style?: React.CSSProperties;
156 | }
157 | ```
158 |
159 | ## Events
160 |
161 | - `colorschemechange`: Fired when the color scheme gets changed.
162 | - `permanentcolorscheme`: Fired when the color scheme should be permanently remembered or not.
163 |
164 | ## Alternatives
165 |
166 | - [dark-mode-toggle](https://github.com/GoogleChromeLabs/dark-mode-toggle)
A custom element that allows you to easily put a Dark Mode 🌒 toggle or switch on your site
167 | - [Darkmode.js](https://github.com/sandoche/Darkmode.js)
Add a dark-mode / night-mode to your website in a few seconds
168 | - [darken](https://github.com/ColinEspinas/darken)
Dark mode made easy
169 | - [use-dark-mode](https://github.com/donavon/use-dark-mode)
A custom React Hook to help you implement a "dark mode" component.
170 | - [Dark Mode Switch](https://github.com/coliff/dark-mode-switch)
Add a dark-mode theme toggle with a Bootstrap Custom Switch
171 |
172 | ## Contributors
173 |
174 | As always, thanks to our amazing contributors!
175 |
176 |
177 |
178 |
179 |
180 | Made with [github-action-contributors](https://github.com/jaywcjlove/github-action-contributors).
181 |
182 | ## License
183 |
184 | Licensed under the [MIT License](https://opensource.org/licenses/MIT).
185 |
--------------------------------------------------------------------------------
/demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | dark-mode
8 |
9 |
10 |
11 |
12 |
20 |
21 |
22 |
23 |
24 | Hi there!
25 | I'm your cool new webpage! Use the toggle in the 👈 button to switch my theme.
26 |
27 |
28 |
29 | Github: @jaywcjlove/dark-mode
30 |
31 |
32 |
52 |
53 |
54 |
55 |
56 |
57 | 👈 The mode is remembered after clicking it.
58 |
59 |
68 |
69 | loading...
70 |
71 |
72 |
82 |
83 |
--------------------------------------------------------------------------------
/main.d.ts:
--------------------------------------------------------------------------------
1 | export type ColorScheme = 'light' | 'dark';
2 | export type ColorSchemeChangeEvent = CustomEvent<{ colorScheme: ColorScheme }>;
3 | export type PermanentColorSchemeEvent = CustomEvent<{ colorScheme: ColorScheme, permanent: boolean }>;
4 |
5 | export class DarkMode extends HTMLElement {
6 | mode?: ColorScheme;
7 | /**
8 | * Defaults to not remember the last choice.
9 | * If present remembers the last selected mode (`dark` or `light`),
10 | * which allows the user to permanently override their usual preferred color scheme.
11 | */
12 | permanent?: boolean;
13 | /**
14 | * Any string value that represents the label for the "dark" mode.
15 | */
16 | dark?: string;
17 | /**
18 | * Any string value that represents the label for the "light" mode.
19 | */
20 | light?: string;
21 | }
22 |
23 | declare global {
24 | interface HTMLElementTagNameMap {
25 | 'dark-mode': DarkMode;
26 | }
27 | interface GlobalEventHandlersEventMap {
28 | /**
29 | * Fired when the color scheme gets changed.
30 | *
31 | * ```js
32 | * const toggle = document.querySelector('dark-mode');
33 | * document.addEventListener('colorschemechange', (e) => {
34 | * console.log(`Color scheme changed to "${e.detail.colorScheme}".`);
35 | * console.log(toggle.mode === 'dark' ? 'Change Theme 🌞' : 'Change Theme 🌒')
36 | * });
37 | * ```
38 | */
39 | 'colorschemechange': ColorSchemeChangeEvent;
40 | /**
41 | * Fired when the color scheme should be permanently remembered or not.
42 | *
43 | * ```js
44 | * document.addEventListener('permanentcolorscheme', (e) => {
45 | * console.log(`~: Color scheme changed to "${e.detail.colorScheme}" , "${e.detail.permanent}" .`);
46 | * });
47 | * ```
48 | */
49 | 'permanentcolorscheme': PermanentColorSchemeEvent;
50 | }
51 | namespace JSX {
52 | interface IntrinsicElements {
53 | 'dark-mode': Partial | {
54 | style?: Partial | React.CSSProperties;
55 | };
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @package @wcj/dark-mode
3 | * Web Component that toggles dark mode 🌒
4 | * Github: https://github.com/jaywcjlove/dark-mode.git
5 | * Website: https://jaywcjlove.github.io/dark-mode
6 | *
7 | * Licensed under the MIT license.
8 | * @license Copyright © 2022. Licensed under the MIT License
9 | * @author kenny wong
10 | */
11 | const doc = document;
12 | const LOCAL_NANE = '_dark_mode_theme_'
13 | const PERMANENT = 'permanent';
14 | const COLOR_SCHEME_CHANGE = 'colorschemechange';
15 | const PERMANENT_COLOR_SCHEME = 'permanentcolorscheme';
16 | const LIGHT = 'light';
17 | const DARK = 'dark';
18 |
19 | // See https://html.spec.whatwg.org/multipage/common-dom-interfaces.html ↵
20 | // #reflecting-content-attributes-in-idl-attributes.
21 | const installStringReflection = (obj, attrName, propName = attrName) => {
22 | Object.defineProperty(obj, propName, {
23 | enumerable: true,
24 | get() {
25 | const value = this.getAttribute(attrName);
26 | return value === null ? '' : value;
27 | },
28 | set(v) {
29 | this.setAttribute(attrName, v);
30 | },
31 | });
32 | };
33 |
34 | const installBoolReflection = (obj, attrName, propName = attrName) => {
35 | Object.defineProperty(obj, propName, {
36 | enumerable: true,
37 | get() {
38 | return this.hasAttribute(attrName);
39 | },
40 | set(v) {
41 | if (v) {
42 | this.setAttribute(attrName, '');
43 | } else {
44 | this.removeAttribute(attrName);
45 | }
46 | },
47 | });
48 | };
49 |
50 | class DarkMode extends HTMLElement {
51 | static get observedAttributes() {
52 | return ['mode', LIGHT, DARK, PERMANENT];
53 | }
54 | LOCAL_NANE = LOCAL_NANE;
55 | constructor() {
56 | super();
57 | this._initializeDOM();
58 | }
59 | connectedCallback() {
60 | installStringReflection(this, 'mode');
61 | installStringReflection(this, DARK);
62 | installStringReflection(this, LIGHT);
63 | installBoolReflection(this, PERMANENT);
64 |
65 | const rememberedValue = localStorage.getItem(LOCAL_NANE);
66 | if (rememberedValue && [LIGHT, DARK].includes(rememberedValue)) {
67 | this.mode = rememberedValue;
68 | this.permanent = true;
69 | }
70 | if (this.permanent && !rememberedValue) {
71 | localStorage.setItem(LOCAL_NANE, this.mode);
72 | }
73 | const hasNativePrefersColorScheme = [LIGHT, DARK].includes(rememberedValue);
74 |
75 | if (this.permanent && rememberedValue) {
76 | this._changeThemeTag();
77 | } else {
78 | if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
79 | this.mode = DARK;
80 | this._changeThemeTag();
81 | }
82 | if (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) {
83 | this.mode = LIGHT;
84 | this._changeThemeTag();
85 | }
86 | }
87 | if (!this.permanent && !hasNativePrefersColorScheme) {
88 | window.matchMedia('(prefers-color-scheme: light)').onchange = (event) => {
89 | this.mode = event.matches ? LIGHT : DARK;
90 | this._changeThemeTag();
91 | }
92 | window.matchMedia('(prefers-color-scheme: dark)').onchange = (event) => {
93 | this.mode = event.matches ? DARK : LIGHT;
94 | this._changeThemeTag();
95 | }
96 | }
97 | const observer = new MutationObserver((mutationsList, observer) => {
98 | this.mode = doc.documentElement.dataset.colorMode;
99 | if (this.permanent && hasNativePrefersColorScheme) {
100 | localStorage.setItem(LOCAL_NANE, this.mode);
101 | this._dispatchEvent(PERMANENT_COLOR_SCHEME, {
102 | permanent: this.permanent,
103 | });
104 | }
105 | this._changeContent();
106 | this._dispatchEvent(COLOR_SCHEME_CHANGE, { colorScheme: this.mode });
107 | });
108 | // Start observing the target node with the above configuration
109 | observer.observe(doc.documentElement, { attributes: true });
110 | // After that, stop observing
111 | // observer.disconnect();
112 | this._dispatchEvent(COLOR_SCHEME_CHANGE, { colorScheme: this.mode });
113 |
114 | this._changeContent();
115 | }
116 | attributeChangedCallback(name, oldValue, newValue) {
117 | if (name === 'mode' && oldValue !== newValue && [LIGHT, DARK].includes(newValue)) {
118 | const rememberedValue = localStorage.getItem(LOCAL_NANE);
119 | if (this.mode === rememberedValue) {
120 | this.mode = newValue;
121 | this._changeContent();
122 | this._changeThemeTag();
123 | } else if (this.mode && this.mode !== rememberedValue) {
124 | this._changeContent();
125 | this._changeThemeTag();
126 | }
127 | } else if ((name === LIGHT || name === DARK) && oldValue !== newValue) {
128 | this._changeContent();
129 | }
130 | if (name === 'permanent' && typeof this.permanent === 'boolean') {
131 | this.permanent ? localStorage.setItem(LOCAL_NANE, this.mode) : localStorage.removeItem(LOCAL_NANE);
132 | }
133 | }
134 | _changeThemeTag() {
135 | doc.documentElement.setAttribute('data-color-mode', this.mode);
136 | }
137 | _changeContent() {
138 | this.icon.textContent = this.mode === LIGHT ? '🌒' : '🌞';
139 | this.text.textContent = this.mode === LIGHT ? this.getAttribute(DARK) : this.getAttribute(LIGHT);
140 | if (!this.text.textContent && this.text.parentElement && this.text) {
141 | this.text.parentElement.removeChild(this.text)
142 | }
143 | }
144 | _initializeDOM() {
145 | var shadow = this.attachShadow({ mode: 'open' });
146 | this.label = doc.createElement('span');
147 | this.label.setAttribute('class', 'wrapper');
148 | this.label.onclick = () => {
149 | this.mode = this.mode === LIGHT ? DARK : LIGHT;
150 | if (this.permanent) {
151 | localStorage.setItem(LOCAL_NANE, this.mode);
152 | }
153 | this._changeThemeTag();
154 | this._changeContent();
155 | }
156 | shadow.appendChild(this.label);
157 | this.icon = doc.createElement('span');
158 | this.icon.part = 'icon';
159 | this.label.appendChild(this.icon);
160 |
161 | this.text = doc.createElement('span');
162 | this.text.part = 'text';
163 | this.label.appendChild(this.text);
164 |
165 | const textContent = `
166 | [data-color-mode*='dark'], [data-color-mode*='dark'] body {
167 | color-scheme: dark;
168 | --color-theme-bg: #0d1117;
169 | --color-theme-text: #c9d1d9;
170 | background-color: var(--color-theme-bg);
171 | color: var(--color-theme-text);
172 | }
173 |
174 | [data-color-mode*='light'], [data-color-mode*='light'] body {
175 | color-scheme: light;
176 | --color-theme-bg: #fff;
177 | --color-theme-text: #24292f;
178 | background-color: var(--color-theme-bg);
179 | color: var(--color-theme-text);
180 | }`;
181 |
182 | const STYLE_ID = '_dark_mode_style_';
183 | const styleDom = doc.getElementById(STYLE_ID);
184 |
185 | if (!styleDom) {
186 | var initstyle = doc.createElement('style');
187 | initstyle.id = STYLE_ID;
188 | initstyle.textContent = textContent;
189 | doc.head.appendChild(initstyle);
190 | }
191 |
192 | var style = doc.createElement('style');
193 | style.textContent = `
194 | .wrapper { cursor: pointer; user-select: none; position: relative; }
195 | .wrapper > span + span { margin-left: .4rem; }
196 | `;
197 | shadow.appendChild(style);
198 | }
199 | _dispatchEvent(type, value) {
200 | this.dispatchEvent(new CustomEvent(type, {
201 | bubbles: true,
202 | composed: true,
203 | detail: value,
204 | }));
205 | }
206 | }
207 |
208 | customElements.define('dark-mode', DarkMode);
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@wcj/dark-mode",
3 | "version": "1.1.0",
4 | "description": "Web Component that toggles dark mode 🌒",
5 | "homepage": "https://jaywcjlove.github.io/dark-mode",
6 | "funding": "https://jaywcjlove.github.io/#/sponsor",
7 | "author": "jaywcjlove",
8 | "license": "MIT",
9 | "main": "./main.js",
10 | "browser": "./dist/dark-mode.min.js",
11 | "module": "./dist/dark-mode.min.js",
12 | "exports": "./dist/dark-mode.min.js",
13 | "unpkg": "./dist/dark-mode.min.js",
14 | "types": "./main.d.ts",
15 | "scripts": {
16 | "clean": "shx rm -rf ./dist && mkdir dist",
17 | "build": "npm run clean && npx terser ./main.js --toplevel --mangle-props regex=\\\"^_\\\" --comments /@license/ --ecma=8 -o ./dist/dark-mode.min.js"
18 | },
19 | "repository": {
20 | "type": "git",
21 | "url": "git+https://github.com/jaywcjlove/dark-mode.git"
22 | },
23 | "files": [
24 | "main.d.ts",
25 | "main.js",
26 | "dist"
27 | ],
28 | "keywords": [
29 | "dark",
30 | "mode"
31 | ],
32 | "devDependencies": {
33 | "shx": "^0.3.4",
34 | "terser": "^5.10.0"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "config:base"
4 | ],
5 | "packageRules": [
6 | {
7 | "matchPackagePatterns": ["*"],
8 | "rangeStrategy": "replace"
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------