picasso-skill 2.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -65
- package/agents/picasso.md +36 -15
- package/bin/install.mjs +1 -1
- package/package.json +2 -2
- package/references/animation-performance.md +244 -0
- package/references/brand-and-identity.md +136 -0
- package/references/code-typography.md +222 -0
- package/references/dark-mode.md +199 -0
- package/references/i18n-visual-patterns.md +177 -0
- package/references/images-and-media.md +222 -0
- package/references/loading-and-states.md +258 -0
- package/references/micro-interactions.md +291 -0
- package/references/navigation-patterns.md +247 -0
- package/references/tables-and-forms.md +227 -0
- package/skills/picasso/SKILL.md +10 -1
- package/templates/picasso-config.md +53 -0
- package/checklists/pre-ship.md +0 -83
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Brand and Identity Reference
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
1. Logo Sizing Rules
|
|
5
|
+
2. Logo Placement
|
|
6
|
+
3. Logo Lockup Variants
|
|
7
|
+
4. Brand Color Usage
|
|
8
|
+
5. Brand Typography
|
|
9
|
+
6. Consistency Across Pages
|
|
10
|
+
7. When to Bend Brand Rules
|
|
11
|
+
8. Common Mistakes
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. Logo Sizing Rules
|
|
16
|
+
|
|
17
|
+
- **Minimum size:** 24px height for icon-only, 32px height for full logo. Below this it's illegible.
|
|
18
|
+
- **Clear space:** Minimum clear space around logo = the height of the logo's icon element. No text or elements may intrude.
|
|
19
|
+
- **Typical sizes by context:**
|
|
20
|
+
|
|
21
|
+
| Context | Height |
|
|
22
|
+
|---|---|
|
|
23
|
+
| Favicon | 32px |
|
|
24
|
+
| Mobile header | 28-32px |
|
|
25
|
+
| Desktop header | 32-40px |
|
|
26
|
+
| Footer | 24-28px |
|
|
27
|
+
| Hero/marketing | 48-64px |
|
|
28
|
+
| App splash | 80-120px |
|
|
29
|
+
|
|
30
|
+
```css
|
|
31
|
+
.logo { height: 32px; width: auto; } /* maintain aspect ratio */
|
|
32
|
+
.logo-icon { height: 28px; width: 28px; }
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 2. Logo Placement
|
|
38
|
+
|
|
39
|
+
- **Header:** top-left for LTR, top-right for RTL. Always links to homepage.
|
|
40
|
+
- **Footer:** bottom-left or bottom-center. Smaller than header.
|
|
41
|
+
- **Marketing pages:** centered for hero sections, left-aligned for navigation.
|
|
42
|
+
- **Loading/splash:** centered vertically and horizontally.
|
|
43
|
+
|
|
44
|
+
The header logo is the most important. It should be the first visual element the user sees, but not dominate the page. Keep it slim.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 3. Logo Lockup Variants
|
|
49
|
+
|
|
50
|
+
Every brand needs at least 3 logo variants:
|
|
51
|
+
|
|
52
|
+
1. **Full lockup:** icon + wordmark (for headers, marketing)
|
|
53
|
+
2. **Icon only:** for favicons, app icons, small spaces, mobile headers
|
|
54
|
+
3. **Wordmark only:** for editorial/text-heavy contexts
|
|
55
|
+
|
|
56
|
+
```jsx
|
|
57
|
+
// Responsive logo: icon on mobile, full on desktop
|
|
58
|
+
<Link href="/" className="flex items-center gap-2">
|
|
59
|
+
<img src="/logo-icon.svg" alt="" className="h-7 w-7" />
|
|
60
|
+
<span className="hidden sm:inline text-sm font-bold tracking-tight">Brand Name</span>
|
|
61
|
+
</Link>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 4. Brand Color Usage
|
|
67
|
+
|
|
68
|
+
Follow the 60-30-10 rule with brand colors:
|
|
69
|
+
- **60%** — neutral surfaces (backgrounds, cards). Brand color should NOT be the 60%.
|
|
70
|
+
- **30%** — supporting colors (borders, secondary text, section backgrounds).
|
|
71
|
+
- **10%** — brand/accent color (CTAs, active states, highlights). This is where brand color goes.
|
|
72
|
+
|
|
73
|
+
```css
|
|
74
|
+
:root {
|
|
75
|
+
--brand: oklch(0.55 0.25 250); /* Primary brand color — use at 10% */
|
|
76
|
+
--brand-subtle: oklch(0.95 0.03 250); /* Tinted background — use sparingly */
|
|
77
|
+
--brand-on-dark: oklch(0.70 0.18 250); /* Lighter variant for dark backgrounds */
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Never use brand color as background for large areas (hero sections, full-width bars) unless the brand is specifically known for it (like Spotify's green). Large saturated surfaces are fatiguing.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 5. Brand Typography
|
|
86
|
+
|
|
87
|
+
If the brand has a custom/licensed font, use it for headings only. Body text should be a readable, web-optimized font.
|
|
88
|
+
|
|
89
|
+
```css
|
|
90
|
+
/* Brand font for headings */
|
|
91
|
+
h1, h2, h3 { font-family: 'Brand Display', var(--font-body); }
|
|
92
|
+
|
|
93
|
+
/* Readable font for body */
|
|
94
|
+
body { font-family: 'DM Sans', system-ui, sans-serif; }
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
If the brand font isn't available for web, find the closest web-safe alternative:
|
|
98
|
+
- Futura → use Outfit or Jost
|
|
99
|
+
- Helvetica Neue → use Inter (only acceptable here) or Geist
|
|
100
|
+
- Garamond → use EB Garamond or Cormorant
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 6. Consistency Across Pages
|
|
105
|
+
|
|
106
|
+
Every page should share:
|
|
107
|
+
- Same header and footer (identical, not "similar")
|
|
108
|
+
- Same color palette (no page-specific colors)
|
|
109
|
+
- Same typography scale
|
|
110
|
+
- Same border-radius philosophy
|
|
111
|
+
- Same spacing scale
|
|
112
|
+
|
|
113
|
+
Variation should come from layout and content, not from inconsistent styling. A dashboard page and a marketing page can look different through layout while sharing the same design tokens.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 7. When to Bend Brand Rules
|
|
118
|
+
|
|
119
|
+
Strict brand adherence isn't always right:
|
|
120
|
+
- **Data-dense dashboards:** Brand color as accent only. Don't fight with data colors.
|
|
121
|
+
- **Error states:** Use standard red for errors, not brand color. Users need instant recognition.
|
|
122
|
+
- **Third-party embeds:** Payment forms, maps, chat widgets have their own styling. Don't fight it.
|
|
123
|
+
- **Dark mode:** Brand color may need lightness/chroma adjustment to maintain contrast.
|
|
124
|
+
- **Accessibility:** If brand colors don't meet WCAG contrast, accessibility wins.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## 8. Common Mistakes
|
|
129
|
+
|
|
130
|
+
- **Logo too small in the header.** Minimum 28px height. Users need to identify the brand instantly.
|
|
131
|
+
- **Brand color as full-width background.** Fatiguing. Use at 10% ratio maximum.
|
|
132
|
+
- **Different fonts on every page.** Pick 2 fonts and use them everywhere.
|
|
133
|
+
- **Logo without clear space.** Crowded logos look unprofessional. Enforce minimum padding.
|
|
134
|
+
- **No icon-only variant.** Mobile needs a compact logo. Don't just shrink the full lockup.
|
|
135
|
+
- **Brand color unchanged in dark mode.** Adjust lightness and chroma for dark backgrounds.
|
|
136
|
+
- **Inconsistent border radius between pages.** One sharp page and one rounded page = two brands.
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# Code Typography Reference
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
1. Monospace Font Selection
|
|
5
|
+
2. Code Block Design
|
|
6
|
+
3. Syntax Highlighting Accessibility
|
|
7
|
+
4. Copy-to-Clipboard
|
|
8
|
+
5. Responsive Code Blocks
|
|
9
|
+
6. Inline Code Styling
|
|
10
|
+
7. Diff Views
|
|
11
|
+
8. Terminal Output
|
|
12
|
+
9. Common Mistakes
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 1. Monospace Font Selection
|
|
17
|
+
|
|
18
|
+
| Font | Ligatures | Style | Best For |
|
|
19
|
+
|---|---|---|---|
|
|
20
|
+
| JetBrains Mono | Yes | Clean, geometric | General purpose, IDEs |
|
|
21
|
+
| Fira Code | Yes | Slightly rounded | Tutorials, docs |
|
|
22
|
+
| Source Code Pro | No | Adobe, professional | Enterprise, clean look |
|
|
23
|
+
| IBM Plex Mono | No | Corporate, legible | Documentation |
|
|
24
|
+
| Geist Mono | No | Vercel, modern | Next.js projects |
|
|
25
|
+
| Cascadia Code | Yes | Microsoft, playful | Terminals |
|
|
26
|
+
|
|
27
|
+
```css
|
|
28
|
+
code, pre, .mono {
|
|
29
|
+
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
|
|
30
|
+
font-feature-settings: 'liga' 1, 'calt' 1; /* enable ligatures */
|
|
31
|
+
font-variant-ligatures: common-ligatures;
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Ligatures: `=>` becomes ⇒, `!==` becomes ≢, `>=` becomes ≥. Enable for display, disable for editing if users copy code.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 2. Code Block Design
|
|
40
|
+
|
|
41
|
+
```css
|
|
42
|
+
pre {
|
|
43
|
+
background: var(--surface-1);
|
|
44
|
+
border: 1px solid var(--border);
|
|
45
|
+
border-radius: 8px;
|
|
46
|
+
padding: 1rem 1.25rem;
|
|
47
|
+
font-size: 0.875rem; /* 14px — slightly smaller than body */
|
|
48
|
+
line-height: 1.65; /* Looser than body text for readability */
|
|
49
|
+
overflow-x: auto;
|
|
50
|
+
tab-size: 2;
|
|
51
|
+
-moz-tab-size: 2;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Dark mode code blocks on light sites */
|
|
55
|
+
[data-theme="light"] pre {
|
|
56
|
+
background: oklch(0.14 0.02 230);
|
|
57
|
+
color: oklch(0.90 0.01 230);
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Line numbers (optional):
|
|
62
|
+
```css
|
|
63
|
+
pre.line-numbers {
|
|
64
|
+
counter-reset: line;
|
|
65
|
+
padding-left: 3.5rem;
|
|
66
|
+
position: relative;
|
|
67
|
+
}
|
|
68
|
+
pre.line-numbers .line::before {
|
|
69
|
+
counter-increment: line;
|
|
70
|
+
content: counter(line);
|
|
71
|
+
position: absolute;
|
|
72
|
+
left: 1rem;
|
|
73
|
+
color: var(--text-muted);
|
|
74
|
+
font-size: 0.75rem;
|
|
75
|
+
user-select: none; /* don't copy line numbers */
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 3. Syntax Highlighting Accessibility
|
|
82
|
+
|
|
83
|
+
Every token color must have **minimum 3:1 contrast** against the code block background. Don't rely on color alone — use font-weight or font-style for emphasis.
|
|
84
|
+
|
|
85
|
+
| Token Type | Suggested OKLCH (dark bg) | Purpose |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| Keywords | `oklch(0.75 0.15 300)` | purple-ish, bold |
|
|
88
|
+
| Strings | `oklch(0.72 0.14 150)` | green |
|
|
89
|
+
| Numbers | `oklch(0.75 0.12 60)` | amber |
|
|
90
|
+
| Comments | `oklch(0.50 0.01 230)` | muted, italic |
|
|
91
|
+
| Functions | `oklch(0.78 0.10 230)` | blue |
|
|
92
|
+
| Variables | `oklch(0.85 0.01 230)` | near-white |
|
|
93
|
+
|
|
94
|
+
```css
|
|
95
|
+
.token-keyword { color: oklch(0.75 0.15 300); font-weight: 600; }
|
|
96
|
+
.token-string { color: oklch(0.72 0.14 150); }
|
|
97
|
+
.token-comment { color: oklch(0.50 0.01 230); font-style: italic; }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 4. Copy-to-Clipboard
|
|
103
|
+
|
|
104
|
+
Position: top-right corner of the code block. Show on hover. Provide visual feedback.
|
|
105
|
+
|
|
106
|
+
```jsx
|
|
107
|
+
function CopyButton({ code }) {
|
|
108
|
+
const [copied, setCopied] = useState(false);
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<button
|
|
112
|
+
onClick={() => {
|
|
113
|
+
navigator.clipboard.writeText(code);
|
|
114
|
+
setCopied(true);
|
|
115
|
+
setTimeout(() => setCopied(false), 2000);
|
|
116
|
+
}}
|
|
117
|
+
className="absolute top-2 right-2 p-1.5 rounded-md bg-white/5 hover:bg-white/10 text-xs text-muted"
|
|
118
|
+
aria-label="Copy code"
|
|
119
|
+
>
|
|
120
|
+
{copied ? 'Copied' : 'Copy'}
|
|
121
|
+
</button>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## 5. Responsive Code Blocks
|
|
129
|
+
|
|
130
|
+
Code blocks should **scroll horizontally** on mobile, never wrap. Terminal output CAN wrap.
|
|
131
|
+
|
|
132
|
+
```css
|
|
133
|
+
pre {
|
|
134
|
+
overflow-x: auto;
|
|
135
|
+
white-space: pre; /* code: no wrap */
|
|
136
|
+
-webkit-overflow-scrolling: touch; /* smooth scroll on iOS */
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
pre.terminal {
|
|
140
|
+
white-space: pre-wrap; /* terminal: wrap is OK */
|
|
141
|
+
word-break: break-all;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* Hide scrollbar but keep functionality */
|
|
145
|
+
pre::-webkit-scrollbar { height: 4px; }
|
|
146
|
+
pre::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
On very small screens (< 375px), consider reducing code font-size to 12px.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 6. Inline Code Styling
|
|
154
|
+
|
|
155
|
+
Inline code needs subtle visual distinction from body text:
|
|
156
|
+
|
|
157
|
+
```css
|
|
158
|
+
code:not(pre code) {
|
|
159
|
+
background: var(--surface-2);
|
|
160
|
+
padding: 0.15em 0.4em;
|
|
161
|
+
border-radius: 4px;
|
|
162
|
+
font-size: 0.9em; /* slightly smaller than surrounding text */
|
|
163
|
+
font-weight: 500;
|
|
164
|
+
border: 1px solid var(--border);
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Never use inline code for emphasis. It's for code references (`useState`, `border-radius`, `GET /api/users`), not for highlighting words.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## 7. Diff Views
|
|
173
|
+
|
|
174
|
+
Use color + icon, not color alone (colorblind users):
|
|
175
|
+
|
|
176
|
+
```css
|
|
177
|
+
.diff-add {
|
|
178
|
+
background: oklch(0.62 0.19 150 / 0.1);
|
|
179
|
+
border-left: 3px solid oklch(0.62 0.19 150);
|
|
180
|
+
}
|
|
181
|
+
.diff-add::before { content: '+'; color: oklch(0.62 0.19 150); }
|
|
182
|
+
|
|
183
|
+
.diff-remove {
|
|
184
|
+
background: oklch(0.55 0.22 25 / 0.1);
|
|
185
|
+
border-left: 3px solid oklch(0.55 0.22 25);
|
|
186
|
+
text-decoration: line-through;
|
|
187
|
+
opacity: 0.7;
|
|
188
|
+
}
|
|
189
|
+
.diff-remove::before { content: '-'; color: oklch(0.55 0.22 25); }
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## 8. Terminal Output
|
|
195
|
+
|
|
196
|
+
Terminal/console styling should feel distinct from code:
|
|
197
|
+
|
|
198
|
+
```css
|
|
199
|
+
.terminal {
|
|
200
|
+
background: oklch(0.08 0.01 230);
|
|
201
|
+
color: oklch(0.80 0.01 150); /* slight green tint for terminal feel */
|
|
202
|
+
font-family: 'JetBrains Mono', monospace;
|
|
203
|
+
padding: 1rem;
|
|
204
|
+
border-radius: 8px;
|
|
205
|
+
}
|
|
206
|
+
.terminal .prompt { color: oklch(0.65 0.10 230); } /* blue prompt */
|
|
207
|
+
.terminal .output { color: oklch(0.75 0.01 230); } /* neutral output */
|
|
208
|
+
.terminal .error { color: oklch(0.65 0.22 25); } /* red errors */
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## 9. Common Mistakes
|
|
214
|
+
|
|
215
|
+
- **Code font too large.** 14px for blocks, 0.9em for inline. Larger fights with body text.
|
|
216
|
+
- **No horizontal scroll on code blocks.** Wrapping code breaks readability. Always `overflow-x: auto`.
|
|
217
|
+
- **Syntax colors with < 3:1 contrast.** Comments especially — they tend to be too faint.
|
|
218
|
+
- **Color-only diff indication.** Always add + / - markers or icons alongside color.
|
|
219
|
+
- **Copying includes line numbers.** Use `user-select: none` on line number elements.
|
|
220
|
+
- **Same styling for code blocks and terminal.** They serve different purposes. Terminal gets darker bg, green tint.
|
|
221
|
+
- **`font-family: monospace` without named fonts.** Browsers default to Courier New which looks dated. Always specify a modern monospace font first.
|
|
222
|
+
- **No copy button.** Users shouldn't have to triple-click and drag to copy code.
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Dark Mode Reference
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
1. Preference Hierarchy
|
|
5
|
+
2. CSS Custom Properties Approach
|
|
6
|
+
3. Mode Transition
|
|
7
|
+
4. Surface Elevation in Dark Mode
|
|
8
|
+
5. Color Adjustments
|
|
9
|
+
6. Image and Media Handling
|
|
10
|
+
7. Testing Dark Mode
|
|
11
|
+
8. Forced Colors and High Contrast
|
|
12
|
+
9. Common Mistakes
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 1. Preference Hierarchy
|
|
17
|
+
|
|
18
|
+
System preference is the default. User override persists via localStorage. Never force dark mode without a toggle.
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
// Check system preference, then user override
|
|
22
|
+
const stored = localStorage.getItem('theme');
|
|
23
|
+
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
24
|
+
const isDark = stored ? stored === 'dark' : systemDark;
|
|
25
|
+
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```css
|
|
29
|
+
/* System preference as base */
|
|
30
|
+
:root { color-scheme: light dark; }
|
|
31
|
+
|
|
32
|
+
/* Light (default) */
|
|
33
|
+
:root, [data-theme="light"] {
|
|
34
|
+
--surface-0: oklch(0.99 0.005 var(--hue));
|
|
35
|
+
--text-primary: oklch(0.15 0.02 var(--hue));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Dark */
|
|
39
|
+
[data-theme="dark"] {
|
|
40
|
+
--surface-0: oklch(0.11 0.02 var(--hue));
|
|
41
|
+
--text-primary: oklch(0.93 0.01 var(--hue));
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 2. CSS Custom Properties Approach
|
|
48
|
+
|
|
49
|
+
Define all colors as CSS variables. Never hardcode hex in components. Dark mode is a variable swap, not a rewrite.
|
|
50
|
+
|
|
51
|
+
```css
|
|
52
|
+
[data-theme="dark"] {
|
|
53
|
+
--surface-0: oklch(0.11 0.02 230); /* page bg */
|
|
54
|
+
--surface-1: oklch(0.14 0.02 230); /* card bg */
|
|
55
|
+
--surface-2: oklch(0.17 0.022 230); /* elevated bg */
|
|
56
|
+
--surface-3: oklch(0.20 0.024 230); /* hover bg */
|
|
57
|
+
--border: oklch(0.22 0.015 230);
|
|
58
|
+
--text-primary: oklch(0.93 0.01 230);
|
|
59
|
+
--text-secondary: oklch(0.62 0.02 230);
|
|
60
|
+
--text-muted: oklch(0.42 0.015 230);
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 3. Mode Transition
|
|
67
|
+
|
|
68
|
+
Never instant-swap. Use a 200ms opacity transition on the body. Disable transitions during the swap to prevent every element animating individually.
|
|
69
|
+
|
|
70
|
+
```css
|
|
71
|
+
/* Add this class during theme switch, remove after 200ms */
|
|
72
|
+
.theme-transitioning * {
|
|
73
|
+
transition: none !important;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
body {
|
|
77
|
+
transition: background-color 0.2s ease-out, color 0.2s ease-out;
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
function toggleTheme() {
|
|
83
|
+
document.body.classList.add('theme-transitioning');
|
|
84
|
+
const next = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
|
|
85
|
+
document.documentElement.dataset.theme = next;
|
|
86
|
+
localStorage.setItem('theme', next);
|
|
87
|
+
requestAnimationFrame(() => {
|
|
88
|
+
requestAnimationFrame(() => {
|
|
89
|
+
document.body.classList.remove('theme-transitioning');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 4. Surface Elevation in Dark Mode
|
|
98
|
+
|
|
99
|
+
In dark mode, elevated surfaces get LIGHTER, not darker. This is the opposite of light mode where shadows darken surfaces. Without this, dark mode looks flat.
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
Light mode elevation: surface-0 (lightest) → surface-3 (slightly darker)
|
|
103
|
+
Dark mode elevation: surface-0 (darkest) → surface-3 (slightly lighter)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Shadows are nearly invisible in dark mode. Replace with surface lightness differentiation and subtle border glow:
|
|
107
|
+
|
|
108
|
+
```css
|
|
109
|
+
[data-theme="dark"] .card {
|
|
110
|
+
background: var(--surface-1);
|
|
111
|
+
border: 1px solid oklch(1 0 0 / 0.06);
|
|
112
|
+
/* Shadow optional — use border + surface tint instead */
|
|
113
|
+
box-shadow: 0 0 0 1px oklch(1 0 0 / 0.03);
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 5. Color Adjustments
|
|
120
|
+
|
|
121
|
+
Accent colors need lower chroma in dark mode. Full-saturation accents on dark backgrounds are harsh.
|
|
122
|
+
|
|
123
|
+
```css
|
|
124
|
+
:root { --accent: oklch(0.55 0.25 250); } /* light: saturated */
|
|
125
|
+
[data-theme="dark"] { --accent: oklch(0.65 0.18 250); } /* dark: lighter, less chroma */
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Semantic colors also need adjustment:
|
|
129
|
+
- Success green: lighter, less saturated
|
|
130
|
+
- Error red: lighter to maintain contrast
|
|
131
|
+
- Warning amber: reduce chroma to avoid glowing
|
|
132
|
+
|
|
133
|
+
Text colors: minimum lightness 0.60 in OKLCH for body text on dark backgrounds. Below 0.55 looks washed out on cheap screens even if it passes WCAG.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## 6. Image and Media Handling
|
|
138
|
+
|
|
139
|
+
Dim images slightly in dark mode to reduce glare. SVGs should use currentColor or CSS variables.
|
|
140
|
+
|
|
141
|
+
```css
|
|
142
|
+
[data-theme="dark"] img:not([data-no-dim]) {
|
|
143
|
+
filter: brightness(0.9) contrast(1.05);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
[data-theme="dark"] svg { color: var(--text-primary); }
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
For decorative images, consider `mix-blend-mode: luminosity` to desaturate:
|
|
150
|
+
```css
|
|
151
|
+
[data-theme="dark"] .hero-image {
|
|
152
|
+
mix-blend-mode: luminosity;
|
|
153
|
+
opacity: 0.8;
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## 7. Testing Dark Mode
|
|
160
|
+
|
|
161
|
+
Dark mode is not "invert the colors." Test these specifically:
|
|
162
|
+
|
|
163
|
+
1. **Contrast ratios** — recheck all text/background pairs. WCAG ratios change.
|
|
164
|
+
2. **Shadows** — are they visible? If not, use border or surface tint instead.
|
|
165
|
+
3. **Form inputs** — autofill styling overrides your dark background. Fix with `-webkit-box-shadow: 0 0 0 1000px var(--surface-1) inset`.
|
|
166
|
+
4. **Selection color** — `::selection` background needs to contrast with dark text highlight.
|
|
167
|
+
5. **Scrollbar** — custom scrollbar thumb must be visible on dark track.
|
|
168
|
+
6. **Third-party embeds** — iframes, maps, payment forms may not respect your dark mode.
|
|
169
|
+
7. **Screenshots** — take a screenshot and look at it on a non-retina screen. Subtle contrast often vanishes.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## 8. Forced Colors and High Contrast
|
|
174
|
+
|
|
175
|
+
Windows High Contrast mode (`@media (forced-colors: active)`) overrides ALL custom colors. Your tinted neutrals, subtle borders, and custom focus rings will disappear.
|
|
176
|
+
|
|
177
|
+
```css
|
|
178
|
+
@media (forced-colors: active) {
|
|
179
|
+
.card { border: 1px solid CanvasText; }
|
|
180
|
+
:focus-visible { outline: 2px solid Highlight; }
|
|
181
|
+
.btn-primary { border: 2px solid ButtonText; }
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Never use `forced-color-adjust: none` globally. Only on specific elements where you've provided a system-color fallback.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## 9. Common Mistakes
|
|
190
|
+
|
|
191
|
+
- **Instant theme swap without transition.** Jarring. Always fade (200ms).
|
|
192
|
+
- **Shadows that work in light but vanish in dark.** Replace with borders + surface tint.
|
|
193
|
+
- **Same accent chroma in both modes.** Reduce chroma for dark mode.
|
|
194
|
+
- **Checking contrast only in light mode.** Dark mode ratios are different — recheck.
|
|
195
|
+
- **`background: black` for dark mode.** Never pure black. Tint toward your hue.
|
|
196
|
+
- **Not testing autofill.** Browsers apply white backgrounds to autofilled inputs.
|
|
197
|
+
- **Not testing scrollbar.** Custom scrollbar may be invisible on dark backgrounds.
|
|
198
|
+
- **Storing theme in state instead of localStorage.** Causes flash of wrong theme on reload.
|
|
199
|
+
- **No `color-scheme: dark` declaration.** Browser UI elements (scrollbars, form controls) won't adapt without it.
|