DocsDark mode

Dark mode

omverse-ui supports dark mode via CSS media query or a class-based toggle — no extra config required.

All omverse-ui colors are CSS custom properties, so dark mode is a matter of overriding those variables. There are two standard approaches — choose whichever fits your app.

Option 1 — System preference (media query)

Override tokens inside a prefers-color-scheme: dark media query. The browser switches automatically based on the OS setting — no JavaScript needed.

index.css
tsx
/* Automatically follow the OS preference */
@media (prefers-color-scheme: dark) {
:root {
--color-background: #0A0A0F;
--color-surface: #111118;
--color-surface-variant: #1A1A24;
--color-text-primary: #F1F5F9;
--color-text-secondary: #94A3B8;
--color-text-tertiary: #64748B;
--color-outline: #334155;
--color-outline-variant: #1E293B;
}
}

Option 2 — Class-based toggle

Scope overrides to a .dark class on <html>. This lets you toggle dark mode programmatically and persist the user's preference.

index.css
tsx
/* Class-based — toggle .dark on <html> */
.dark {
--color-background: #0A0A0F;
--color-surface: #111118;
--color-surface-variant: #1A1A24;
--color-text-primary: #F1F5F9;
--color-text-secondary: #94A3B8;
--color-text-tertiary: #64748B;
--color-outline: #334155;
--color-outline-variant: #1E293B;
}

Use a small hook to toggle the class and save the preference to localStorage:

useDarkMode.ts
tsx
import { useEffect, useState } from 'react'
 
export function useDarkMode() {
const [dark, setDark] = useState(
() => document.documentElement.classList.contains('dark')
)
 
useEffect(() => {
document.documentElement.classList.toggle('dark', dark)
localStorage.setItem('theme', dark ? 'dark' : 'light')
}, [dark])
 
return [dark, setDark] as const
}

Preventing flash of wrong theme

When using class-based dark mode, the page can briefly flash the wrong theme before JavaScript runs. Fix this by injecting an inline script in <head> that reads localStorage and applies the class synchronously.

The script must be inline (not a deferred or async script) so it runs before the browser paints anything.

Standalone anti-flash snippet

index.html / _document.tsx
tsx
// Prevent flash of wrong theme — add to <head> before JS loads
<script dangerouslySetInnerHTML={{
__html: `
const saved = localStorage.getItem('theme')
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
if (saved === 'dark' || (!saved && prefersDark)) {
document.documentElement.classList.add('dark')
}
`,
}} />

In a Next.js App Router layout

app/layout.tsx
tsx
// app/layout.tsx
import type { Metadata } from 'next'
 
export const metadata: Metadata = { title: 'My App' }
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
{/* Anti-flash script — must be inline, before any JS */}
<script dangerouslySetInnerHTML={{
__html: `
const t = localStorage.getItem('theme')
const d = window.matchMedia('(prefers-color-scheme: dark)').matches
if (t === 'dark' || (!t && d)) document.documentElement.classList.add('dark')
`,
}} />
</head>
<body>{children}</body>
</html>
)
}

Tips

  • Add suppressHydrationWarning to <html> in Next.js to silence the class mismatch hydration warning.
  • Both approaches can be combined — use the media query as the default and class override for an explicit user toggle.
  • See the Design tokens page for a full list of color variables you can override.