Skip to content

Polymer Theming — CSS Custom Properties and Design System Theming

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Polymer Theming. We cover key concepts, practical examples, and best practices to help you master this topic.

Theming provides a consistent visual identity across components. CSS custom properties naturally cross Shadow Dom boundaries, making them ideal for web component theming.

What You'll Learn

  • Design tokens with CSS custom properties
  • Light and dark theme switching
  • Theme providers and inheritance
  • Component-level theme overrides
  • Accessible color contrast

Why It Matters

A well-designed theme system ensures visual consistency and Accessibility. Users can switch themes, and components adapt without code changes.

Real-World Use

An enterprise application supporting light mode, dark mode, and high-contrast accessibility themes across a design system of 200+ components.

Theming Architecture

flowchart TD
    A[Theming] --> B[Design Tokens]
    A --> C[Theme Switching]
    A --> D[Inheritance]
    B --> E[Colors]
    B --> F[Typography]
    B --> G[Spacing]
    C --> H[Light]
    C --> I[Dark]
    D --> J[Document Level]
    D --> K[Component Override]
    style A fill:#e6f3ff,stroke:#4a90d9,stroke-width:2px

Design Token System

import { LitElement, html } from 'lit';

/* tokens.css */
/*
:root {
  --color-primary: #1a237e;
  --color-primary-light: #e8eaf6;
  --color-primary-dark: #0d1642;
  --color-on-primary: #ffffff;
  --color-secondary: #ff6f00;
  --color-surface: #ffffff;
  --color-background: #f5f5f5;
  --color-text: #212121;
  --color-text-secondary: #757575;
  --font-family: 'Roboto', sans-serif;
  --font-size-sm: 12px;
  --font-size-md: 14px;
  --font-size-lg: 16px;
  --font-size-xl: 20px;
  --font-size-h1: 32px;
  --font-size-h2: 24px;
  --spacing-xs: 4px;
  --spacing-sm: 8px;
  --spacing-md: 16px;
  --spacing-lg: 24px;
  --spacing-xl: 48px;
  --border-radius-sm: 4px;
  --border-radius-md: 8px;
  --border-radius-lg: 16px;
  --shadow-sm: 0 1px 3px rgba(0,0,0,0.12);
  --shadow-md: 0 4px 6px rgba(0,0,0,0.1);
  --shadow-lg: 0 10px 25px rgba(0,0,0,0.15);
}
*/

class ThemedCard extends LitElement {
  static styles = css`
    :host {
      display: block;
      background: var(--color-surface);
      border: 1px solid var(--border-color, #e0e0e0);
      border-radius: var(--border-radius-md);
      padding: var(--spacing-lg);
      box-shadow: var(--shadow-sm);
      font-family: var(--font-family);
      color: var(--color-text);
    }
    h2 { font-size: var(--font-size-h2); margin: 0 0 var(--spacing-sm); color: var(--color-primary); }
    p { font-size: var(--font-size-md); color: var(--color-text-secondary); margin: 0; }
    .actions { margin-top: var(--spacing-md); display: flex; gap: var(--spacing-sm); }
  `;

  render() {
    return html`
      <h2><slot name="title"></slot></h2>
      <p><slot></slot></p>
      <div class="actions"><slot name="actions"></slot></div>
    `;
  }
}
customElements.define('themed-card', ThemedCard);

Expected output: The component uses design tokens via var(). Changing :root variables updates all themed components.

Theme Switching

import { LitElement, html, css } from 'lit';

class ThemeToggle extends LitElement {
  static properties = { theme: { type: String } };

  static styles = css`
    :host { display: inline-block; }
    button { padding: 8px 16px; border: 2px solid var(--color-primary); border-radius: var(--border-radius-md); background: var(--color-surface); color: var(--color-primary); cursor: pointer; font-family: var(--font-family); }
  `;

  constructor() { super(); this.theme = 'light'; }

  _toggle() {
    this.theme = this.theme === 'light' ? 'dark' : 'light';
    document.documentElement.setAttribute('data-theme', this.theme);
    this.dispatchEvent(new CustomEvent('theme-changed', { detail: { theme: this.theme } }));
  }

  render() {
    return html`<button @click=${this._toggle}>${this.theme === 'light' ? 'Dark Mode' : 'Light Mode'}</button>`;
  }
}
customElements.define('theme-toggle', ThemeToggle);
[data-theme="dark"] {
  --color-primary: #90caf9;
  --color-primary-dark: #1565c0;
  --color-on-primary: #000000;
  --color-surface: #1e1e1e;
  --color-background: #121212;
  --color-text: #e0e0e0;
  --color-text-secondary: #a0a0a0;
  --border-color: #333333;
  --shadow-sm: 0 1px 3px rgba(0,0,0,0.3);
}

Expected output: Toggling the button switches data-theme attribute. All components using var() update immediately.

Theme Provider Component

import { LitElement, html } from 'lit';

const themes = {
  light: {
    '--color-primary': '#1a237e',
    '--color-surface': '#ffffff',
    '--color-text': '#212121',
    '--color-text-secondary': '#757575',
    '--border-color': '#e0e0e0'
  },
  dark: {
    '--color-primary': '#90caf9',
    '--color-surface': '#1e1e1e',
    '--color-text': '#e0e0e0',
    '--color-text-secondary': '#a0a0a0',
    '--border-color': '#333333'
  },
  highContrast: {
    '--color-primary': '#0000ff',
    '--color-surface': '#ffffff',
    '--color-text': '#000000',
    '--color-text-secondary': '#000000',
    '--border-color': '#000000'
  }
};

class ThemeProvider extends LitElement {
  static properties = { theme: { type: String } };

  constructor() {
    super();
    this.theme = 'light';
  }

  get _tokens() {
    const t = themes[this.theme] || themes.light;
    return Object.entries(t).map(([key, val]) => `${key}: ${val}`).join(';');
  }

  updated(changedProperties) {
    if (changedProperties.has('theme')) {
      this.dispatchEvent(new CustomEvent('theme-changed', {
        detail: { theme: this.theme },
        bubbles: true, composed: true
      }));
    }
  }

  render() {
    return html`
      <div style="${this._tokens}">
        <slot></slot>
      </div>
    `;
  }
}
customElements.define('theme-provider', ThemeProvider);

Expected output: ThemeProvider sets tokens on its wrapper div. Descendants inherit token values through CSS custom properties.

Component-Level Override

import { LitElement, html, css } from 'lit';

class AlertBanner extends LitElement {
  static styles = css`
    :host {
      display: block;
      --alert-bg: var(--color-surface);
      --alert-border: var(--color-primary);
      --alert-text: var(--color-text);
    }
    :host([type="success"]) { --alert-bg: #e8f5e9; --alert-border: #2e7d32; --alert-text: #1b5e20; }
    :host([type="warning"]) { --alert-bg: #fff3e0; --alert-border: #e65100; --alert-text: #bf360c; }
    :host([type="error"]) { --alert-bg: #ffebee; --alert-border: #c62828; --alert-text: #b71c1c; }
    .alert {
      background: var(--alert-bg);
      border-left: 4px solid var(--alert-border);
      color: var(--alert-text);
      padding: var(--spacing-md, 12px);
      border-radius: var(--border-radius-sm, 4px);
    }
  `;

  static properties = { type: { type: String } };
  constructor() { super(); this.type = 'info'; }

  render() { return html`<div class="alert"><slot></slot></div>`; }
}
customElements.define('alert-banner', AlertBanner);

Expected output: Component-level CSS variables override theme defaults. :host([type]) selectors change for success/warning/error.

Accessible Color Contrast

import { LitElement, html, css } from 'lit';

function hexToRgb(hex) {
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  return result ? {
    r: parseInt(result[1], 16),
    g: parseInt(result[2], 16),
    b: parseInt(result[3], 16)
  } : { r: 0, g: 0, b: 0 };
}

function getLuminance(r, g, b) {
  const [rs, gs, bs] = [r, g, b].map(c => {
    c = c / 255;
    return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
  });
  return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
}

class ContrastSafeText extends LitElement {
  static properties = { bgColor: { type: String }, text: { type: String } };

  get _textColor() {
    const bg = hexToRgb(this.bgColor || '#ffffff');
    const lum = getLuminance(bg.r, bg.g, bg.b);
    return lum > 0.5 ? '#000000' : '#ffffff';
  }

  render() {
    return html`
      <div style="background: ${this.bgColor}; color: ${this._textColor}; padding: 8px; border-radius: 4px;">
        ${this.text}
      </div>
    `;
  }
}
customElements.define('contrast-safe-text', ContrastSafeText);

Expected output: Text color dynamically selects black or white based on background luminance for WCAG contrast.

Common Mistakes

  1. Hardcoding colors in components - Always use var() for themeable properties.

  2. Not providing fallback values - Use var(--name, fallback) for compatibility.

  3. Forgetting theme tokens on nested providers - Use inheritance via :root or document-level tokens.

  4. Ignoring focus ring contrast - Ensure focus outlines have sufficient contrast.

  5. Not testing with multiple themes - Verify all states in light, dark, and high-contrast.

Practice Questions

  1. How do CSS custom properties enable theming across shadow boundaries?
  2. How do you implement a light/dark theme toggle?
  3. How do you override a theme token at the component level?
  4. How do you ensure accessible color contrast in themes?
  5. What is the purpose of fallback values in var()?

Challenge: Build a complete design system theme with: full token set (color, typography, spacing, shadow), light/dark/high-contrast themes, theme provider component, per-component overrides, theme persistence (localStorage), and a theme inspector panel.

FAQ

Can I use CSS-in-JS with LitElement for theming?

Yes, but CSS custom properties are the recommended approach. They cascade naturally through shadow DOM.

How do I handle theme transitions?

Use CSS transition on var() properties. Components without transitions animate smoothly.

Can themes be nested?

Yes. A child theme provider overrides tokens for its subtree.

How do I test theme components?

Set document-level CSS variables, mount component, assert computed styles with getComputedStyle.

Mini Project

Build a themed design system with: button, card, input, badge, and dialog components using CSS custom properties, light/dark/high-contrast themes, theme provider, theme toggle with persistence, and a theme playground for live token editing.

What's Next

Theming provides consistent design. Learn how Polymer Testing ensures component quality with @open-wc/testing.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro