Initial commit — Singular Particular Space v1

Homepage (site/index.html): integration-v14 promoted, Writings section
integrated with 33 pieces clustered by type (stories/essays/miscellany),
Writings welcome lightbox, content frame at 98% opacity.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-27 12:09:22 +02:00
commit 5422131782
359 changed files with 117437 additions and 0 deletions

View File

@@ -0,0 +1,446 @@
# Theming & Dark Mode Design
Creating effective color themes and implementing dark mode correctly.
## Dark Mode Philosophy
Dark mode isn't just inverting colors—it requires deliberate design decisions to maintain usability, hierarchy, and aesthetics.
### Why Dark Mode Matters
- **User preference:** Many users prefer it
- **Eye strain:** Reduces strain in low-light environments
- **Battery life:** Saves power on OLED screens
- **Accessibility:** Some users have photosensitivity
- **Professional expectation:** Users expect modern apps to support it
---
## Dark Mode Color Principles
### Don't Just Invert
| Light Mode | Bad Dark Mode | Good Dark Mode |
|------------|---------------|----------------|
| White `#ffffff` | Black `#000000` | Dark gray `#18181b` |
| Black text `#000000` | White text `#ffffff` | Off-white `#fafafa` |
| Gray `#6b7280` | Gray `#6b7280` | Lighter gray `#a1a1aa` |
### Key Principles
**1. Use dark grays, not pure black**
Pure black (`#000000`) creates harsh contrast and "halation" (text appears to glow).
```css
/* Background scale for dark mode */
--bg-base: #09090b; /* Deepest background */
--bg-subtle: #18181b; /* Cards, elevated surfaces */
--bg-muted: #27272a; /* Hover states, inputs */
--bg-emphasis: #3f3f46; /* Active states */
```
**2. Reduce contrast slightly**
Max contrast in dark mode is harsher than in light mode.
```css
/* Text colors for dark mode */
--text-primary: #fafafa; /* ~95% white, not 100% */
--text-secondary: #a1a1aa; /* Muted text */
--text-tertiary: #71717a; /* Subtle text */
```
**3. Desaturate colors**
Bright saturated colors on dark backgrounds cause eye strain.
```css
/* Light mode brand color */
--primary-light: #3b82f6; /* Bright blue */
/* Dark mode - slightly desaturated */
--primary-dark: #60a5fa; /* Lighter, less saturated */
```
**4. Elevate with lightness, not shadow**
In dark mode, shadows are invisible. Show elevation with lighter surfaces.
```css
/* Light mode: shadow for depth */
.card-light {
background: white;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
/* Dark mode: lighter surface for depth */
.card-dark {
background: #27272a; /* Lighter than base */
box-shadow: none; /* Or very subtle */
}
```
---
## Building a Dark Mode Palette
### Step 1: Define Your Gray Scale
Create 9-10 shades from near-black to near-white:
```css
/* Dark mode gray scale (Zinc example) */
--gray-950: #09090b; /* Deepest background */
--gray-900: #18181b; /* Card backgrounds */
--gray-800: #27272a; /* Elevated surfaces */
--gray-700: #3f3f46; /* Borders, dividers */
--gray-600: #52525b; /* Disabled states */
--gray-500: #71717a; /* Placeholder text */
--gray-400: #a1a1aa; /* Secondary text */
--gray-300: #d4d4d8; /* Primary text (alt) */
--gray-200: #e4e4e7; /* Headings */
--gray-100: #f4f4f5; /* Emphasis text */
--gray-50: #fafafa; /* Primary text */
```
### Step 2: Adjust Accent Colors
```css
/* Primary color adjustments */
/* Light mode: use 500-600 range */
--primary-light: #2563eb;
/* Dark mode: use 400-500 range (lighter) */
--primary-dark: #3b82f6;
/* Same for semantic colors */
--success-light: #16a34a;
--success-dark: #22c55e;
--error-light: #dc2626;
--error-dark: #ef4444;
```
### Step 3: Define Semantic Tokens
```css
/* Semantic tokens that switch based on mode */
:root {
--color-bg: var(--gray-50);
--color-bg-subtle: var(--gray-100);
--color-text: var(--gray-900);
--color-text-muted: var(--gray-600);
--color-border: var(--gray-200);
--color-primary: var(--blue-600);
}
[data-theme="dark"] {
--color-bg: var(--gray-950);
--color-bg-subtle: var(--gray-900);
--color-text: var(--gray-50);
--color-text-muted: var(--gray-400);
--color-border: var(--gray-800);
--color-primary: var(--blue-400);
}
```
---
## Implementation Strategies
### Strategy 1: CSS Custom Properties
```css
:root {
--bg: #ffffff;
--text: #18181b;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #18181b;
--text: #fafafa;
}
}
body {
background: var(--bg);
color: var(--text);
}
```
### Strategy 2: Data Attribute + Class
```html
<html data-theme="dark">
```
```css
[data-theme="light"] {
--bg: #ffffff;
}
[data-theme="dark"] {
--bg: #18181b;
}
```
```javascript
// Toggle theme
function toggleTheme() {
const current = document.documentElement.dataset.theme;
document.documentElement.dataset.theme = current === 'dark' ? 'light' : 'dark';
localStorage.setItem('theme', document.documentElement.dataset.theme);
}
// Initialize from preference
function initTheme() {
const saved = localStorage.getItem('theme');
const preferred = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
document.documentElement.dataset.theme = saved || preferred;
}
```
### Strategy 3: Tailwind Dark Mode
```html
<!-- With class strategy -->
<html class="dark">
<body class="bg-white dark:bg-zinc-950 text-zinc-900 dark:text-zinc-50">
```
```javascript
// tailwind.config.js
module.exports = {
darkMode: 'class', // or 'media' for system preference only
}
```
---
## Component Considerations
### Cards and Surfaces
```css
/* Light: white with shadow */
.card {
background: white;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
/* Dark: lighter surface, subtle or no shadow */
[data-theme="dark"] .card {
background: var(--gray-900);
box-shadow: 0 1px 3px rgba(0,0,0,0.3); /* Darker shadow if any */
/* Or: border: 1px solid var(--gray-800); */
}
```
### Form Inputs
```css
.input {
background: white;
border: 1px solid var(--gray-300);
}
[data-theme="dark"] .input {
background: var(--gray-900);
border: 1px solid var(--gray-700);
}
```
### Buttons
```css
/* Primary button */
.btn-primary {
background: var(--primary);
color: white;
}
[data-theme="dark"] .btn-primary {
/* Often same or slightly adjusted */
background: var(--primary-dark);
}
/* Secondary button */
.btn-secondary {
background: var(--gray-100);
color: var(--gray-900);
}
[data-theme="dark"] .btn-secondary {
background: var(--gray-800);
color: var(--gray-100);
}
```
### Images and Media
```css
/* Reduce brightness/contrast of images in dark mode */
[data-theme="dark"] img:not([data-no-dim]) {
filter: brightness(0.9) contrast(1.1);
}
/* Invert diagrams/illustrations if needed */
[data-theme="dark"] .diagram {
filter: invert(1) hue-rotate(180deg);
}
```
### Syntax Highlighting
Don't forget code blocks need dark mode variants:
- Use dark theme variants of syntax highlighters
- Or invert colors appropriately
- Popular: One Dark, Dracula, Night Owl
---
## Testing Dark Mode
### Checklist
- [ ] All text is readable (sufficient contrast)
- [ ] Hierarchy still clear (headings vs body)
- [ ] Focus states visible
- [ ] Images don't blow out
- [ ] Forms inputs clearly visible
- [ ] Error/success states distinct
- [ ] Loading states visible
- [ ] Shadows/elevation still work
- [ ] Icons visible (may need color swap)
- [ ] Brand colors still recognizable
### Contrast Ratios
Same WCAG requirements apply:
- Normal text: 4.5:1 minimum
- Large text: 3:1 minimum
- UI components: 3:1 minimum
**Common dark mode fails:**
- Gray text on dark background
- Colored text on colored backgrounds
- Disabled states too subtle
---
## Theme Toggle UI
### Placement
- Header/navigation (most common)
- Settings page
- Footer (less common)
### Icon Patterns
```html
<!-- Sun/Moon toggle -->
<button aria-label="Toggle dark mode">
<svg class="sun hidden dark:block">...</svg>
<svg class="moon block dark:hidden">...</svg>
</button>
```
### State Options
1. **Light / Dark** - Simple toggle
2. **Light / Dark / System** - Respect OS preference option
3. **Auto only** - Always follow system (no toggle)
### Persistence
```javascript
// Save preference
localStorage.setItem('theme', 'dark');
// Load preference (with system fallback)
const theme = localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
```
---
## Advanced: Multiple Themes
### Brand Themes
```css
[data-theme="brand-a"] {
--primary: #ff6b6b;
--primary-hover: #ee5a5a;
}
[data-theme="brand-b"] {
--primary: #4ecdc4;
--primary-hover: #3dbdb5;
}
```
### Theme Structure
```css
/* Base tokens (don't change) */
--spacing-4: 16px;
--radius-md: 8px;
/* Color tokens (change per theme) */
--color-primary: ...;
--color-bg: ...;
/* Component tokens (reference color tokens) */
--button-bg: var(--color-primary);
--card-bg: var(--color-bg);
```
---
## Common Mistakes
| Mistake | Problem | Fix |
|---------|---------|-----|
| Pure black background | Harsh, looks flat | Use dark gray (#18181b) |
| Pure white text | Too much contrast | Use off-white (#fafafa) |
| Same saturated colors | Eye strain | Desaturate for dark mode |
| Shadows for elevation | Invisible in dark | Use lighter surfaces |
| Forgetting images | Can be too bright | Dim images slightly |
| One contrast check | Colors interact differently | Check all combinations |
| Forgetting focus states | Invisible borders | Ensure visible focus rings |
---
## Quick Reference
### Minimum Viable Dark Mode
```css
:root {
--bg: #ffffff;
--bg-subtle: #f4f4f5;
--text: #18181b;
--text-muted: #71717a;
--border: #e4e4e7;
--primary: #2563eb;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #18181b;
--bg-subtle: #27272a;
--text: #fafafa;
--text-muted: #a1a1aa;
--border: #3f3f46;
--primary: #3b82f6;
}
}
body {
background: var(--bg);
color: var(--text);
}
```