Overview
Astro Portfolio v3 features a dynamic theming system that allows users to switch between three distinct visual themes: Light, Dark, and Retro. The theme preference is persisted in localStorage and respects the user’s system preferences.
The theme switching logic is implemented in ~/workspace/source/src/components/shared/ThemeSwitcher.astro using vanilla JavaScript with Tailwind CSS utility classes.
Available Themes
Light Theme Clean, bright theme for daytime viewing with high contrast and readability
Dark Theme Easy on the eyes for low-light environments with reduced eye strain
Retro Theme Nostalgic sepia-toned theme with a vintage aesthetic
How It Works
Theme Switcher Component
The ThemeSwitcher component displays different icons based on the current theme and allows users to cycle through themes by clicking:
---
// src/components/shared/ThemeSwitcher.astro:1-44
---
< button
class = 'theme-toggle p-2 rounded-md flex items-center justify-center hover:bg-accent transition-colors relative'
aria-label = 'Toggle theme'
>
<!-- Sun icon (Light theme) -->
< svg
class = 'w-5 h-5 rotate-0 scale-100 transition-all dark:rotate-90 dark:scale-0 retro:rotate-90 retro:scale-0'
fill = 'none'
viewBox = '0 0 24 24'
stroke = 'currentColor'
>
< path
stroke-linecap = 'round'
stroke-linejoin = 'round'
stroke-width = '2'
d = 'M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z'
></ path >
</ svg >
<!-- Moon icon (Dark theme) -->
< svg
class = 'absolute w-5 h-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100 retro:rotate-90 retro:scale-0'
fill = 'none'
viewBox = '0 0 24 24'
stroke = 'currentColor'
>
< path
stroke-linecap = 'round'
stroke-linejoin = 'round'
stroke-width = '2'
d = 'M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z'
></ path >
</ svg >
<!-- Retro icon (Retro theme) -->
< svg
class = 'absolute w-5 h-5 rotate-90 scale-0 transition-all retro:rotate-0 retro:scale-100'
fill = 'none'
viewBox = '0 0 24 24'
stroke = 'currentColor'
>
< path
stroke-linecap = 'round'
stroke-linejoin = 'round'
stroke-width = '2'
d = 'M20 7h-3m-3 10a6 6 0 01-6-6V7a2 2 0 012-2h6a2 2 0 012 2v4a6 6 0 01-1 3.33m0 0A6 6 0 0120 11v2a2 2 0 01-2 2h-1'
></ path >
</ svg >
</ button >
< script >
// Theme switching logic
</ script >
Location: src/components/shared/ThemeSwitcher.astro:1
Theme Logic
The component includes a client-side script that handles theme detection, switching, and persistence:
Type Definition
Getting Theme
Setting Theme
Cycling Themes
// src/components/shared/ThemeSwitcher.astro:47
type Theme = 'light' | 'dark' | 'retro' ;
The theme type ensures type safety when working with themes. // src/components/shared/ThemeSwitcher.astro:49-56
const getTheme = () : Theme => {
if ( typeof localStorage !== 'undefined' && localStorage . getItem ( 'theme' )) {
return localStorage . getItem ( 'theme' ) as Theme ;
}
return window . matchMedia ( '(prefers-color-scheme: dark)' ). matches
? 'dark'
: 'light' ;
};
Priority:
Check localStorage for saved preference
Fall back to system preference (prefers-color-scheme)
Default to light theme
// src/components/shared/ThemeSwitcher.astro:58-62
const setTheme = ( theme : Theme ) => {
document . documentElement . classList . remove ( 'light' , 'dark' , 'retro' );
document . documentElement . classList . add ( theme );
localStorage . setItem ( 'theme' , theme );
};
Actions:
Remove all theme classes from <html> element
Add the new theme class
Save preference to localStorage
// src/components/shared/ThemeSwitcher.astro:64-69
const cycleTheme = () => {
const themes : Theme [] = [ 'light' , 'dark' , 'retro' ];
const current = getTheme ();
const next = themes [( themes . indexOf ( current ) + 1 ) % themes . length ];
setTheme ( next );
};
Cycle order:
Light → Dark → Retro → Light → …
Initialization
The theme system initializes on page load and after view transitions:
// src/components/shared/ThemeSwitcher.astro:71-82
function initThemeToggle () {
const themeToggles = document . querySelectorAll ( '.theme-toggle' );
themeToggles . forEach (( toggle ) => {
toggle . addEventListener ( 'click' , cycleTheme );
});
setTheme ( getTheme ());
}
initThemeToggle ();
// Re-initialize after Astro view transitions
document . addEventListener ( 'astro:after-swap' , initThemeToggle );
The astro:after-swap event ensures the theme switcher works correctly with Astro’s view transitions feature.
Using Themes in Styles
Tailwind CSS Utilities
The project uses Tailwind’s arbitrary variant feature to apply theme-specific styles:
<!-- Element visible only in light theme -->
< div class = 'block dark:hidden retro:hidden' >
Light theme content
</ div >
<!-- Element visible only in dark theme -->
< div class = 'hidden dark:block retro:hidden' >
Dark theme content
</ div >
<!-- Element visible only in retro theme -->
< div class = 'hidden dark:hidden retro:block' >
Retro theme content
</ div >
<!-- Different colors per theme -->
< h1 class = 'text-gray-900 dark:text-gray-100 retro:text-amber-900' >
Theme-aware heading
</ h1 >
Icon Transitions
The ThemeSwitcher uses transform and scale to smoothly transition between icons:
<!-- Shows in light theme -->
< svg class = 'rotate-0 scale-100 dark:rotate-90 dark:scale-0 retro:rotate-90 retro:scale-0' >
<!-- Sun icon -->
</ svg >
<!-- Shows in dark theme -->
< svg class = 'rotate-90 scale-0 dark:rotate-0 dark:scale-100 retro:rotate-90 retro:scale-0' >
<!-- Moon icon -->
</ svg >
<!-- Shows in retro theme -->
< svg class = 'rotate-90 scale-0 retro:rotate-0 retro:scale-100' >
<!-- Retro icon -->
</ svg >
CSS Variables
Themes can also be implemented using CSS custom properties:
/* Global styles */
:root {
--color-primary : #3b82f6 ;
--color-background : #ffffff ;
--color-text : #1f2937 ;
}
.dark {
--color-primary : #60a5fa ;
--color-background : #1f2937 ;
--color-text : #f9fafb ;
}
.retro {
--color-primary : #d97706 ;
--color-background : #fef3c7 ;
--color-text : #78350f ;
}
Then use the variables in your components:
< div style = "background-color: var(--color-background); color: var(--color-text);" >
Theme-aware content
</ div >
Tailwind Configuration
To enable the custom retro: variant, configure Tailwind:
// tailwind.config.js
module . exports = {
darkMode: 'class' ,
theme: {
extend: {},
},
plugins: [
function ({ addVariant }) {
addVariant ( 'retro' , '.retro &' );
}
],
};
The darkMode: 'class' setting allows Tailwind’s dark: variant to work with class-based theme switching instead of media queries.
Integration in Navbar
The ThemeSwitcher is integrated into the main navigation:
---
// src/components/navs/Navbar.astro:110-113
import ThemeSwitcher from '@/components/shared/ThemeSwitcher.astro' ;
---
< nav >
<!-- Navigation links -->
< div class = 'flex items-center gap-2 border-l border-border pl-4' >
<!-- <LanguageSwitcher /> -->
< ThemeSwitcher />
</ div >
</ nav >
Location: src/components/navs/Navbar.astro:110-113
Implementing Custom Themes
Adding a New Theme
To add a new theme (e.g., “ocean”):
Step 1: Update Type Definition
// In ThemeSwitcher.astro
type Theme = 'light' | 'dark' | 'retro' | 'ocean' ;
Step 2: Update Theme Array
const cycleTheme = () => {
const themes : Theme [] = [ 'light' , 'dark' , 'retro' , 'ocean' ];
const current = getTheme ();
const next = themes [( themes . indexOf ( current ) + 1 ) % themes . length ];
setTheme ( next );
};
Step 3: Add Theme Icon
<!-- Ocean theme icon -->
< svg
class = 'absolute w-5 h-5 rotate-90 scale-0 transition-all ocean:rotate-0 ocean:scale-100'
fill = 'none'
viewBox = '0 0 24 24'
stroke = 'currentColor'
>
<!-- Ocean wave icon path -->
</ svg >
// tailwind.config.js
plugins : [
function ({ addVariant }) {
addVariant ( 'retro' , '.retro &' );
addVariant ( 'ocean' , '.ocean &' ); // Add new variant
}
],
Step 5: Add Theme Styles
.ocean {
--color-primary : #0891b2 ;
--color-background : #cffafe ;
--color-text : #164e63 ;
}
Using the New Theme
< div class = 'bg-white dark:bg-gray-900 retro:bg-amber-50 ocean:bg-cyan-50' >
Multi-theme content
</ div >
Advanced Theme Features
Respecting System Preferences
The theme system automatically detects the user’s system preference on first visit:
// Checks system preference if no saved theme
return window . matchMedia ( '(prefers-color-scheme: dark)' ). matches
? 'dark'
: 'light' ;
Preventing Flash of Wrong Theme
To prevent a flash of the wrong theme on page load, add this script to your <head>:
< script is:inline >
( function () {
const theme = localStorage . getItem ( 'theme' )
|| ( window . matchMedia ( '(prefers-color-scheme: dark)' ). matches ? 'dark' : 'light' );
document . documentElement . classList . add ( theme );
})();
</ script >
The is:inline directive tells Astro to include this script inline in the HTML, ensuring it runs before the page renders.
Theme-based Images
Show different images for different themes:
---
const heroImages = {
light: '/images/hero-light.jpg' ,
dark: '/images/hero-dark.jpg' ,
retro: '/images/hero-retro.jpg' ,
};
---
< picture >
< source srcset = { heroImages . dark } media = "(prefers-color-scheme: dark)" >
< img src = { heroImages . light } alt = "Hero" />
</ picture >
Or use CSS:
< div class = 'hero-image light-hero dark:hidden retro:hidden' ></ div >
< div class = 'hero-image dark-hero hidden dark:block retro:hidden' ></ div >
< div class = 'hero-image retro-hero hidden retro:block' ></ div >
Accessibility
The theme switcher follows accessibility best practices:
< button
class = 'theme-toggle'
aria-label = 'Toggle theme'
title = 'Switch between light, dark, and retro themes'
>
<!-- Icons with proper transitions -->
</ button >
Features:
aria-label describes the button’s purpose
Visual focus indicators on hover
Keyboard accessible (can be activated with Enter/Space)
Smooth transitions for reduced motion preference
Respecting Motion Preferences
To respect users who prefer reduced motion:
@media (prefers-reduced-motion: reduce) {
.theme-toggle svg {
transition : none ;
}
}
Best Practices
Provide sufficient contrast - Ensure text is readable in all themes
Test all themes - View all pages in each theme during development
Use semantic names - Name themes by their purpose, not just colors
Persist user choice - Always save the theme preference
Respect system preferences - Use system preference as default
Avoid theme flash - Load theme before page renders
Support keyboard navigation - Make theme switcher keyboard accessible
Add visual feedback - Show which theme is currently active
Consider images - Provide theme-appropriate images when needed
Document color variables - Keep a reference of theme colors
Troubleshooting
Issue: Theme flashes wrong color on page loadSolution: Add an inline script in <head> to apply theme before render:< script is:inline >
const theme = localStorage . getItem ( 'theme' ) || 'light' ;
document . documentElement . classList . add ( theme );
</ script >
Issue: Theme doesn’t persist after page transitionsSolution: Ensure you’re listening to astro:after-swap event and re-initializing.Issue: Custom theme variant not working in TailwindSolution: Make sure you’ve added the variant in tailwind.config.js and rebuilt your CSS.Issue: Icons not showing correctlySolution: Check that each icon has the correct combination of scale/rotate classes for all themes.
Related Pages
Components Learn more about the ThemeSwitcher component
Project Structure Understand where theme files are located