Skip to content

Polymer Styling — Component-Scoped Styling with CSS Custom Properties

DodaTech Updated 2026-06-28 5 min read

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

  1. Assuming global styles pierce shadow - They do not. Use CSS custom properties.

  2. Using @import in Shadow DOM - Not supported. Use link or adoptedStyleSheets.

  3. Inline styles for dynamic values - Use style maps or CSS properties.

  4. Overusing :host-context - Creates tight coupling to parent DOM structure.

  5. Forgetting to define CSS custom property fallbacks - Use var(--name, fallback).

Practice Questions

  1. How do CSS custom properties cross the shadow boundary?
  2. What does the :host selector target in Shadow DOM?
  3. How do you pass dynamic style values to a component?
  4. How do media queries work inside Shadow DOM?
  5. 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

Can I use CSS variables inside Shadow DOM?

Yes. CSS custom properties inherit through the shadow boundary and can have fallbacks.

How do I style a component based on its parent's class?

Use :host-context(parent-class) to style based on ancestor.

What is the difference between css tagged template and style elements?

css`` is parsed once and shared. Inline styles re-parse per instance.

Can I use CSS modules with LitElement?

Yes. Import CSS module files and use adoptedStyleSheets or unsafeCSS.

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