Skip to main content

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:
// src/components/shared/ThemeSwitcher.astro:47
type Theme = 'light' | 'dark' | 'retro';
The theme type ensures type safety when working with themes.

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>

Step 4: Configure Tailwind Variant

// 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

  1. Provide sufficient contrast - Ensure text is readable in all themes
  2. Test all themes - View all pages in each theme during development
  3. Use semantic names - Name themes by their purpose, not just colors
  4. Persist user choice - Always save the theme preference
  5. Respect system preferences - Use system preference as default
  6. Avoid theme flash - Load theme before page renders
  7. Support keyboard navigation - Make theme switcher keyboard accessible
  8. Add visual feedback - Show which theme is currently active
  9. Consider images - Provide theme-appropriate images when needed
  10. 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.

Components

Learn more about the ThemeSwitcher component

Project Structure

Understand where theme files are located

Build docs developers (and LLMs) love