Customization
Style your site in dark mode.
Now that dark mode is a first-class feature of many operating systems, it’s becoming more and more common to design a dark version of your website to go along with the default design.
To make this as easy as possible, Tailwind and Preline UI include a dark
variant that lets you style your site differently when dark mode is enabled.
If you want to support toggling dark mode manually instead of relying on the operating system preference, use the class
strategy:
// tailwind.config.js
module.exports = {
darkMode: 'class',
// ...
}
If you want to rely on the browser's color scheme preference, use the media
strategy:
// tailwind.config.js
module.exports = {
darkMode: 'media',
// ...
}
How you add the dark
class to the html
element is up to you, but a common approach is to use a bit of JS that reads a preference from somewhere (like localStorage
) and updates the DOM accordingly.
Use data-hs-theme-switch
If you want to create a dark mode switcher for Tailwind CSS and Preline UI, you'll have to add some JavaScript code and a toggle element that a user can use to change their preferences.
<input data-hs-theme-switch class="relative w-[3.25rem] h-7 bg-gray-100 checked:bg-none checked:bg-blue-600 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 border border-transparent ring-1 ring-transparent focus:border-slate-700 focus:ring-slate-700 focus:outline-none appearance-none
before:inline-block before:w-6 before:h-6 before:bg-white checked:before:bg-blue-200 before:translate-x-0 checked:before:translate-x-full before:shadow before:rounded-full before:transform before:ring-0 before:transition before:ease-in-out before:duration-200
after:absolute after:end-1.5 after:top-[calc(50%-0.40625rem)] after:w-[.8125rem] after:h-[.8125rem] after:bg-no-repeat after:bg-[right_center] after:bg-[length:.8125em_.8125em] after:bg-[url('../svg/illustration/moon-stars.svg')] checked:after:bg-[url('../svg/illustration/brightness-high.svg')] after:transform after:transition-all after:ease-in-out after:duration-200 after:opacity-70 checked:after:start-1.5 checked:after:end-auto" type="checkbox" id="darkSwitch">
const HSThemeAppearance = {
init() {
const defaultTheme = 'default'
let theme = localStorage.getItem('hs_theme') || defaultTheme
if (document.querySelector('html').classList.contains('dark')) return
this.setAppearance(theme)
},
_resetStylesOnLoad() {
const $resetStyles = document.createElement('style')
$resetStyles.innerText = `*{transition: unset !important;}`
$resetStyles.setAttribute('data-hs-appearance-onload-styles', '')
document.head.appendChild($resetStyles)
return $resetStyles
},
setAppearance(theme, saveInStore = true, dispatchEvent = true) {
const $resetStylesEl = this._resetStylesOnLoad()
if (saveInStore) {
localStorage.setItem('hs_theme', theme)
}
if (theme === 'auto') {
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'default'
}
document.querySelector('html').classList.remove('dark')
document.querySelector('html').classList.remove('default')
document.querySelector('html').classList.remove('auto')
document.querySelector('html').classList.add(this.getOriginalAppearance())
setTimeout(() => {
$resetStylesEl.remove()
})
if (dispatchEvent) {
window.dispatchEvent(new CustomEvent('on-hs-appearance-change', {detail: theme}))
}
},
getAppearance() {
let theme = this.getOriginalAppearance()
if (theme === 'auto') {
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'default'
}
return theme
},
getOriginalAppearance() {
const defaultTheme = 'default'
return localStorage.getItem('hs_theme') || defaultTheme
}
}
HSThemeAppearance.init()
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
if (HSThemeAppearance.getOriginalAppearance() === 'auto') {
HSThemeAppearance.setAppearance('auto', false)
}
})
window.addEventListener('load', () => {
const $clickableThemes = document.querySelectorAll('[data-hs-theme-click-value]')
const $switchableThemes = document.querySelectorAll('[data-hs-theme-switch]')
$clickableThemes.forEach($item => {
$item.addEventListener('click', () => HSThemeAppearance.setAppearance($item.getAttribute('data-hs-theme-click-value'), true, $item))
})
$switchableThemes.forEach($item => {
$item.addEventListener('change', (e) => {
HSThemeAppearance.setAppearance(e.target.checked ? 'dark' : 'default')
})
$item.checked = HSThemeAppearance.getAppearance() === 'dark'
})
window.addEventListener('on-hs-appearance-change', e => {
$switchableThemes.forEach($item => {
$item.checked = e.detail === 'dark'
})
})
})
<button type="button" class="hs-dark-mode-active:hidden block hs-dark-mode group flex items-center text-gray-600 hover:text-blue-600 font-medium dark:text-gray-400 dark:hover:text-gray-500" data-hs-theme-click-value="dark">
<svg class="flex-shrink-0 w-4 h-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>
</button>
<button type="button" class="hs-dark-mode-active:block hidden hs-dark-mode group flex items-center text-gray-600 hover:text-blue-600 font-medium dark:text-gray-400 dark:hover:text-gray-500" data-hs-theme-click-value="light">
<svg class="flex-shrink-0 w-4 h-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 8a2 2 0 1 0 4 4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>
</button>
const HSThemeAppearance = {
init() {
const defaultTheme = 'default'
let theme = localStorage.getItem('hs_theme') || defaultTheme
if (document.querySelector('html').classList.contains('dark')) return
this.setAppearance(theme)
},
_resetStylesOnLoad() {
const $resetStyles = document.createElement('style')
$resetStyles.innerText = `*{transition: unset !important;}`
$resetStyles.setAttribute('data-hs-appearance-onload-styles', '')
document.head.appendChild($resetStyles)
return $resetStyles
},
setAppearance(theme, saveInStore = true, dispatchEvent = true) {
const $resetStylesEl = this._resetStylesOnLoad()
if (saveInStore) {
localStorage.setItem('hs_theme', theme)
}
if (theme === 'auto') {
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'default'
}
document.querySelector('html').classList.remove('dark')
document.querySelector('html').classList.remove('default')
document.querySelector('html').classList.remove('auto')
document.querySelector('html').classList.add(this.getOriginalAppearance())
setTimeout(() => {
$resetStylesEl.remove()
})
if (dispatchEvent) {
window.dispatchEvent(new CustomEvent('on-hs-appearance-change', {detail: theme}))
}
},
getAppearance() {
let theme = this.getOriginalAppearance()
if (theme === 'auto') {
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'default'
}
return theme
},
getOriginalAppearance() {
const defaultTheme = 'default'
return localStorage.getItem('hs_theme') || defaultTheme
}
}
HSThemeAppearance.init()
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
if (HSThemeAppearance.getOriginalAppearance() === 'auto') {
HSThemeAppearance.setAppearance('auto', false)
}
})
window.addEventListener('load', () => {
const $clickableThemes = document.querySelectorAll('[data-hs-theme-click-value]')
const $switchableThemes = document.querySelectorAll('[data-hs-theme-switch]')
$clickableThemes.forEach($item => {
$item.addEventListener('click', () => HSThemeAppearance.setAppearance($item.getAttribute('data-hs-theme-click-value'), true, $item))
})
$switchableThemes.forEach($item => {
$item.addEventListener('change', (e) => {
HSThemeAppearance.setAppearance(e.target.checked ? 'dark' : 'default')
})
$item.checked = HSThemeAppearance.getAppearance() === 'dark'
})
window.addEventListener('on-hs-appearance-change', e => {
$switchableThemes.forEach($item => {
$item.checked = e.detail === 'dark'
})
})
})
Use data-hs-theme-click-value
.
<div class="hs-dropdown" data-hs-dropdown-placement="bottom-right" data-hs-dropdown-offset="30">
<button type="button" class="hs-dropdown-toggle hs-dark-mode group flex items-center text-gray-600 hover:text-blue-600 font-medium dark:text-gray-400 dark:hover:text-gray-500">
<svg class="hs-dark-mode-active:hidden block w-4 h-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>
<svg class="hs-dark-mode-active:block hidden w-4 h-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 8a2 2 0 1 0 4 4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>
</button>
<div id="selectThemeDropdown" class="hs-dropdown-menu hs-dropdown-open:opacity-100 mt-2 hidden z-10 transition-[margin,opacity] opacity-0 duration-300 mb-2 origin-bottom-left bg-white shadow-md rounded-lg p-2 space-y-1 dark:bg-gray-800 dark:border dark:border-gray-700 dark:divide-gray-700">
<button type="button" class="hs-auto-mode-active:bg-gray-100 w-full flex items-center gap-x-3.5 py-2 px-3 rounded-lg text-sm text-gray-800 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300" data-hs-theme-click-value="auto">
Auto (system default)
</button>
<button type="button" class="hs-default-mode-active:bg-gray-100 w-full flex items-center gap-x-3.5 py-2 px-3 rounded-lg text-sm text-gray-800 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300" data-hs-theme-click-value="default">
Default (light mode)
</button>
<button type="button" class="hs-dark-mode-active:bg-gray-700 w-full flex items-center gap-x-3.5 py-2 px-3 rounded-lg text-sm text-gray-800 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300" data-hs-theme-click-value="dark">
Dark
</button>
</div>
</div>
const HSThemeAppearance = {
init() {
const defaultTheme = 'default'
let theme = localStorage.getItem('hs_theme') || defaultTheme
if (document.querySelector('html').classList.contains('dark')) return
this.setAppearance(theme)
},
_resetStylesOnLoad() {
const $resetStyles = document.createElement('style')
$resetStyles.innerText = `*{transition: unset !important;}`
$resetStyles.setAttribute('data-hs-appearance-onload-styles', '')
document.head.appendChild($resetStyles)
return $resetStyles
},
setAppearance(theme, saveInStore = true, dispatchEvent = true) {
const $resetStylesEl = this._resetStylesOnLoad()
if (saveInStore) {
localStorage.setItem('hs_theme', theme)
}
if (theme === 'auto') {
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'default'
}
document.querySelector('html').classList.remove('dark')
document.querySelector('html').classList.remove('default')
document.querySelector('html').classList.remove('auto')
document.querySelector('html').classList.add(this.getOriginalAppearance())
setTimeout(() => {
$resetStylesEl.remove()
})
if (dispatchEvent) {
window.dispatchEvent(new CustomEvent('on-hs-appearance-change', {detail: theme}))
}
},
getAppearance() {
let theme = this.getOriginalAppearance()
if (theme === 'auto') {
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'default'
}
return theme
},
getOriginalAppearance() {
const defaultTheme = 'default'
return localStorage.getItem('hs_theme') || defaultTheme
}
}
HSThemeAppearance.init()
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
if (HSThemeAppearance.getOriginalAppearance() === 'auto') {
HSThemeAppearance.setAppearance('auto', false)
}
})
window.addEventListener('load', () => {
const $clickableThemes = document.querySelectorAll('[data-hs-theme-click-value]')
const $switchableThemes = document.querySelectorAll('[data-hs-theme-switch]')
$clickableThemes.forEach($item => {
$item.addEventListener('click', () => HSThemeAppearance.setAppearance($item.getAttribute('data-hs-theme-click-value'), true, $item))
})
$switchableThemes.forEach($item => {
$item.addEventListener('change', (e) => {
HSThemeAppearance.setAppearance(e.target.checked ? 'dark' : 'default')
})
$item.checked = HSThemeAppearance.getAppearance() === 'dark'
})
window.addEventListener('on-hs-appearance-change', e => {
$switchableThemes.forEach($item => {
$item.checked = e.detail === 'dark'
})
})
})
Parameters | Description | Options | Default value |
---|---|---|---|
data-hs-theme-click-value
|
Changing the theme when clicking on an element | default | dark | auto |
null
|
data-hs-theme-switch
|
Changing theme when changing checkbox/radio button | No options | null |
Name | Description |
---|---|
hs-dark-mode-active
|
Dark mode is ON |
hs-default-mode-active
|
Default (light) mode is ON |
hs-auto-mode-active
|
Auto mode is ON |
Name | Description |
---|---|
on-hs-appearance-change
|
Fire theme changed for window object
|