Polymer Theming — CSS Custom Properties and Design System Theming
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
Hardcoding colors in components - Always use var() for themeable properties.
Not providing fallback values - Use var(--name, fallback) for compatibility.
Forgetting theme tokens on nested providers - Use inheritance via :root or document-level tokens.
Ignoring focus ring contrast - Ensure focus outlines have sufficient contrast.
Not testing with multiple themes - Verify all states in light, dark, and high-contrast.
Practice Questions
- How do CSS custom properties enable theming across shadow boundaries?
- How do you implement a light/dark theme toggle?
- How do you override a theme token at the component level?
- How do you ensure accessible color contrast in themes?
- 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
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