Dark mode
Turn on dark mode with data-theme, next-themes or a theme switcher of your own.
Dark mode is a single attribute:
<html data-theme="dark"></html>With that attribute in place, every semantic token (--ak-surface, --ak-text,
--ak-primary-soft ...) switches to its dark value and the components follow with no extra work.
Without it, or with data-theme="light", you get light mode. Tailwind's dark: variant is wired
to the same attribute.
'use client';
import { Badge, Button, Field, Input } from '@akhatmirzo/ui';
// data-theme istalgan elementga qo'yiladi — tokenlar faqat shu blok ichida almashadi
export default function ThemingScoped() {
return (
<div className="grid w-full gap-4 sm:grid-cols-2">
{(['light', 'dark'] as const).map((theme) => (
<div
key={theme}
data-theme={theme}
className="flex flex-col gap-4 rounded-xl border border-border bg-background p-5 text-text"
>
<div className="flex items-center justify-between">
<span className="text-sm font-semibold">data-theme="{theme}"</span>
<Badge color="success" dot>
Faol
</Badge>
</div>
<Field label="Ism">
<Input placeholder="Sarvar" />
</Field>
<div className="flex gap-2">
<Button variant="primary">Saqlash</Button>
<Button variant="outline">Bekor qilish</Button>
</div>
</div>
))}
</div>
);
}
The system setting isn't followed automatically
theme.css doesn't look at prefers-color-scheme — your app decides which mode is on. That way
the user's choice, the OS setting and a saved value never fight each other. Both approaches are
shown below.
With next-themes (Next.js)
next-themes remembers the theme, follows the OS setting and prevents the white flash on page load. Tell it which attribute to set:
pnpm add next-themes'use client';
import { ConfirmProvider, Toaster, TooltipProvider } from '@akhatmirzo/ui';
import { ThemeProvider } from 'next-themes';
import type { ReactNode } from 'react';
export function Providers({ children }: { children: ReactNode }) {
return (
<ThemeProvider attribute="data-theme" defaultTheme="system" enableSystem>
<TooltipProvider>
<ConfirmProvider>
{children}
<Toaster />
</ConfirmProvider>
</TooltipProvider>
</ThemeProvider>
);
}next-themes changes the <html> attribute before the page paints, so add
suppressHydrationWarning in layout.tsx:
<html lang="en" suppressHydrationWarning>If another library in your project relies on class="dark", set both attributes:
attribute={['class', 'data-theme']}. This documentation site works exactly that way.
Theme switcher
'use client';
import { IconButton } from '@akhatmirzo/ui';
import { Moon, Sun } from 'lucide-react';
import { useTheme } from 'next-themes';
export function ThemeToggle() {
const { resolvedTheme, setTheme } = useTheme();
const dark = resolvedTheme === 'dark';
return (
<IconButton
variant="ghost"
aria-label={dark ? 'Switch to light mode' : 'Switch to dark mode'}
onClick={() => setTheme(dark ? 'light' : 'dark')}
>
{dark ? <Sun /> : <Moon />}
</IconButton>
);
}resolvedTheme is undefined during server rendering. If the icon swaps right after hydration,
that's expected.
Without a library (Vite and others)
If you manage the theme yourself, you need two things: a tiny script that sets the attribute before the page paints, and a hook that changes it.
<head>
<script>
try {
// useLocalStorage stores the value as JSON: "dark"
const saved = JSON.parse(localStorage.getItem('theme') || 'null');
const system = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
document.documentElement.dataset.theme = saved || system;
} catch {}
</script>
</head>import { useLocalStorage } from '@akhatmirzo/hooks';
import { useEffect } from 'react';
type Theme = 'light' | 'dark';
export function useTheme() {
const [theme, setTheme] = useLocalStorage<Theme>('theme', () =>
document.documentElement.dataset.theme === 'dark' ? 'dark' : 'light',
);
useEffect(() => {
document.documentElement.dataset.theme = theme;
}, [theme]);
return [theme, setTheme] as const;
}Why the script matters
If the attribute is only set after React loads, users see a white page for a split second. The
script in <head> removes that flash.
On part of a page
data-theme works on any element, not just <html> — the tokens switch inside that element. The
demo above is built that way. Use it for a dark footer on a light page, or to compare themes side
by side.
<footer data-theme="dark" className="bg-background text-text">
...
</footer>Overlays render in a portal
Dialog, Popover, DropdownMenu, Tooltip and toasts render into document.body, so they
take the theme of <html>, not of the surrounding block.
What changes in dark mode
- Backgrounds. The page is slate-900, cards and inputs slate-800, popovers and toasts one step lighter (slate-750). In dark mode, elevation comes from a lighter surface rather than a shadow.
- Shadows.
shadow-mdand larger get a 1px ring. - The primary button stays the same (indigo-600) for contrast. Hover states, soft fills and links get lighter.
- Native controls.
color-scheme: darkis set, so scrollbars, date pickers and autofill go dark too.
In your own styles
If you style with semantic tokens, you'll rarely need dark: — bg-surface text-text border-border is right in both modes. Save dark: for places that need a genuinely different
decision, such as dimming an image on a dark background: dark:opacity-80.
To check the OS setting yourself, use useMediaQuery:
const prefersDark = useMediaQuery('(prefers-color-scheme: dark)');