Light / Dark Theme

Tema claro/escuro para todas as UIs Koder (web, Flutter mobile/desktop, TV, landing pages): comportamento padrão pós-instalação (ThemeMode.system), persistência da escolha do usuário, anti-flash, CSS vars.

Source spec: specs/themes/light-dark.kmd

Semantic color tokens

Every Koder UI exposes the same semantic CSS variables. Pages declare the light values on :root and override them under [data-theme="dark"]. The exact values below are the canonical baseline; products may tighten them.

Token Light Dark Used for
--bg #ffffff #0b1120 page background
--text #0f172a #f1f5f9 primary text
--text-muted #475569 #94a3b8 secondary text
--surface #f8fafc #111827 card / panel surface
--surface-2 #e2e8f0 #1f2937 raised surface
--accent #1d4ed8 #60a5fa primary action
--accent-on #ffffff #0b1120 text on accent
--border #cbd5e1 #334155 divider / border
--focus #1e3a8a #bfdbfe focus ring

Required behavior

  1. Two modes only — Light and Dark — no third "system" toggle position.
  2. Initial selection — Honor the OS prefers-color-scheme on first load (or after localStorage is cleared).
  3. User choice persistence — Save the explicit toggle to localStorage["theme"]; this overrides the OS preference on later visits.
  4. Live OS propagation — When no user preference is saved, follow OS changes via matchMedia(...).addEventListener("change", …).
  5. No flash of wrong theme — Apply the saved theme inline before the first render, in <head> before the CSS link.

Required CSS structure

:root {
  --bg: #ffffff;
  --text: #0f172a;
  /* ... other semantic tokens ... */
  color-scheme: light;
}

[data-theme="dark"] {
  --bg: #0b1120;
  --text: #f1f5f9;
  /* ... */
  color-scheme: dark;
}

Anti-flash inline script

Place this snippet inside <head>, before any external stylesheet. It is the smallest correct implementation.

<script>
  (function(){
    const s = localStorage.getItem('theme');
    const dark = s ? s === 'dark' : matchMedia('(prefers-color-scheme:dark)').matches;
    if (dark) document.documentElement.setAttribute('data-theme','dark');
  })();
</script>

Required toggle JavaScript

function toggleTheme() {
  const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
  const next = isDark ? 'light' : 'dark';
  localStorage.setItem('theme', next);
  applyTheme();
}
function applyTheme() {
  const saved = localStorage.getItem('theme');
  const isDark = saved
    ? saved === 'dark'
    : matchMedia('(prefers-color-scheme:dark)').matches;
  document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
}
matchMedia('(prefers-color-scheme:dark)').addEventListener('change', () => {
  if (!localStorage.getItem('theme')) applyTheme();
});
applyTheme();

Flutter / native apps

Native apps follow the same contract. Until KoderTheme widget ships in koder_kit v0.6.0, use the inline pattern below — and never hardcode ThemeMode.dark or ThemeMode.light before reading the saved preference.

// main.dart — until KoderTheme widget ships in koder_kit v0.6.0+
Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final prefs = await SharedPreferences.getInstance();
  final saved = prefs.getString('theme'); // 'light' | 'dark' | null
  runApp(MyApp(initialTheme: saved));
}

ThemeMode _resolve(String? saved) {
  if (saved == 'dark') return ThemeMode.dark;
  if (saved == 'light') return ThemeMode.light;
  return ThemeMode.system; // follow OS when no preference saved
}

Audit checklist

/k-housekeep and equivalent linters verify each web surface meets every item.

  1. <head> contains the anti-flash script that reads localStorage["theme"] and sets data-theme.
  2. CSS contains a [data-theme="dark"] selector with overrides.
  3. color-scheme: light on :root and color-scheme: dark on [data-theme="dark"].
  4. A toggle button referencing toggleTheme() (or equivalent) lives in the navbar.
  5. JavaScript exposes both toggleTheme and applyTheme functions with the documented behavior.
  6. Two icons (sun / moon) swap visibility when the toggle fires.
  7. matchMedia('(prefers-color-scheme:dark)') is used and reacted to.
  8. Saved values are exactly "light" or "dark" — never any other string.