AstroNova
Technical 6 min read

Dynamic Giscus Theme Switching: How Our Astro Blog Adapts to User Preferences

A deep dive into implementing seamless giscus comment system theme switching based on user-selected color schemes, with automatic detection and real-time updates.

Dynamic Giscus Theme Switching: How Our Astro Blog Adapts to User Preferences

In modern web development, providing users with theme preferences has become essential for accessibility and user experience. Our Astro blog implements a sophisticated giscus comment system that automatically adapts to the user’s selected theme, whether it’s light, dark, or system preference. This article explores the technical implementation behind this seamless theme switching mechanism.

The Challenge: Synchronizing Giscus with Site Themes

Giscus, being an embedded comment system, operates within an iframe that loads independently from the main page. This isolation creates a challenge: how do we ensure the comment section matches the site’s current theme without requiring page reloads or manual user intervention?

The solution involves three key components:

  1. Theme Detection: Identifying the current theme preference
  2. Dynamic Loading: Initializing giscus with the correct theme
  3. Real-time Updates: Responding to theme changes without page reload

Theme Detection Architecture

Our theme detection system follows a hierarchical approach, checking preferences in order of user intent:

// Theme detection logic from Comments.astro
function getCurrentTheme() {
  // Priority 1: User's explicit preference stored in localStorage
  const savedTheme = localStorage.getItem('theme');
  if (savedTheme) {
    return savedTheme;
  }
  
  // Priority 2: Current HTML class indicating active theme
  if (document.documentElement.classList.contains('dark')) {
    return 'dark';
  }
  
  // Priority 3: System preference as fallback
  if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
    return 'dark';
  }
  
  return 'light'; // Default
}

Giscus Theme Architecture Diagram

This approach respects user autonomy while providing sensible defaults based on system preferences.

Initial Giscus Integration

The giscus integration begins with a comprehensive configuration object that includes theme settings:

// Giscus configuration structure
const giscusConfig = {
  repo: siteConfig.giscus.repo,
  repoId: siteConfig.giscus.repoId,
  category: siteConfig.giscus.category,
  categoryId: siteConfig.giscus.categoryId,
  mapping: siteConfig.giscus.mapping,
  reactionsEnabled: siteConfig.giscus.reactionsEnabled,
  emitMetadata: siteConfig.giscus.emitMetadata,
  inputPosition: 'top',
  theme: siteConfig.giscus.theme, // Initial theme
  lang: 'en',
  loading: 'lazy',
};

Dynamic Script Loading

Rather than statically including the giscus script, we dynamically create and inject it based on the detected theme:

function loadGiscus(theme) {
  const script = document.createElement('script');
  script.src = 'https://giscus.app/client.js';
  
  // Configure giscus attributes
  script.setAttribute('data-repo', giscusConfig.repo);
  script.setAttribute('data-repo-id', giscusConfig.repoId);
  script.setAttribute('data-category', giscusConfig.category);
  script.setAttribute('data-category-id', giscusConfig.categoryId);
  script.setAttribute('data-mapping', giscusConfig.mapping);
  script.setAttribute('data-reactions-enabled', giscusConfig.reactionsEnabled);
  script.setAttribute('data-emit-metadata', giscusConfig.emitMetadata);
  script.setAttribute('data-input-position', giscusConfig.inputPosition);
  script.setAttribute('data-theme', theme); // Dynamic theme
  script.setAttribute('data-lang', giscusConfig.lang);
  script.setAttribute('data-loading', giscusConfig.loading);
  
  script.crossOrigin = 'anonymous';
  script.async = true;
  
  const container = document.getElementById('giscus-container');
  if (container) {
    container.appendChild(script);
  }
}

Real-time Theme Updates

The most sophisticated aspect is handling theme changes after initial load. Since giscus loads in an iframe, we use the postMessage API to communicate theme changes:

function updateGiscusTheme() {
  const iframe = document.querySelector('.giscus-frame');
  if (iframe && iframe.contentWindow) {
    const theme = getCurrentTheme();
    iframe.contentWindow.postMessage(
      { giscus: { setConfig: { theme } } }, 
      'https://giscus.app'
    );
  }
}

Multi-layered Event Listening

To ensure theme changes are captured from any source, we implement multiple event listeners:

function setupThemeObserver() {
  // System theme changes (when user changes OS preference)
  window.matchMedia('(prefers-color-scheme: dark)')
        .addEventListener('change', updateGiscusTheme);
  
  // Manual theme toggle via class changes
  const observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      if (mutation.attributeName === 'class') {
        updateGiscusTheme();
      }
    });
  });
  
  observer.observe(document.documentElement, {
    attributes: true,
    attributeFilter: ['class']
  });
  
  // Cross-tab theme synchronization
  window.addEventListener('storage', (e) => {
    if (e.key === 'theme') {
      updateGiscusTheme();
    }
  });
}

Integration with Theme Toggle Component

The theme detection system works seamlessly with our React-based theme toggle component:

// ThemeToggle.tsx integration
const toggleTheme = () => {
  const newTheme = theme === 'light' ? 'dark' : 'light';
  setTheme(newTheme);
  localStorage.setItem('theme', newTheme);
  
  if (newTheme === 'dark') {
    document.documentElement.classList.add('dark');
  } else {
    document.documentElement.classList.remove('dark');
  }
  
  // This triggers the MutationObserver, which updates giscus
};

Initial Theme Application

To prevent flash of unstyled content, we apply the theme as early as possible using an inline script in ThemeScript.astro:

// ThemeScript.astro - Applied before hydration
const theme = (() => {
  if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
    return localStorage.getItem('theme');
  }
  if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
    return 'dark';
  }
  return 'light';
})();

if (theme === 'light') {
  document.documentElement.classList.remove('dark');
} else {
  document.documentElement.classList.add('dark');
}

window.localStorage.setItem('theme', theme);

Performance Considerations

The implementation includes several performance optimizations:

  1. Lazy Loading: Giscus loads asynchronously with loading="lazy"
  2. Single Initialization: Checks prevent duplicate script loading
  3. Efficient Observers: MutationObserver targets only necessary DOM changes
  4. Debounced Updates: Theme changes are batched to prevent excessive iframe updates

Accessibility Features

The theme switching system includes accessibility considerations:

  • Keyboard Navigation: Theme toggle is keyboard accessible
  • Screen Reader Support: Proper ARIA labels and announcements
  • Reduced Motion: Respects prefers-reduced-motion media query
  • High Contrast: Maintains WCAG compliance across themes

Testing and Validation

The implementation has been tested across:

  • Browser Compatibility: Chrome, Firefox, Safari, Edge
  • Device Testing: Desktop, tablet, and mobile devices
  • Theme Persistence: Survives page refreshes and navigation
  • Cross-tab Synchronization: Theme changes sync across browser tabs
  • System Preference Changes: Responds to OS-level theme switches

Configuration Flexibility

The system is designed to be configurable through site configuration:

// site.ts configuration
export const siteConfig = {
  giscus: {
    repo: 'username/repository',
    repoId: 'your-repo-id',
    category: 'General',
    categoryId: 'your-category-id',
    mapping: 'pathname',
    reactionsEnabled: '1',
    emitMetadata: '0',
    theme: 'preferred_color_scheme', // Fallback theme
  }
};

Future Enhancements

The architecture supports future enhancements including:

  • Custom Theme Colors: Allowing users to define custom accent colors
  • High Contrast Mode: Additional accessibility theme option
  • Automatic Theme Scheduling: Time-based theme switching
  • Per-user Preferences: Storing preferences server-side for logged-in users

Conclusion

This giscus theme switching implementation demonstrates how careful attention to user preferences, accessibility, and performance can create a seamless commenting experience. By leveraging browser APIs, efficient event handling, and thoughtful architecture, we’ve created a system that respects user choice while maintaining excellent performance across all devices and scenarios.

The solution bridges the gap between embedded third-party content and site-wide theming, proving that even complex integrations can feel native and responsive to user preferences.


The complete implementation can be found in the Comments.astro component and related theme configuration files.

Comments