Theme semantic roles rather than scattered colors
A theme should define surface, text, border and accent roles. Components then consume those roles. This makes dark mode a coherent palette change rather than a collection of unrelated overrides.
:root {
color-scheme: light;
--surface: #ffffff;
--text: #0f172a;
--border: #cbd5e1;
--accent: #1d4ed8;
}
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark;
--surface: #0f172a;
--text: #f8fafc;
--border: #475569;
--accent: #93c5fd;
}
}
.tech-lesson-content { background: var(--surface); color: var(--text); }
a { color: var(--accent); }
.card { border: 1px solid var(--border); }The system preference selects the automatic palette. color-scheme also informs the browser about supported schemes for built-in UI such as form controls; custom colors still require your own tokens.
Manual choice
If a theme button is provided, use a real button with a clear current-state label. A data-theme attribute on the root can select explicit light or dark tokens. Decide how manual choice overrides automatic preference, and provide a way back to system mode. Persistence belongs in application logic, not CSS alone.
Test the full interface
Check disabled text, errors, focus outlines, tables, code blocks and images. A dark background with unchanged dark text is a common partial-theme failure. Do not invert every image with a blanket filter; photographs, logos and diagrams need individual consideration.
Initial paint
A persisted manual theme applied late can cause a bright/dark flash. Coordinate initial theme selection with the application's loading and security constraints. The page should remain readable when scripts fail; the CSS preference-based baseline provides a useful fallback.
Practice
- Emulate both system schemes in developer tools.
- Inspect a form and its focus states in each theme.
- Add a manual three-option choice: system, light and dark.
- Reload and verify the chosen behavior without losing a readable initial state.