One Logo Pool Ball

System

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.

To start, there's only one cross-platform style library that supports SSR - 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.

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.

import { Slot } from 'one'
import { TamaguiProvider, Theme } from '@tamagui/core'
import { config } from '@tamagui/config/v3'
import { MetaTheme, SchemeProvider, useColorScheme } from '@vxrn/color-scheme'
export default function Layout() {
return (
<SchemeProvider>
<MetaTheme darkColor={config.themes.dark.color1.val} lightColor={config.themes.light.color1.val} />
<TamaguiRoot>
<Theme name="yellow">
<Slot />
</Theme>
</TamaguiRoot>
</SchemeProvider>
)
}
const TamaguiRoot = ({ children }) => {
const [scheme] = useColorScheme()
return (
<TamaguiProvider disableInjectCSS config={config} defaultTheme={scheme}>
{children}
</TamaguiProvider>
)
}

Now lets make a toggle button:

features/theme/ThemeToggleButton.tsx

import { Moon, Sun, SunMoon } from '@tamagui/lucide-icons'
import { useSchemeSetting } from '@vxrn/color-scheme'
import { Appearance } from 'react-native'
import { View, isWeb, Paragraph, YStack } from 'tamagui'
const schemeSettings = ['light', 'dark', 'system'] as const
export function ThemeToggleButton() {
const { onPress, Icon, setting } = useToggleTheme()
return (
<View group ai="center" containerType="normal" gap="$1">
<View p="$3" br="$10" hoverStyle={{ bg: '$color2', }} pressStyle={{ bg: '$color1', }} pointerEvents="auto" cur="pointer" onPress={onPress} >
<Icon size={20} />
</View>
<YStack>
<Paragraph animation="100ms" size="$1" mb={-20} color="$color10" o={0} $group-hover={{ o: 1, }} >
{setting[0].toUpperCase()}
{setting.slice(1)}
</Paragraph>
</YStack>
</View>
)
}
export function useToggleTheme() {
const [{ setting, scheme }, setSchemeSetting] = useSchemeSetting()
const Icon = setting === 'system' ? SunMoon : setting === 'dark' ? Moon : Sun
return {
setting,
scheme,
Icon,
onPress: () => {
const next = schemeSettings[(schemeSettings.indexOf(setting) + 1) % 3]
if (!isWeb) {
Appearance.setColorScheme(next === 'system' ? scheme : next)
}
setSchemeSetting(next)
},
}
}

Edit this page on GitHub.