Polymer Styling — Component-Scoped Styling with CSS Custom Properties
In this tutorial, you will learn about Polymer Styling. We cover key concepts, practical examples, and best practices to help you master this topic.
Shadow Dom provides built-in style Encapsulation, but components also need theming via CSS custom properties and dynamic styling through LitElement's reactive system.
What You'll Learn
- CSS custom properties for theming
- :host and :host-context selectors
- Dynamic styling with style maps
- Shared styles with adoptedStyleSheets
- Responsive component patterns
Why It Matters
Encapsulated styles prevent conflicts, and CSS custom properties let users theme components without breaking shadow boundaries.
Real-World Use
A design system with themed components — users set --primary-color and --font-size on the document, and all components respond.
Styling Architecture
flowchart TD
A[Styling] --> B[Encapsulation]
A --> C[Theming]
A --> D[Dynamic Styles]
B --> E[Shadow DOM]
B --> F[:host]
C --> G[Custom Properties]
C --> H[:host-context]
D --> I[style Maps]
D --> J[Class Toggles]
style A fill:#e6f3ff,stroke:#4a90d9,stroke-width:2px
CSS Custom Properties
import { LitElement, html, css } from 'lit';
class ThemedButton extends LitElement {
static styles = css`
:host {
--btn-bg: var(--primary-color, #1a237e);
--btn-text: var(--on-primary, white);
--btn-radius: var(--border-radius, 4px);
}
button {
background: var(--btn-bg);
color: var(--btn-text);
border: none;
border-radius: var(--btn-radius);
padding: 8px 16px;
cursor: pointer;
font-size: var(--font-size, 14px);
transition: opacity 0.2s;
}
button:hover { opacity: 0.9; }
button:disabled { opacity: 0.5; cursor: not-allowed; }
`;
static properties = { disabled: { type: Boolean } };
render() {
return html`<button ?disabled=${this.disabled}><slot></slot></button>`;
}
}
customElements.define('themed-button', ThemedButton);
/* Global CSS */
:root {
--primary-color: #ff6f00;
--on-primary: #ffffff;
--border-radius: 8px;
--font-size: 16px;
}
Expected output: The button uses custom properties from the document. Properties cascade through the shadow boundary.
:host Selector
import { LitElement, html, css } from 'lit';
class Card extends LitElement {
static styles = css`
:host {
display: block;
border: 1px solid var(--card-border, #e0e0e0);
border-radius: var(--card-radius, 8px);
padding: 16px;
background: var(--card-bg, white);
box-shadow: var(--card-shadow, 0 1px 3px rgba(0,0,0,0.1));
}
:host(.primary) {
border-color: var(--primary-color, #1a237e);
background: var(--primary-light, #e8eaf6);
}
:host([hidden]) { display: none; }
:host(.loading) { opacity: 0.6; pointer-events: none; }
:host(:hover) {
box-shadow: var(--card-shadow-hover, 0 4px 12px rgba(0,0,0,0.15));
}
`;
render() {
return html`<div class="content"><slot></slot></div>`;
}
}
customElements.define('my-card', Card);
Expected output: :host styles the component itself. :host(.primary) applies when the component has the primary class. :host([hidden]) respects the hidden attribute.
Dynamic Styling
import { LitElement, html, css } from 'lit';
class DynamicBadge extends LitElement {
static styles = css`
:host { display: inline-block; }
.badge {
display: inline-flex;
align-items: center;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
`;
static properties = {
variant: { type: String },
size: { type: String },
customColor: { type: String }
};
get _styles() {
const variants = {
success: { bg: '#e8f5e9', color: '#2e7d32' },
warning: { bg: '#fff3e0', color: '#e65100' },
error: { bg: '#ffebee', color: '#c62828' },
info: { bg: '#e3f2fd', color: '#1565c0' }
};
const v = variants[this.variant] || variants.info;
const sizes = { sm: '10px', md: '12px', lg: '14px' };
const fontSize = sizes[this.size] || sizes.md;
return {
backgroundColor: this.customColor || v.bg,
color: v.color,
fontSize
};
}
render() {
return html`<span class="badge" style=${this._styles}><slot></slot></span>`;
}
}
customElements.define('dynamic-badge', DynamicBadge);
<dynamic-badge variant="success" size="lg">Verified</dynamic-badge>
Expected output: Style object returned from getter is applied to the element. Variant, size, and customColor control appearance.
Theming with Context
import { LitElement, html, css } from 'lit';
class ThemeProvider extends LitElement {
static properties = { theme: { type: String } };
static styles = css`
:host { display: contents; }
`;
constructor() { super(); this.theme = 'light'; }
render() {
return html`
<div class="theme-${this.theme}" style="
--bg: ${this.theme === 'dark' ? '#333' : '#fff'};
--text: ${this.theme === 'dark' ? '#eee' : '#333'};
--primary: ${this.theme === 'dark' ? '#90caf9' : '#1a237e'};
">
<slot></slot>
</div>
`;
}
}
customElements.define('theme-provider', ThemeProvider);
class ThemedPanel extends LitElement {
static styles = css`
:host {
display: block;
background: var(--bg, white);
color: var(--text, #333);
padding: 16px;
border-radius: 8px;
}
h2 { color: var(--primary, #1a237e); }
`;
render() {
return html`<h2><slot name="title"></slot></h2><slot></slot>`;
}
}
customElements.define('themed-panel', ThemedPanel);
Expected output: ThemeProvider sets CSS custom properties on its wrapper div. Child components inherit them automatically.
Media Queries and Responsive Styling
import { LitElement, html, css } from 'lit';
class ResponsiveGrid extends LitElement {
static styles = css`
:host { display: block; }
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 16px;
padding: 16px;
}
@media (max-width: 600px) {
.grid { grid-template-columns: 1fr; }
}
@media (min-width: 601px) and (max-width: 1024px) {
.grid { grid-template-columns: repeat(2, 1fr); }
}
`;
render() {
return html`<div class="grid"><slot></slot></div>`;
}
}
customElements.define('responsive-grid', ResponsiveGrid);
Expected output: CSS media queries inside Shadow DOM use the component's container width. The grid adjusts columns based on viewport.
Common Mistakes
Assuming global styles pierce shadow - They do not. Use CSS custom properties.
Using @import in Shadow DOM - Not supported. Use link or adoptedStyleSheets.
Inline styles for dynamic values - Use style maps or CSS properties.
Overusing :host-context - Creates tight coupling to parent DOM structure.
Forgetting to define CSS custom property fallbacks - Use var(--name, fallback).
Practice Questions
- How do CSS custom properties cross the shadow boundary?
- What does the :host selector target in Shadow DOM?
- How do you pass dynamic style values to a component?
- How do media queries work inside Shadow DOM?
- How do you implement theme switching with CSS custom properties?
Challenge: Build a component library with full theme support (light/dark/high-contrast), dynamic sizing (sm/md/lg), responsive layout, CSS custom property API, and theme toggle that updates all components.
FAQ
Mini Project
Build a themed form component set with: CSS custom property API for colors/sizes/spacing, light/dark theme, responsive layout, dynamic variant styling (primary/success/error), and document-level theme switching.
What's Next
Styling makes components presentable. Learn how LitElement Basics combines everything into a modern component base class.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro