Flicker-free darkmode in SvelteKit with Tailwind v4
Learn how to implement flicker-free darkmode in SvelteKit using Tailwind CSS v4 with server-side rendering support.
This approach eliminates flickering by rendering the correct theme server-side, removing the need for post-mount JavaScript.
Implementation steps
Add darkmode variant
This custom variant respects explicit theme choices while gracefully falling back to system preferences.
@import 'tailwindcss';
@custom-variant dark {
/* Explicitly set dark theme */
&:where([data-theme='dark'], [data-theme='dark'] *) {
@slot;
}
/* System preference fallback */
@media (prefers-color-scheme: dark) {
&:where(html:not([data-theme]), html:not([data-theme]) *) {
@slot;
}
}
} Create a theme handler
This server-side handler renders the correct theme on initial load. Without a stored preference, it defers to system settings via CSS media queries.
export const handle = async ({ event, resolve }) => {
const cookieTheme = event.cookies.get('theme')
// No stored theme → defer to system preference
if (!cookieTheme) return resolve(event)
// Apply stored theme
return resolve(event, {
transformPageChunk: ({ html }) => html.replace('<html', `<html data-theme="${cookieTheme}"`)
})
} Create theme endpoint
Create an endpoint that persists the theme preference in a cookie. This example is simplified—add proper validation and security for production use.
import { dev } from '$app/environment'
import { json, type Cookies } from '@sveltejs/kit'
const cookiesConfig: Parameters<Cookies['set']>[2] = {
path: '/',
sameSite: 'lax',
secure: !dev,
httpOnly: true
}
export const GET = async ({ url, cookies }) => {
const newTheme = url.searchParams.get('newTheme')
if (!newTheme) return json({ success: false }, { status: 400 })
// Clear cookie for system preference
if (newTheme === 'system') cookies.delete('theme', cookiesConfig)
// Store explicit theme choice
else cookies.set('theme', newTheme, { ...cookiesConfig, maxAge: 60 * 60 * 24 * 365 })
return json({ success: true })
} Settings page
Create a UI for theme selection by calling the endpoint. This can live in settings or anywhere in your app.
<script lang="ts">
const setTheme = async (newTheme: string) => {
const { ok } = await fetch(`/api/settings/theme?newTheme=${newTheme}`)
if (ok && newTheme === 'system') document.documentElement.removeAttribute('data-theme')
else if (ok) document.documentElement.setAttribute('data-theme', newTheme)
}
</script>
<button onclick={() => setTheme('light')}> Light </button>
<button onclick={() => setTheme('dark')}> Dark </button>
<button onclick={() => setTheme('system')}> System </button> That’s it!
Use the dark: variant throughout your app for theme-aware styles.
<div class="bg-white dark:bg-gray-800">
<h3 class="text-gray-900 dark:text-white">Writes upside-down</h3>
<p class="text-pretty text-gray-500 dark:text-gray-400">
The Zero Gravity Pen can be used to write in any orientation, including upside-down. It even
works in outer space.
</p>
</div> Test it using the theme selector in the footer below 👇