Deleted two obsolete documentation files that are no longer needed: - COLOR-THEME-IMPLEMENTATION.md (204 lines) - Feature complete and documented in code - prompts/003-implement-htmx-indicators.md (427 lines) - Implementation prompt no longer relevant These files served their purpose during development but are now redundant with: - Inline code documentation - TEST-SUMMARY.md for test coverage - README.md for user-facing features - Git history for implementation
24 KiB
Implement System-Aware Theme Switcher with Animated Expanding Button
Implement a comprehensive light/dark/auto theme system that respects the user's system preferences and allows manual override. The feature will include an animated expanding button in the top-right corner that reveals three theme options (Light, Dark, Auto) when interacted with.Key Goals:
- Support three theme modes: Light (force light), Dark (force dark), Auto (follow system)
- Detect and respect system theme preference via
prefers-color-schememedia query - Create an elegant animated button that expands to reveal three options
- Persist user preference in localStorage across sessions
- Apply theme instantly without page reload using CSS classes
Why This Matters:
- Modern UX standard: Users expect dark mode support (65% of users prefer it at night)
- Accessibility: Dark mode reduces eye strain in low-light environments
- System integration: Respecting OS preferences shows attention to detail
- User control: Some users want to override system settings (e.g., dark mode during day)
Desired Implementation:
-
Three Theme Options:
- Light: Force light color scheme regardless of system preference
- Dark: Force dark color scheme regardless of system preference
- Auto: Follow system preference (uses
prefers-color-schememedia query)
-
Animated Button Behavior:
- Default state: Single circular button showing current theme icon
- On hover (desktop): Button expands horizontally left-to-right revealing 3 options
- On click/tap (mobile): Button expands to show 3 options
- Each option shows an icon (sun, moon, auto) and optional label
- Smooth animation: width expansion + fade-in of additional buttons
- Click any option: Collapse button + apply theme + save preference
-
Positioning:
- Fixed position at top-right of viewport
- Always visible (doesn't scroll with page)
- Positioned above other content (high z-index)
- Works on mobile and desktop viewports
-
Persistence:
- Save user preference to localStorage:
localStorage['theme-mode'] - Values: 'light', 'dark', 'auto'
- On page load: Read localStorage and apply saved preference
- If no saved preference: Default to 'auto' (follow system)
- Save user preference to localStorage:
Reference Files: @templates/partials/navigation/view-controls.html - Existing toggle pattern with localStorage @static/css/main.css - Existing theme classes and styles @static/hyperscript/functions._hs - Existing hyperscript functions
Tech Stack:
- CSS custom properties (CSS variables) for theme colors
- CSS
prefers-color-schememedia query for system detection - Hyperscript for button animation and theme switching logic
- localStorage for persistence
- Iconify icons for theme indicators (already in use)
1. Theme System Architecture
CSS Variables Structure: Create a comprehensive set of CSS custom properties for theming:
:root {
/* Light theme (default) */
--bg-primary: #ffffff;
--bg-secondary: #f5f5f5;
--text-primary: #333333;
--text-secondary: #666666;
--border-color: #e0e0e0;
--shadow: rgba(0, 0, 0, 0.1);
}
/* Dark theme - applied via [data-theme="dark"] */
[data-theme="dark"] {
--bg-primary: #1a1a1a;
--bg-secondary: #2a2a2a;
--text-primary: #e0e0e0;
--text-secondary: #b0b0b0;
--border-color: #404040;
--shadow: rgba(0, 0, 0, 0.3);
}
/* Auto theme - uses media query */
@media (prefers-color-scheme: dark) {
[data-theme="auto"] {
/* Same as dark theme variables */
}
}
Why CSS Variables:
- Single source of truth for colors
- Easy to maintain and extend
- Automatically cascade to all components
- Better performance than class-based theme switching for large DOMs
Theme Application:
- Apply theme via
data-themeattribute on<html>or<body> - Values:
data-theme="light",data-theme="dark",data-theme="auto" - JavaScript/Hyperscript sets this attribute based on user selection
2. Animated Button Component
Button States:
-
Collapsed (default):
- Single circular button (~48px diameter)
- Shows icon representing current theme (sun/moon/auto)
- Subtle shadow and hover effect
-
Expanded (on hover/click):
- Expands horizontally to ~160px width (or 3 × 48px = 144px)
- Reveals 3 circular buttons side-by-side
- Each button: icon + optional tooltip label
- Smooth width transition (300ms ease-out)
- Icons fade in with staggered delay for polish
Animation Specifications:
- Expansion:
width: 48px → 160pxover 300ms with ease-out easing - Icon fade: Opacity 0 → 1 over 200ms with 50ms stagger
- Collapse: Reverse animation when mouse leaves (desktop) or after selection
- Use
transformandopacityfor GPU acceleration (avoid width if possible, use scale + clip)
HTML Structure Example:
<div id="theme-switcher" class="theme-switcher"
_="on mouseenter add .expanded to me
on mouseleave remove .expanded from me">
<!-- Current theme indicator (always visible) -->
<button class="theme-btn active" data-theme-mode="auto">
<iconify-icon icon="mdi:theme-light-dark" width="20"></iconify-icon>
</button>
<!-- Additional options (visible when expanded) -->
<button class="theme-btn" data-theme-mode="light"
_="on click call setTheme('light')">
<iconify-icon icon="mdi:white-balance-sunny" width="20"></iconify-icon>
<span class="tooltip">Light</span>
</button>
<button class="theme-btn" data-theme-mode="dark"
_="on click call setTheme('dark')">
<iconify-icon icon="mdi:moon-waning-crescent" width="20"></iconify-icon>
<span class="tooltip">Dark</span>
</button>
<button class="theme-btn" data-theme-mode="auto"
_="on click call setTheme('auto')">
<iconify-icon icon="mdi:theme-light-dark" width="20"></iconify-icon>
<span class="tooltip">Auto</span>
</button>
</div>
Responsive Behavior:
- Desktop (>768px): Expand on hover, collapse on mouse leave
- Mobile/Tablet (≤768px): Expand on tap, collapse on background tap or selection
- Use media query + hyperscript to detect and apply appropriate behavior
3. Theme Switching Logic
Hyperscript Function:
Create a global setTheme(mode) function in static/hyperscript/functions._hs:
def setTheme(mode)
-- Save preference to localStorage
set localStorage['theme-mode'] to mode
-- Apply theme to document
if mode is 'light'
set document.documentElement's @data-theme to 'light'
else if mode is 'dark'
set document.documentElement's @data-theme to 'dark'
else if mode is 'auto'
set document.documentElement's @data-theme to 'auto'
end
-- Update button active states
set buttons to .theme-btn in #theme-switcher
for btn in buttons
if btn's @data-theme-mode is mode
add .active to btn
else
remove .active from btn
end
end
-- Collapse button on mobile
if window.innerWidth <= 768
remove .expanded from #theme-switcher
end
end
Page Load Theme Detection: Create an initialization script that runs immediately on page load:
def initTheme()
-- Get saved preference or default to 'auto'
set savedTheme to localStorage['theme-mode'] or 'auto'
call setTheme(savedTheme)
end
-- Run on page load
on load call initTheme()
System Preference Detection: Listen for system theme changes when in 'auto' mode:
// Optional: Listen for system theme changes
const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');
darkModeQuery.addEventListener('change', (e) => {
const currentMode = localStorage.getItem('theme-mode');
if (currentMode === 'auto' || !currentMode) {
// Theme will automatically update via CSS media query
// No action needed, but could trigger a visual indicator
}
});
4. Positioning and Layout
Fixed Positioning:
.theme-switcher {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000; /* Above all content */
display: flex;
gap: 4px;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 24px;
padding: 4px;
box-shadow: 0 2px 8px var(--shadow);
width: 56px; /* Single button + padding */
transition: width 300ms ease-out;
overflow: hidden;
}
.theme-switcher.expanded {
width: 176px; /* 3 buttons + gaps + padding */
}
.theme-btn {
min-width: 48px;
height: 48px;
border-radius: 50%;
border: none;
background: transparent;
cursor: pointer;
transition: background 200ms ease;
opacity: 1;
}
.theme-btn:not(.active) {
opacity: 0.5;
}
.theme-switcher:not(.expanded) .theme-btn:not(.active) {
display: none; /* Hide non-active buttons when collapsed */
}
.theme-btn:hover {
background: var(--bg-secondary);
}
.theme-btn.active {
background: var(--bg-secondary);
}
Mobile Considerations:
- Increase touch target size to minimum 44×44px (iOS HIG)
- Ensure button doesn't overlap with hamburger menu or other controls
- Consider adding backdrop overlay when expanded on mobile
- Position may need adjustment on smaller screens (e.g.,
top: 16px; right: 16px)
5. Color Scheme Design
Light Theme Colors:
- Background: White to light gray (#ffffff, #f5f5f5)
- Text: Dark gray to black (#333333, #1a1a1a)
- Accent: Keep existing brand colors
- Borders: Light gray (#e0e0e0)
Dark Theme Colors:
- Background: Very dark gray to black (#1a1a1a, #0a0a0a)
- Text: Light gray to white (#e0e0e0, #ffffff)
- Accent: Slightly brighter versions of brand colors
- Borders: Medium dark gray (#404040)
Design Principles:
- Maintain sufficient contrast (WCAG AA: 4.5:1 for text, AAA: 7:1 preferred)
- Don't use pure black (#000000) for dark backgrounds (too harsh)
- Don't use pure white text on dark backgrounds (causes halation)
- Test with both themes for readability and accessibility
6. Persistence and State Management
localStorage Schema:
// Store theme preference
localStorage.setItem('theme-mode', 'light'); // or 'dark', 'auto'
// Read on page load
const savedTheme = localStorage.getItem('theme-mode') || 'auto';
State Synchronization:
- Button active state must always reflect current theme
- System theme changes should update UI if in 'auto' mode
- Manual theme selection should override system preference
- Theme should apply before page render to avoid flash (FOUC)
Initial Load Optimization:
To prevent flash of wrong theme, add inline script in <head>:
<script>
(function() {
const theme = localStorage.getItem('theme-mode') || 'auto';
document.documentElement.setAttribute('data-theme', theme);
})();
</script>
This runs before page render, applying theme instantly.
7. Integration with Existing Theme System
Current System:
.theme-cleanclass toggles between default and clean layouts- This is a layout theme, not a color theme
New System:
data-themeattribute controls color theme (light/dark/auto).theme-cleanclass still controls layout theme
Both Can Coexist:
<body class="theme-clean" data-theme="dark">
<!-- Clean layout + Dark colors -->
</body>
CSS Organization:
- Keep existing
.theme-cleanstyles unchanged - Add new
[data-theme="dark"]styles for colors - Ensure both systems work independently and together
Step-by-Step Implementation Plan
Phase 1: Create CSS Theme Variables
-
Add CSS Variables to
static/css/main.css:- Define
:root(light theme) color variables - Define
[data-theme="dark"]color variables - Define
@media (prefers-color-scheme: dark)with[data-theme="auto"] - Update existing components to use CSS variables instead of hardcoded colors
- Define
-
Color Mapping Strategy:
- Audit existing colors in
main.css - Create a mapping from hardcoded colors to CSS variable names
- Replace colors incrementally (high-impact areas first: backgrounds, text, borders)
- Audit existing colors in
Phase 2: Create Theme Switcher Button Component
-
Create HTML Template:
- New file:
templates/partials/theme-switcher.html - Structure: Container with 3 buttons (light, dark, auto)
- Include iconify icons for each theme
- Add hyperscript for expand/collapse behavior
- New file:
-
Create CSS Styles:
- Add to
static/css/main.cssor newstatic/css/theme-switcher.css - Fixed positioning at top-right
- Collapsed and expanded states
- Button hover and active states
- Smooth transitions and animations
- Responsive behavior (desktop vs mobile)
- Add to
-
Include in Main Layout:
- Add
{{template "theme-switcher" .}}to main layout template - Position below action bar but above content (z-index management)
- Add
Phase 3: Implement Theme Switching Logic
-
Add Hyperscript Functions to
static/hyperscript/functions._hs:setTheme(mode)- Apply theme and save to localStorageinitTheme()- Load saved theme on page load- Button click handlers for each theme option
-
Add Inline Script for FOUC Prevention:
- In
<head>of main template, add inline script - Reads localStorage and sets
data-themebefore render - Prevents flash of wrong theme
- In
-
System Theme Detection (Optional Enhancement):
- Add media query listener for system theme changes
- Update UI when system preference changes (if in 'auto' mode)
Phase 4: Update Existing Components with Theme Variables
Priority Order:
- Main backgrounds and text (highest visual impact)
- CV paper and content areas
- Navigation and controls
- Borders and shadows
- Accent colors and highlights
Example Refactor:
/* Before */
.cv-container {
background: #ffffff;
color: #333333;
}
/* After */
.cv-container {
background: var(--bg-primary);
color: var(--text-primary);
}
Phase 5: Testing and Refinement
-
Visual Testing:
- Test all three theme modes (light, dark, auto)
- Verify color contrast meets WCAG AA standards
- Check all components render correctly in both themes
-
Interaction Testing:
- Button expands smoothly on hover (desktop)
- Button expands/collapses on tap (mobile)
- Theme applies instantly when selected
- Active state updates correctly
-
Persistence Testing:
- Save theme preference and reload page
- Verify saved theme is applied before render (no FOUC)
- Clear localStorage and verify default to 'auto'
-
System Integration Testing:
- Change system theme preference (OS settings)
- Verify 'auto' mode respects system preference
- Verify 'light' and 'dark' modes override system
What to Prioritize:
- Core theme switching functionality
- FOUC prevention (inline script)
- Button animation and UX
- High-impact component theming (backgrounds, text)
- Fine-tuning colors and contrast
What to Avoid:
- Don't try to theme every single pixel in first pass - prioritize high-impact areas
- Don't use JavaScript for theme application if CSS can handle it (performance)
- Don't forget mobile UX - touch targets, tap behavior, responsive design
- Don't hardcode colors - always use CSS variables
- Don't sacrifice accessibility for aesthetics (contrast ratios are critical)
Why These Constraints Matter:
- CSS Variables: Maintainable, performant, scalable theming system
- FOUC Prevention: Critical for professional UX - theme must apply before render
- Mobile-First: Touch devices are primary interaction method for many users
- Accessibility: WCAG compliance is non-negotiable for professional applications
- Progressive Enhancement: Light theme works even if JavaScript fails
-
./templates/partials/theme-switcher.html(NEW)- Animated theme switcher button component
- Three buttons for light, dark, auto modes
- Hyperscript for expand/collapse and theme selection
- Iconify icons for each theme option
-
./static/css/theme-variables.css(NEW) or add to./static/css/main.css- CSS custom properties for light theme (
:root) - CSS custom properties for dark theme (
[data-theme="dark"]) - Media query for auto mode (
@media (prefers-color-scheme: dark)) - Theme switcher button styles
- CSS custom properties for light theme (
-
./static/hyperscript/functions._hs- Add
setTheme(mode)function - Add
initTheme()function - Add page load initialization
- Add
-
./templates/index.html(or main layout template)- Include theme switcher component
- Add inline FOUC prevention script in
<head>
-
./static/css/main.css- Refactor existing hardcoded colors to use CSS variables
- Update backgrounds, text, borders, shadows
- Ensure compatibility with both theme systems (layout + color)
Optional (recommended):
6. ./static/js/theme-system-listener.js (NEW)
- Listen for system theme changes
- Update UI when system preference changes in 'auto' mode
- Only needed for dynamic system theme updates
1. Visual Verification:
- Light mode: All text readable, good contrast, professional appearance
- Dark mode: All text readable, good contrast, not too harsh
- Auto mode: Follows system preference correctly
- Button expands smoothly showing 3 options
- Button collapses smoothly after selection
- Active button is visually distinct
2. Interaction Testing:
- Desktop: Button expands on hover, collapses on mouse leave
- Mobile: Button expands on tap, collapses on selection or backdrop tap
- Click light button: Theme switches to light instantly
- Click dark button: Theme switches to dark instantly
- Click auto button: Theme follows system preference
- No delay or lag in theme application
3. Persistence Testing:
- Select light theme, reload page → Still light
- Select dark theme, reload page → Still dark
- Select auto theme, reload page → Still auto (follows system)
- Clear localStorage → Defaults to auto mode
- No flash of unstyled content (FOUC) on page load
4. System Integration:
- Set system to light mode, set app to auto → Light theme applied
- Change system to dark mode with app in auto → Dark theme applied
- Set app to light mode, change system to dark → Stays light (override works)
- Media query correctly detects system preference
5. Accessibility Testing:
- Light mode: WCAG AA contrast ratio for all text (4.5:1 minimum)
- Dark mode: WCAG AA contrast ratio for all text
- Button tooltips/labels are readable
- Keyboard navigation works (tab through theme options)
- Focus states are visible
- Screen reader announces theme changes
6. Component Coverage:
- CV container background themed correctly
- Text colors themed correctly (headings, body, secondary)
- Navigation elements themed correctly
- Borders and dividers visible in both themes
- Shadows appropriate for both themes
- Icons and images work in both themes
- Toggle controls remain functional and readable
7. Layout Compatibility:
- Theme works with default layout (not
.theme-clean) - Theme works with
.theme-cleanlayout - Both theme systems can be used simultaneously
- No conflicts between layout theme and color theme
8. Animation Performance:
- Button expansion is smooth (60fps, no jank)
- Theme switching is instant (no visible delay)
- GPU-accelerated properties used (opacity, transform)
- No layout thrashing during animations
9. Mobile Specific:
- Button positioned correctly on mobile (no overlap with other controls)
- Touch targets are adequate size (44×44px minimum)
- Tap behavior works correctly (expand/collapse)
- Responsive design adapts to small screens
10. Edge Cases:
- Very fast clicking doesn't break state
- System theme change during 'auto' mode updates correctly
- Works in private/incognito mode (localStorage available)
- Graceful degradation if JavaScript disabled (defaults to light)
Success Indicators:
✅ Three theme modes work correctly (light, dark, auto)
✅ System preference detected and respected in auto mode
✅ Theme persists across page reloads (localStorage)
✅ No FOUC - theme applies before page render
✅ Animated button expands/collapses smoothly
✅ Responsive behavior (hover on desktop, tap on mobile)
✅ All text meets WCAG AA contrast standards
✅ Theme switching is instant and smooth
✅ Works alongside existing .theme-clean layout system
✅ Professional, polished UX
<success_criteria>
- Three distinct theme modes implemented: Light, Dark, Auto (system)
- System preference correctly detected via
prefers-color-schememedia query - User preference persists in localStorage across sessions
- Animated expanding button reveals theme options smoothly (300ms transition)
- Responsive behavior: hover on desktop (>768px), tap on mobile (≤768px)
- Fixed positioning at top-right, always visible, doesn't interfere with content
- No flash of unstyled content (FOUC) - theme applies before render
- All text meets WCAG AA contrast ratio (4.5:1 for normal text)
- Theme applies instantly on selection (<50ms perceived delay)
- Compatible with existing
.theme-cleanlayout system - Accessible keyboard navigation and screen reader support
- Smooth animations using GPU-accelerated properties
- Code follows existing project patterns and conventions
- Implementation is maintainable and well-documented </success_criteria>
Best Practices:
- Avoid pure black in dark mode (use #1a1a1a or similar)
- Maintain consistent contrast ratios
- Use elevation (shadows) to create depth in dark mode
- Test with real system theme preferences
- Prevent FOUC with inline script in head
Icon Suggestions (iconify):
- Light mode:
mdi:white-balance-sunnyormdi:brightness-7 - Dark mode:
mdi:moon-waning-crescentormdi:weather-night - Auto mode:
mdi:theme-light-darkormdi:brightness-auto
Animation Patterns:
- Expand: ease-out (starts fast, ends slow)
- Collapse: ease-in (starts slow, ends fast)
- Theme switch: instant (no transition on color change)
- Icons: staggered fade-in for polish
Accessibility:
- WCAG AA: 4.5:1 for normal text, 3:1 for large text
- WCAG AAA: 7:1 for normal text, 4.5:1 for large text
- Test with actual screen readers (VoiceOver, NVDA)
- Ensure focus indicators are visible in both themes
Questions to Answer:
- What colors are currently hardcoded and need to become variables?
- How is the existing
.theme-cleansystem implemented? - Where should the theme switcher button be positioned to not conflict with existing UI?
- What iconify icons are already in use (maintain consistency)?
- What localStorage keys are already in use (avoid conflicts)?
<additional_notes> Implementation Philosophy:
- Progressive enhancement: Works without JavaScript (defaults to light)
- Mobile-first: Touch interactions are primary, hover is enhancement
- Performance-first: CSS handles theme, JavaScript only manages state
- Accessibility-first: WCAG compliance is non-negotiable
- Maintainability: CSS variables make future updates trivial
Design Considerations:
- Button should feel premium and polished (subtle shadows, smooth animations)
- Don't over-animate - smooth and subtle is better than flashy
- Dark mode should be comfortable for extended reading, not just "looks cool"
- Auto mode should be the intelligent default (respects user's OS preference)
Future Enhancements:
- Add transition animation when theme changes (optional fade)
- Support custom theme colors (user-selectable accent colors)
- Add sunrise/sunset auto-scheduling (auto-switch based on time)
- Sync theme preference across devices (server-side storage)
- Add "high contrast" mode for accessibility </additional_notes>