| name | light-dark-theming |
| description | Light and dark theming via system integration, per-surface semantics, and elevation overlays. Use this when wiring color themes to the platform or adding a new surface. |
Light / Dark Theming
Instructions
Dark mode is not a palette swap. It is a different alias mapping of the same tonal palettes with its own contrast and elevation rules.
1. System Integration
Follow the OS. Never ship an in-app toggle that ignores prefers-color-scheme — only an override.
- Android:
AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_FOLLOW_SYSTEM) by default; apps read Configuration.UI_MODE_NIGHT_YES or isSystemInDarkTheme() in Compose.
- iOS:
UITraitCollection.userInterfaceStyle / SwiftUI @Environment(\.colorScheme).
- Flutter:
MediaQuery.of(context).platformBrightness or ThemeMode.system.
- React Native:
Appearance.getColorScheme() and useColorScheme().
2. Per-Surface Semantics
Ship tokens for a small, named set of surfaces — not a single "background."
| Token | Light | Dark | Use |
|---|
color.surface.default | neutral.99 | neutral.10 | App background |
color.surface.subtle | neutral.95 | neutral.15 | Cards, sheets |
color.surface.raised | neutral.99 | neutral.20 | Elevated cards |
color.surface.overlay | neutral.99 α 95% | neutral.10 α 90% | Modals, popovers |
color.surface.inverse | neutral.10 | neutral.95 | Tooltips, toasts |
Each surface has a paired content token (color.content.*) whose contrast against the surface is verified AA.
3. Dark Elevation: Tint Over Shadow
On dark themes, shadows become invisible. Convey elevation with surface tint (Material 3) — a subtle tonal wash at higher levels.
val DarkColorScheme = darkColorScheme(
surface = ds.color.surface.default,
surfaceContainer = ds.color.surface.subtle,
surfaceContainerHigh = ds.color.surface.raised,
surfaceTint = ds.color.action.primary.bg,
)
4. Compose Wiring
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit,
) {
val scheme = if (darkTheme) DarkColorScheme else LightColorScheme
MaterialTheme(colorScheme = scheme, typography = AppTypography, content = content)
}
5. SwiftUI Wiring
Use asset catalog color sets with separate Any Appearance + Dark Appearance values, referenced semantically:
extension Color {
static let dsSurfaceDefault = Color("surface/default")
static let dsContentDefault = Color("content/default")
}
To force a theme locally:
ContentView().preferredColorScheme(.dark)
6. Flutter Wiring
MaterialApp(
themeMode: ThemeMode.system,
theme: AppTheme.light(),
darkTheme: AppTheme.dark(),
);
class AppTheme {
static ThemeData light() => ThemeData(
useMaterial3: true,
colorScheme: lightColorScheme,
extensions: [AppColors.light, AppSpacing.standard],
);
static ThemeData dark() => ThemeData(
useMaterial3: true,
colorScheme: darkColorScheme,
extensions: [AppColors.dark, AppSpacing.standard],
);
}
7. React Native Wiring
const scheme = useColorScheme();
const theme = scheme === 'dark' ? darkTheme : lightTheme;
return <ThemeProvider value={theme}>{children}</ThemeProvider>;
8. Edge Cases
- Images and illustrations: ship light and dark variants or use monochrome assets that inherit
color.content.default.
- Status bar / navigation bar: set to match surface color and icon contrast (
WindowCompat on Android, preferredStatusBarStyle / UIStatusBarStyle on iOS, StatusBar on RN).
- System keyboards and sheets: inherit dark mode automatically — do not override unless the design explicitly requires it.
- Screenshots and sharing: use
color.surface.default so exports are consistent.
9. Anti-Patterns
- Hardcoded
Color(0xFFFFFFFF) anywhere.
- Using
Colors.white.withOpacity(0.7) as muted text — fails contrast in light mode.
- A single "background" token used for app, card, and modal.
- Dark-only shadows; they are invisible and waste layout cost.
- Manual theme toggles that ignore the system preference as a default.
Checklist