One Logo Pool Ball
wwwwwwwwwwwwwwwwwww

Dark Mode

SSR safe and with meta theme-color

Supporting SSR safe styling while also supporting Native and web requires a lot of work, until now.

There’s about one cross-platform React style library that has excellent SSR support - Tamagui 👌.

Tamagui gives you styling out of the box that works perfectly with no flickers, and even renders properly with JS turned off (give it a try on this very page).

Using our style-library-agnostic @vxrn/color-scheme library, you can also let your users choose between three options: light, dark, or system settings - also, fully SSR safe. one re-exports everything from @vxrn/color-scheme, so you can import these from either package.

Finally, there’s another wrinkle in the theme-color meta tag - it also requires some fancy scripting to be SSR safe. Luckily, @vxrn/color-scheme helps again with the MetaTheme component.

Here’s a complete example in just a few lines of code, all set up in your root _layout.tsx file. We’re using Tamagui, but if you just want light/dark mode without SSR, you can swap out your own solution.

app/_layout.tsx
import type { ReactNode } from 'react'
import { MetaTheme, SchemeProvider, Slot, useUserScheme } from 'one'
import { TamaguiProvider, Theme } from 'tamagui'
import config from '../tamagui.config'
export default function Layout() {
return (
<SchemeProvider>
<MetaTheme darkColor="#050505" lightColor="#ffffff" />
<TamaguiRoot>
<Theme name="yellow">
<Slot />
</Theme>
</TamaguiRoot>
</SchemeProvider>
)
}
const TamaguiRoot = ({ children }: { children: ReactNode }) => {
const userScheme = useUserScheme()
return (
<TamaguiProvider disableInjectCSS config={config} defaultTheme={userScheme.value}>
{children}
</TamaguiProvider>
)
}

SchemeProvider renders a small inline script that sets a t_dark or t_light class on <html> before the page paints, reading the stored setting or falling back to the system preference. It accepts:

MetaTheme takes darkColor and lightColor. Its inline script picks one before hydration; after that, an optional color prop replaces the scheme’s color while it is set. useUserScheme() returns { setting, value, set }: the stored setting (system, light, or dark), the resolved scheme (light or dark), and a setter. Outside React, use getUserScheme, setUserScheme, and onUserSchemeChange; useSystemScheme and getSystemScheme read the operating system’s scheme.

Now lets make a toggle button:

features/theme/ThemeToggleButton.tsx
import { Moon, Sun, SunMoon } from '~/components/icons'
import { useUserScheme } from 'one'
import { Button } from 'tamagui'
const schemeSettings = ['light', 'dark', 'system'] as const
export function ThemeToggleButton() {
const { setting, set } = useUserScheme()
const Icon = setting === 'system' ? SunMoon : setting === 'dark' ? Moon : Sun
const next = schemeSettings[(schemeSettings.indexOf(setting) + 1) % schemeSettings.length]
return (
<Button
circular
icon={<Icon size={20} />}
aria-label={`Color scheme: ${setting}`}
onPress={() => set(next)}
/>
)
}

Each press cycles light, dark, and system. The stored setting survives reloads, and with system the page follows the operating system’s preference.

Edit this page on GitHub.