Home / books / building-with-larc / chapters / 11-theming-and-styling

Theming and Styling

Quick reference for theming and styling patterns in LARC applications. For detailed tutorials, see Learning LARC Chapter 15.

Overview

LARC applications use CSS custom properties (variables) for themeable styling, supporting light/dark modes, system preferences, and dynamic theme switching via the PAN bus. The pan-theme-provider component manages global theme state.

Key Concepts:
  • CSS custom properties: Runtime-modifiable variables (--color-primary)
  • Semantic tokens: Meaningful names (--color-text-primary, not --gray-900)
  • Theme provider: Component managing theme state via PAN bus
  • System preference: Respect prefers-color-scheme media query
  • Scoped themes: Component-specific styling independent of global theme

Quick Example

/* Define theme variables */
:root {
  --color-primary: #3b82f6;
  --color-text: #111827;
  --color-bg: #ffffff;
}

[data-theme="dark"] {
  --color-text: #f9fafb;
  --color-bg: #111827;
}

/* Use in components */
button {
  background: var(--color-primary);
  color: var(--color-bg);
}
<pan-theme-provider theme="auto"></pan-theme-provider>
<pan-theme-toggle></pan-theme-toggle>

Theme System Structure

Two-Tier Token System

| Tier | Purpose | Example | |------|---------|---------| | Primitives | Raw color values | --blue-500: #3b82f6 | | Semantic | Meaningful tokens | --color-primary: var(--blue-500) |

Components use semantic tokens only, never primitives. This allows theme changes without updating components.

Standard Theme Variables

:root {
  /* Colors */
  --color-primary: #3b82f6;
  --color-text-primary: #111827;
  --color-text-secondary: #6b7280;
  --color-bg-primary: #ffffff;
  --color-bg-secondary: #f9fafb;
  --color-border: #e5e7eb;
  
  /* Typography */
  --font-family-base: system-ui, sans-serif;
  --font-size-base: 1rem;
  --font-weight-normal: 400;
  --font-weight-bold: 700;
  --line-height-normal: 1.5;
  
  /* Spacing */
  --space-xs: 0.25rem;
  --space-sm: 0.5rem;
  --space-md: 1rem;
  --space-lg: 1.5rem;
  --space-xl: 2rem;
  
  /* Borders & Shadows */
  --border-radius: 0.5rem;
  --border-width: 1px;
  --shadow-sm: 0 1px 2px 0 rgba(0,0,0,0.05);
  --shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1);
  
  /* Transitions */
  --transition-fast: 150ms ease-in-out;
  --transition-base: 250ms ease-in-out;
}

Dark Mode Implementation

Manual Dark Mode Override

[data-theme="dark"] {
  --color-text-primary: #f9fafb;
  --color-text-secondary: #d1d5db;
  --color-bg-primary: #111827;
  --color-bg-secondary: #1f2937;
  --color-border: #374151;
  --shadow-sm: 0 1px 2px 0 rgba(0,0,0,0.3);
  --shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.5);
}

System Preference Detection

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --color-text-primary: #f9fafb;
    --color-bg-primary: #111827;
    /* ... other dark mode overrides */
  }
}

Theme Provider Component

See Chapter 17 for full API documentation of pan-theme-provider.

Basic Usage

<pan-theme-provider theme="auto"></pan-theme-provider>

JavaScript API

const provider = document.querySelector('pan-theme-provider');

// Set theme
provider.setTheme('dark'); // 'light', 'dark', or 'auto'

// Get current theme
const theme = provider.getTheme(); // Returns setting ('auto')
const effective = provider.getEffectiveTheme(); // Returns actual ('dark')

// Listen for changes
provider.addEventListener('theme-change', (e) => {
  console.log('Theme:', e.detail.theme);
});

PAN Bus Integration

import { bus } from '/core/pan-bus.mjs';

// Subscribe to theme changes
bus.subscribe('theme.changed', (msg) => {
  console.log('New theme:', msg.data.effective);
});

// Request theme change
bus.publish('theme.change', { theme: 'dark' });

Theme Toggle Component

<!-- Icon button (default) -->
<pan-theme-toggle></pan-theme-toggle>

<!-- Button with label -->
<pan-theme-toggle variant="button" label="Theme"></pan-theme-toggle>

<!-- Dropdown with all options -->
<pan-theme-toggle variant="dropdown"></pan-theme-toggle>

Component-Specific Theming

Shadow DOM Scoping

class BrandedCard extends HTMLElement {
  connectedCallback() {
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          --card-bg: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
          --card-text: #ffffff;
        }
        
        .card {
          background: var(--card-bg);
          color: var(--card-text);
          padding: var(--space-lg);
          border-radius: var(--border-radius);
        }
        
        /* Allow customization */
        :host([variant="flat"]) {
          --card-bg: var(--color-bg-secondary);
          --card-text: var(--color-text-primary);
        }
      </style>
      <div class="card"><slot></slot></div>
    `;
  }
}

Responsive Theming

Viewport-Based Variables

:root {
  --space-page: var(--space-md);
  --font-display: var(--font-size-2xl);
}

@media (min-width: 768px) {
  :root {
    --space-page: var(--space-xl);
    --font-display: var(--font-size-3xl);
  }
}

@media (min-width: 1024px) {
  :root {
    --space-page: var(--space-2xl);
    --font-display: 2.5rem;
  }
}

.container {
  padding: var(--space-page);
}

Smooth Theme Transitions

* {
  transition:
    background-color var(--transition-base),
    border-color var(--transition-base),
    color var(--transition-base);
}

/* Disable on page load */
.no-transitions * {
  transition: none !important;
}

/* Respect user preference */
@media (prefers-reduced-motion: reduce) {
  * {
    transition: none !important;
  }
}
// Disable transitions during initial theme apply
document.documentElement.classList.add('no-transitions');
applyTheme(theme);
requestAnimationFrame(() => {
  document.documentElement.classList.remove('no-transitions');
});

Multi-Brand Support

:root {
  --brand-primary: #667eea;
  --brand-logo: url('/logos/default.svg');
}

[data-brand="acme"] {
  --brand-primary: #10b981;
  --brand-logo: url('/logos/acme.svg');
}

[data-brand="techstart"] {
  --brand-primary: #f59e0b;
  --brand-logo: url('/logos/techstart.svg');
}

.brand-button {
  background: var(--brand-primary);
}
// Switch brands
document.documentElement.setAttribute('data-brand', 'acme');

Accessibility

Contrast Requirements

Maintain WCAG contrast ratios:

  • AA Normal text: 4.5:1 minimum
  • AA Large text: 3:1 minimum
  • AAA Normal text: 7:1 minimum
/* Good contrast */
:root {
  --color-text-primary: #111827; /* 16.3:1 on white */
  --color-bg-primary: #ffffff;
}

[data-theme="dark"] {
  --color-text-primary: #f9fafb; /* 17.5:1 on dark */
  --color-bg-primary: #111827;
}

Reduced Motion

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

High Contrast Mode

@media (prefers-contrast: high) {
  :root {
    --color-text-primary: #000000;
    --color-bg-primary: #ffffff;
    --color-border: #000000;
    --border-width: 2px;
  }
}

Screen Reader Announcements

function announceThemeChange(theme) {
  const announcement = document.createElement('div');
  announcement.setAttribute('role', 'status');
  announcement.setAttribute('aria-live', 'polite');
  announcement.className = 'sr-only';
  announcement.textContent = `Theme changed to ${theme} mode`;
  document.body.appendChild(announcement);
  
  setTimeout(() => announcement.remove(), 1000);
}

Performance Tips

| Optimization | Benefit | |--------------|---------| | Use data attributes for theme switching | Single change triggers all updates | | Avoid deep custom property nesting | Reduces lookup cost | | Transition specific properties only | Better performance than all | | Use CSS containment | Helps browser optimize rendering |

Efficient Theme Switching

// Good - single attribute change
document.documentElement.setAttribute('data-theme', 'dark');

// Bad - multiple property changes
document.documentElement.style.setProperty('--color-text', '#fff');
document.documentElement.style.setProperty('--color-bg', '#000');
// ... dozens more

Component Reference

See Chapter 17 for complete API documentation:

  • pan-theme-provider: Global theme management
  • pan-theme-toggle: Theme switcher UI control

Cross-References

  • Tutorial: Learning LARC Chapter 15 (Theming and Styling)
  • Components: Chapter 17 (pan-theme-provider, pan-theme-toggle)
  • Patterns: Appendix E (Theming Patterns)
  • Related: Chapter 19 (UI Components)

Common Issues

Issue: Theme not applying on load

Problem: Flash of unstyled content Solution: Apply theme in inline