Skip to main content
Lit components use Shadow DOM to scope styles — styles defined inside a component don’t affect the outside document, and external styles can’t accidentally override component internals. Lit provides the css tagged template literal to define these styles in a way that is safe, performant, and composable.

The css tagged template literal

import { css } from 'lit';

const styles = css`
  :host {
    display: block;
    color: var(--my-element-color, #333);
  }

  p {
    margin: 0;
    font-size: 1rem;
  }
`;
css returns a CSSResult object. Under the hood, Lit uses the CSSResult to construct a CSSStyleSheet (via the Constructable Stylesheets API) and applies it with shadowRoot.adoptedStyleSheets. This means a single CSSStyleSheet object is shared across all instances of a component — no per-instance style duplication.

Applying styles with static styles

Assign a CSSResult or an array of them to static styles:
import { LitElement, html, css } from 'lit';
import { customElement } from 'lit/decorators.js';

@customElement('my-card')
class MyCard extends LitElement {
  static styles = css`
    :host {
      display: block;
      border: 1px solid #e0e0e0;
      border-radius: 4px;
      padding: 1rem;
    }

    h2 {
      margin-top: 0;
    }
  `;

  render() {
    return html`
      <h2><slot name="title"></slot></h2>
      <slot></slot>
    `;
  }
}
static styles can accept:
  • A single CSSResult: static styles = css…“
  • An array of CSSResult values: static styles = [baseStyles, css]
  • Native CSSStyleSheet objects (in browsers that support them)
Duplicate stylesheet entries are automatically deduplicated — the last occurrence in the array wins.

Shadow DOM style scoping

Styles defined in static styles apply only inside the component’s shadow root. Selectors like p, div, or .my-class are scoped and will not match elements outside the shadow boundary.
static styles = css`
  /* Only matches <p> inside this component's shadow root */
  p {
    color: blue;
  }
`;
External global stylesheets don’t penetrate the shadow boundary. Only CSS custom properties, ::part(), and ::slotted() can cross it.

The :host selector

Target the component element itself (the custom element host) with :host:
static styles = css`
  :host {
    display: block;       /* Custom elements are inline by default */
    box-sizing: border-box;
  }

  /* Conditional styling based on an attribute */
  :host([hidden]) {
    display: none;
  }

  /* Context-dependent styling */
  :host([variant='primary']) {
    background: #344cfc;
    color: white;
  }
`;
:host([attr]) lets you apply styles based on reflected properties — a common pattern for public API flags like disabled, hidden, or variant.

:host-context()

Match based on an ancestor outside the shadow root (limited browser support):
static styles = css`
  :host-context(.dark-theme) {
    background: #1a1a1a;
    color: white;
  }
`;

CSS custom properties for theming

CSS custom properties (variables) cross the shadow boundary, making them the primary mechanism for theming Lit components:
// Component definition
static styles = css`
  :host {
    --button-bg: #344cfc;
    --button-color: white;
    --button-radius: 4px;
  }

  button {
    background: var(--button-bg);
    color: var(--button-color);
    border-radius: var(--button-radius);
    border: none;
    padding: 0.5rem 1rem;
    cursor: pointer;
  }
`;
Consumers override these variables from outside:
/* Global CSS */
my-button {
  --button-bg: #e53e3e;
  --button-radius: 9999px;
}
The var(--name, fallback) syntax lets you provide a default value used when the variable is not set.

CSS parts (::part())

Expose internal elements for external styling using the part attribute:
render() {
  return html`
    <div part="container">
      <span part="label">${this.label}</span>
      <button part="button" @click=${this._click}>Go</button>
    </div>
  `;
}
Consumers can then style named parts from outside the shadow root:
my-element::part(button) {
  background: green;
  font-weight: bold;
}

my-element::part(label) {
  font-style: italic;
}

Sharing styles across components

Extract shared styles into a module and import them:
// shared-styles.ts
import { css } from 'lit';

export const buttonReset = css`
  button {
    all: unset;
    cursor: pointer;
  }
`;

export const typography = css`
  :host {
    font-family: system-ui, sans-serif;
    line-height: 1.5;
  }
`;
// my-element.ts
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
import { buttonReset, typography } from './shared-styles.js';

@customElement('my-element')
class MyElement extends LitElement {
  static styles = [typography, buttonReset, css`
    /* Component-specific styles */
    :host { padding: 1rem; }
  `];

  render() {
    return html`<button>Click</button>`;
  }
}
Because CSSStyleSheet objects are shared, the same stylesheet instance is reused across all components that include it — no duplication in memory.

adoptedStyleSheets for performance

Lit automatically uses adoptedStyleSheets when the browser supports it:
// From css-tag.ts
export const supportsAdoptingStyleSheets: boolean =
  global.ShadowRoot &&
  (global.ShadyCSS === undefined || global.ShadyCSS.nativeShadow) &&
  'adoptedStyleSheets' in Document.prototype &&
  'replace' in CSSStyleSheet.prototype;
When adoptedStyleSheets is not available (older browsers), Lit falls back to inserting <style> elements into the shadow root.
To use a nonce for Content Security Policy (CSP) compliance with the <style> fallback, set window.litNonce to a server-generated nonce before loading your components:
<script>window.litNonce = 'server-generated-nonce';</script>

Using unsafeCSS

To include CSS from a non-literal source (e.g. a runtime value), use unsafeCSS:
import { unsafeCSS } from 'lit';

const brandColor = getComputedStyle(document.documentElement)
  .getPropertyValue('--brand-color');

const styles = css`
  :host {
    color: ${unsafeCSS(brandColor)};
  }
`;
unsafeCSS bypasses the security check that prevents arbitrary strings from being used as CSS. Only use it with trusted values — never with user-supplied input.

Build docs developers (and LLMs) love