Build Dark Mode UI with Elevation-Based Design System
Design-style skill for proper dark mode: never-pure-black backgrounds, elevation via lightness, and desaturated accent colors.
Why it matters
Implement accessible, OLED-optimized dark mode interfaces that use lightness for elevation instead of shadows, with desaturated accent colors and carefully calibrated grey scales to prevent eye strain and screen smearing.
Outcomes
What it gets done
Generate CSS custom properties with #121212 base and elevation-based grey scales
Create SwiftUI views using semantic colors and secondarySystemBackground for elevation
Build Flutter dark themes with desaturated accents and subtle border separators
Implement React Native color schemes with rgba opacity for text emphasis levels
Install
Add it to your toolbox
Run in your project directory:
curl -fsSL https://spark.entire.vc/get/ag-dark-mode | bash Overview
Dark Mode Design
A design-style skill for proper dark mode - never-pure-black backgrounds, elevation via lightness instead of shadows, and desaturated accent colors - with matching Web CSS and SwiftUI implementations. Use it whenever a dark theme is requested; build it as a real separate design system with its own elevation and color logic, not a color inversion of the light theme.
What it does
Dark Mode Design is a design-style child skill ('not just inverted colors - a carefully constructed hierarchy of light on dark'), part of the design-it family and not meant to be triggered directly. Three core principles: Never Pure Black (true #000000 causes smearing on OLED screens and extreme eye strain against white text, so dark greys like #121212 or #0A0A0A are used instead), Elevation via Lightness (in light mode, shadows show elevation, but shadows are invisible in dark mode - so elevated surfaces must be lighter than the background instead), and Desaturated Accents (saturated colors vibrate painfully against dark backgrounds, so brand colors get their saturation toned down for dark contexts).
When to use - and when NOT to
Use it whenever a user requests dark mode or a dark theme for an interface, and it must be built as a genuinely separate design system rather than a naive color inversion of the light theme. As a design sub-style, it's selected by the parent design-it skill's routing rather than invoked on its own.
Inputs and outputs
Visual DNA uses Midnight Luxury or an inverted Minimalist Slate palette: base background #121212, elevated card surfaces stepping up through #1E1E1E and #252525, primary text at #E1E1E1 rather than pure white, and body text dropped one font weight lighter than the light-mode equivalent since light text on dark backgrounds reads optically thicker. The web implementation defines a semantic elevation scale as CSS variables (--bg-base, --bg-elevated-1, --bg-elevated-2) and two text-emphasis opacities rather than fixed colors, with hover states moving a card to the next elevation step lighter rather than adding a shadow. Accent colors are explicitly desaturated - a muted purple (#BB86FC) instead of a bright saturated purple - and buttons pair that light accent with dark, near-black text for maximum readability.
:root {
--bg-base: #121212;
--bg-elevated-1: #1E1E1E;
--accent-color: #BB86FC;
}
.dark-card:hover { background-color: var(--bg-elevated-2); }
Integrations
SwiftUI leans on native semantic colors rather than hardcoded hex values wherever possible - .primary/.secondary text colors and .secondarySystemBackground (lighter than .systemBackground) auto-adapt to the system color scheme, while the desaturated accent button still uses an explicit RGB value to match the exact brand purple, paired with black button text for contrast against the light accent fill.
Who it's for
Designers and developers building a real dark theme - not a quick color-scheme inversion - who need a proper elevation hierarchy, OLED-safe backgrounds, and desaturated accents that don't vibrate against a dark canvas.
Source README
Dark Mode Design
"Not just inverted colors. A carefully constructed hierarchy of light on dark."
When to Use
Use this sub-style when the user's request matches the aesthetic described above. This is a child reference of the design-it skill and is not meant to be triggered directly.
Core Principles
- Never Pure Black: True
#000000causes smearing on OLED screens and extreme eye strain with white text. Use dark greys (e.g.,#121212or#0A0A0A). - Elevation via Lightness: In light mode, shadows show elevation. In dark mode, shadows are invisible, so elevated surfaces must be lighter than the background.
- Desaturated Accents: Saturated colors vibrate painfully against dark backgrounds. Tone down the saturation of brand colors.
Visual DNA
- Colors: Midnight Luxury or Minimalist Slate (inverted). Background
#121212. Elevated cards#1E1E1E,#252525. Primary text#E1E1E1(not#FFFFFF). - Typography: Standard highly readable sans-serifs, but often dropped down one font weight compared to light mode, as light text on dark backgrounds appears optically thicker.
- Shadows: Pure black shadows, but with much lower opacity, mostly to separate slightly different shades of grey.
Web Implementation
- CSS Example:
:root {
--bg-base: #121212;
--bg-elevated-1: #1E1E1E;
--bg-elevated-2: #242424;
--text-high-emphasis: rgba(255, 255, 255, 0.87);
--text-medium-emphasis: rgba(255, 255, 255, 0.60);
/* Accent color: Desaturated purple instead of bright purple */
--accent-color: #BB86FC;
}
body {
background-color: var(--bg-base);
color: var(--text-high-emphasis);
font-weight: 300; /* Thinner weight for dark mode */
}
.dark-card {
background-color: var(--bg-elevated-1);
border-radius: 8px;
padding: 24px;
/* Very subtle border can help separate dark surfaces */
border: 1px solid rgba(255, 255, 255, 0.05);
}
.dark-card:hover {
/* On hover, the element moves closer to the user, so it gets lighter */
background-color: var(--bg-elevated-2);
}
.dark-btn {
background-color: var(--accent-color);
color: #000; /* Dark text on light accent is highly readable */
font-weight: 600;
border: none;
padding: 12px 24px;
border-radius: 4px;
}
App Implementation
SwiftUI
struct DarkModeView: View {
// Force Dark Mode on this specific view (or use system settings)
@Environment(\.colorScheme) var colorScheme
var body: some View {
ScrollView {
VStack(spacing: 20) {
// Primary elevated card
VStack(alignment: .leading, spacing: 12) {
Text("Elevation via Lightness")
.font(.headline)
.foregroundColor(.primary) // Auto-adapts
Text("In dark mode, elevated surfaces are lighter grey, not shadowed.")
.font(.subheadline)
.foregroundColor(.secondary) // Auto-adapts
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
// Use native semantic colors. .secondarySystemBackground is lighter than .systemBackground
.background(Color(UIColor.secondarySystemBackground))
.cornerRadius(12)
// Desaturated Accent Button
Button(action: {}) {
Text("Desaturated Accent")
.fontWeight(.semibold)
.foregroundColor(.black) // Dark text on light accent
.padding()
.frame(maxWidth: .infinity)
.background(Color(red: 0.73, green: 0.52, blue: 0.98)) // #BB86FC (Desaturated purple)
.cornerRadius(8)
}
}
.padding()
}
// #121212 is the standard dark mode background, which systemBackground maps to closely
.background(Color(UIColor.systemBackground))
}
}
// .preferredColorScheme(.dark) to force
- Rely on Semantics: SwiftUI's
Color.primary,Color.secondary,Color(UIColor.systemBackground)andColor(UIColor.secondarySystemBackground)handle perfect dark mode transitions automatically. - Avoid forcing explicit hex codes for backgrounds unless you are building a custom-themed app.
Flutter
import 'package:flutter/material.dart';
class DarkModeApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
// Configure the Dark Theme
themeMode: ThemeMode.dark,
darkTheme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: const Color(0xFF121212), // Standard dark background
cardColor: const Color(0xFF1E1E1E), // Elevated surface
colorScheme: const ColorScheme.dark().copyWith(
primary: const Color(0xFFBB86FC), // Desaturated accent
onPrimary: Colors.black, // Dark text on light accent
surface: const Color(0xFF1E1E1E),
),
),
home: Scaffold(
appBar: AppBar(title: const Text('Dark Mode', style: TextStyle(color: Colors.white70))),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
Card(
elevation: 0, // Shadows don't show well anyway
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: Colors.white.withOpacity(0.05)), // Subtle border
),
child: const Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Elevation via Lightness', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
SizedBox(height: 8),
Text('Elevated surfaces use lighter greys.', style: TextStyle(color: Colors.white60)),
],
),
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {},
child: const Text('Desaturated Accent'),
),
],
),
),
);
}
}
- When using
ThemeData.dark(), actively overridescaffoldBackgroundColorto#121212andcardColorto#1E1E1E. - Ensure your
colorScheme.onPrimaryis black so text is readable when placed on top of your desaturated primary accent button.
React Native
import { useColorScheme } from 'react-native';
const DarkModeScreen = () => {
const isDark = useColorScheme() === 'dark';
// Custom dark theme dictionary
const theme = {
bgBase: isDark ? '#121212' : '#FFFFFF',
bgElevated: isDark ? '#1E1E1E' : '#F5F5F5',
textHigh: isDark ? 'rgba(255, 255, 255, 0.87)' : 'rgba(0, 0, 0, 0.87)',
textMedium: isDark ? 'rgba(255, 255, 255, 0.60)' : 'rgba(0, 0, 0, 0.60)',
accent: isDark ? '#BB86FC' : '#6200EE', // Desaturated for dark, vibrant for light
onAccent: isDark ? '#000' : '#FFF',
};
return (
<View style={{ flex: 1, backgroundColor: theme.bgBase, padding: 16 }}>
<View style={{
backgroundColor: theme.bgElevated,
padding: 24,
borderRadius: 12,
borderWidth: isDark ? 1 : 0,
borderColor: 'rgba(255,255,255,0.05)',
marginBottom: 20
}}>
<Text style={{ color: theme.textHigh, fontSize: 18, fontWeight: 'bold', marginBottom: 8 }}>
Elevation via Lightness
</Text>
<Text style={{ color: theme.textMedium }}>
Elevated surfaces use lighter greys.
</Text>
</View>
<TouchableOpacity style={{
backgroundColor: theme.accent,
padding: 16,
borderRadius: 8,
alignItems: 'center'
}}>
<Text style={{ color: theme.onAccent, fontWeight: 'bold' }}>Desaturated Accent</Text>
</TouchableOpacity>
</View>
);
};
- Rely on
useColorScheme()hook from React Native. - Define a strict dictionary of your color tokens. Notice how
theme.accentshifts from a vibrant purple (#6200EE) in light mode to a desaturated pastel purple (#BB86FC) in dark mode.
Jetpack Compose
@Composable
fun DarkModeScreen() {
// Typically this logic lives in your Theme.kt
val darkColors = darkColorScheme(
background = Color(0xFF121212),
surface = Color(0xFF1E1E1E),
primary = Color(0xFFBB86FC),
onPrimary = Color.Black,
onBackground = Color.White.copy(alpha = 0.87f),
onSurface = Color.White.copy(alpha = 0.87f)
)
MaterialTheme(colorScheme = darkColors) {
Column(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.padding(16.dp)
) {
Card(
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
shape = RoundedCornerShape(12.dp),
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.05f))
) {
Column(modifier = Modifier.padding(24.dp)) {
Text("Elevation via Lightness",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface)
Spacer(Modifier.height(8.dp))
Text("Elevated surfaces use lighter greys.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f))
}
}
Spacer(Modifier.height(20.dp))
Button(
onClick = {},
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary
),
modifier = Modifier.fillMaxWidth().height(50.dp)
) {
Text("Desaturated Accent", fontWeight = FontWeight.Bold)
}
}
}
}
- Material 3 handles Dark Mode semantics perfectly. Define your
darkColorScheme. - Use
Color(0xFF1E1E1E)forsurfaceandColor(0xFF121212)forbackground. Compose will automatically map these toCards and Scaffolds.
Do's and Don'ts
- DO: Meet WCAG contrast standards. Just because it's dark doesn't mean the text should be illegibly dim.
- DON'T: Use bright, highly saturated primary colors.
Limitations
- This is a styling reference and does not replace environment-specific validation, accessibility testing, or expert review.
- Ensure appropriate contrast ratios and responsive behaviors are verified separately.
FAQ
Common questions
Discussion
Questions & comments · 0
Sign In Sign in to leave a comment.