Skip to content

Commit e8a444d

Browse files
committed
Vue dark mode component
1 parent 11e5d77 commit e8a444d

File tree

1 file changed

+107
-96
lines changed

1 file changed

+107
-96
lines changed

src/vue-dark-mode.vue

Lines changed: 107 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,151 +1,162 @@
11
<template>
22
<button
3-
:aria-pressed="darkMode"
3+
:aria-label="`toggle to ${getNextMode} mode color`"
4+
:title="`toggle to ${getNextMode} mode color`"
45
class="vue-dark-mode"
5-
@click="toggleDarkMode"
6+
@click="toggleColorMode"
67
>
7-
<slot :isDark="darkMode" />
8-
<component
9-
:is="'style'"
10-
:media="darkMode && $_use_filter ? 'screen' : 'none'"
11-
scoped
12-
v-text="styles.trim()"
13-
/>
8+
<span
9+
class="visually-hidden"
10+
aria-live="assertive"
11+
>
12+
{{ chosenMode }} color mode is enabled
13+
</span>
14+
<slot :mode="chosenMode" />
1415
</button>
1516
</template>
1617

1718
<script>
18-
const styles = `
19-
html {
20-
background-color: #222 !important;
21-
color: #333 !important;
22-
}
23-
24-
body {
25-
filter: contrast(90%) invert(90%) hue-rotate(180deg) !important;
26-
-ms-filter: invert(100%);
27-
-webkit-filter: contrast(90%) invert(90%) hue-rotate(180deg) !important;
28-
}
29-
30-
input, textarea, select {
31-
color: purple;
32-
}
33-
34-
img, video, iframe, canvas, svg, embed[type='application/x-shockwave-flash'], object[type='application/x-shockwave-flash'], *[style*='url('] {
35-
filter: invert(100%) hue-rotate(-180deg) !important;
36-
-ms-filter: invert(100%) !important;
37-
-webkit-filter: invert(100%) hue-rotate(-180deg) !important;
38-
}
39-
`
40-
4119
export default {
4220
name: 'DarkMode',
4321
4422
props: {
45-
isDark: {
46-
type: Boolean,
47-
default: false
48-
},
49-
useFilter: {
50-
type: Boolean,
51-
default: true
52-
},
53-
styles: {
23+
defaultMode: {
5424
type: String,
55-
default: styles
25+
default: 'light'
5626
},
57-
className: {
58-
type: String,
59-
default: 'dark-mode'
27+
modes: {
28+
type: Array,
29+
default () {
30+
return ['light', 'dark', 'system']
31+
}
6032
},
61-
persist: {
33+
storage: {
6234
type: String,
6335
default: 'localStorage'
6436
},
65-
themeColorLight: {
66-
type: String,
67-
default: '#f2f2f2'
68-
},
69-
themColorDark: {
70-
type: String,
71-
default: '#999'
37+
mobileThemeColor: {
38+
type: Object,
39+
default () {
40+
return {
41+
light: '#f2f2f2',
42+
dark: '#999'
43+
}
44+
}
7245
}
7346
},
7447
7548
data () {
7649
return {
77-
darkMode: false,
7850
themeColorMeta: null,
79-
$_use_filter: false
51+
chosenMode: null,
52+
currentMode: null,
53+
listenerDark: null
8054
}
8155
},
8256
83-
created () {
84-
this.darkMode = this.isDark
85-
if (!this.$isServer) {
86-
this.darkMode = !!window[this.persist].getItem('darkMode') || this.isDark || this.prefersDark()
87-
this.themeColorMeta = document.querySelector('meta[name="theme-color"]')
88-
if (this.useFilter) {
89-
this.$_use_filter = this.supportsFilters()
90-
}
91-
if (this.darkMode) this.setDarkMode()
57+
computed: {
58+
getPrefersColorScheme () {
59+
const colorSchemeTypes = ['dark', 'light']
60+
let colorScheme = null
61+
colorSchemeTypes.forEach(type => {
62+
if (this.getMediaQueryList(type).matches) {
63+
colorScheme = type
64+
}
65+
})
66+
return colorScheme
67+
},
68+
69+
getNextMode () {
70+
const currentIndex = this.modes.findIndex(mode => mode === this.chosenMode)
71+
return this.modes[currentIndex === (this.modes.length - 1) ? 0 : currentIndex + 1]
72+
},
73+
74+
getStorageColorMode () {
75+
return window[this.storage].getItem('colorMode')
76+
},
77+
78+
isSystem () {
79+
return this.getStorageColorMode === 'system'
9280
}
9381
},
9482
95-
methods: {
96-
supportsFilters () {
97-
const div = document.createElement('div')
98-
const isSupported = 'filter' in div.style
99-
if (!isSupported) console.warn('CSS filter is not supported')
100-
return isSupported
101-
},
83+
beforeMount () {
84+
if (this.getPrefersColorScheme && this.isSystem) {
85+
this.currentMode = this.getPrefersColorScheme
86+
return this.setMode('system')
87+
}
88+
const colorMode = this.getStorageColorMode || this.defaultMode
89+
this.currentMode = colorMode
90+
this.setMode(colorMode)
91+
},
10292
103-
prefersDark () {
104-
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
93+
mounted () {
94+
this.metaThemeColor = document.querySelector('meta[name="theme-color"]')
95+
this.listenerDark = this.getMediaQueryList('dark')
96+
this.listenerDark.addListener(this.handlePreferColorScheme)
97+
},
98+
99+
beforeDestroy () {
100+
this.listenerDark.removeListener(this.handlePreferColorScheme)
101+
},
102+
103+
methods: {
104+
setMode (chosenMode) {
105+
this.chosenMode = chosenMode
106+
window[this.storage].setItem('colorMode', this.chosenMode)
107+
this.handleClassList('add', `${this.currentMode}-mode`)
108+
this.setMetaThemeColor(this.mobileThemeColor[this.currentMode] || this.mobileThemeColor[this.getPrefersColorScheme] || '#fff')
105109
},
106110
107-
setThemeColor (color) {
108-
if (this.themeColorMeta) return this.themeColorMeta.setAttribute('content', color)
111+
getMediaQueryList (type) {
112+
return window.matchMedia(`(prefers-color-scheme: ${type})`)
109113
},
110114
111-
toggleClass () {
112-
document.documentElement.classList.toggle(this.className)
115+
setMetaThemeColor (color) {
116+
this.$nextTick(() => {
117+
if (this.metaThemeColor) this.metaThemeColor.setAttribute('content', color)
118+
})
113119
},
114120
115-
setDarkMode () {
116-
window[this.persist].setItem('darkMode', 'on')
117-
this.toggleClass()
118-
this.setThemeColor(this.themeColorDark)
121+
handleClassList (action, cls) {
122+
return document.documentElement.classList[action](cls)
119123
},
120124
121-
removeDarkMode () {
122-
window[this.persist].removeItem('darkMode')
123-
this.toggleClass()
124-
this.setThemeColor(this.themeColorLight)
125+
handlePreferColorScheme (e) {
126+
this.currentMode = this.isSystem && e.matches ? 'dark' : 'light'
127+
this.handleClassList('remove', `${this.currentMode}-mode`)
128+
this.setMode('system')
125129
},
126130
127-
toggleDarkMode () {
128-
this.darkMode = !this.darkMode
129-
if (this.darkMode) return this.setDarkMode()
130-
this.removeDarkMode()
131+
toggleColorMode () {
132+
const selectedMode = this.getNextMode
133+
this.handleClassList('remove', `${this.currentMode}-mode`)
134+
this.currentMode = selectedMode === 'system' ? this.getPrefersColorScheme : selectedMode
135+
this.setMode(selectedMode)
131136
}
132137
}
133138
}
134139
</script>
135140

136141
<style>
137-
body {
138-
text-rendering: optimizeSpeed;
139-
image-rendering: optimizeSpeed;
140-
-webkit-font-smoothing: antialiased;
141-
-webkit-image-rendering: optimizeSpeed;
142-
}
143-
144142
.vue-dark-mode {
145143
appearance: none;
146144
background-color: transparent;
147145
color: inherit;
148146
border: none;
149147
cursor: pointer;
150148
}
149+
150+
.visually-hidden {
151+
position: absolute;
152+
white-space: nowrap;
153+
width: 1px;
154+
height: 1px;
155+
overflow: hidden;
156+
border: 0;
157+
padding: 0;
158+
clip: rect(0 0 0 0);
159+
clip-path: inset(50%);
160+
margin: -1px;
161+
}
151162
</style>

0 commit comments

Comments
 (0)