Skip to content

The css Tag

The css tagged template function is how you write styles for static styles. This lesson explains why it exists, what it protects against, and how to compose multiple stylesheets.

Why css Is a Tagged Template, Not a Plain String

You might expect static styles to just accept a plain string of CSS, but Lit requires the css tagged template function specifically. This exists as a security measure: css marks its result as a trusted CSSResult, and by default only allows interpolating other CSSResult values (not arbitrary strings) into a style block, preventing attacker-controlled strings from being injected as CSS (which could otherwise be used for CSS-based data exfiltration attacks).

In practice, this means your styles are almost always fully static, or composed from other trusted css-tagged values — which is exactly the pattern for sharing common styles across multiple components.

import { css } from 'lit';

const spacing = css`8px`; // a trusted CSSResult, not a plain string

static styles = css`
  .card {
    padding: ${spacing};
    border-radius: 4px;
  }
`;

Because spacing itself came from css`...`, Lit trusts it and allows the interpolation — a plain string '8px' would be rejected at runtime by default.

Composing Shared Styles

// shared-styles.js
export const resetStyles = css`
  * { box-sizing: border-box; margin: 0; }
`;

// my-component.js
import { resetStyles } from './shared-styles.js';

static styles = [
  resetStyles,
  css`:host { display: block; }`,
];
  • Export reusable css results from a shared module and import them into multiple components' static styles arrays.
  • static styles accepts either a single CSSResult or an array of them — Lit combines the array automatically.
  • Only interpolate trusted CSSResult values (from css or unsafeCSS, used carefully) into a css template — never raw external strings.
  • For a genuinely dynamic, user-controlled value that must be CSS, use the unsafeCSS() escape hatch deliberately and only with trusted input.

css Tag Cheat Sheet

Rules and patterns for working with the css tagged template.

Pattern Valid?
`css`.card { color: red; }` Yes — fully static CSS
`css`.card { padding: \${otherCssResult}; }` Yes — interpolating a trusted CSSResult
`css`.card { color: \${userInputString}; }` No — throws at runtime by default
`css`.card { color: \${unsafeCSS(trustedValue)}; }` Yes, but bypasses the safety check — use with care
static styles = [styleA, styleB] Yes — arrays of CSSResult are combined automatically

Sharing Design Tokens Across Components

A common real-world pattern is a small shared module exporting reusable css fragments — spacing scales, typography, resets — that every component in a design system imports, keeping visual consistency without duplicating raw values everywhere.

// tokens.js
export const colorPrimary = css`#3b82f6`;
export const radiusMd = css`8px`;

// button.js
import { colorPrimary, radiusMd } from './tokens.js';

static styles = css`
  button {
    background: ${colorPrimary};
    border-radius: ${radiusMd};
  }
`;

The unsafeCSS() Escape Hatch

For rare cases where you genuinely need to build CSS from a plain string at runtime (e.g. a value coming from a trusted internal configuration, never from user input), unsafeCSS() wraps a string as a CSSResult, bypassing the safety check. Treat it exactly like innerHTML — powerful, but a real injection risk with untrusted input.

import { css, unsafeCSS } from 'lit';

const trustedFontStack = 'system-ui, sans-serif'; // from your own config, not user input
static styles = css`
  :host { font-family: ${unsafeCSS(trustedFontStack)}; }
`;

Never pass genuinely user-controlled or untrusted data through unsafeCSS() — that defeats the protection Lit's css tag provides by default.

Common Mistakes

  • Trying to pass a plain JavaScript string directly into static styles instead of wrapping it with css.
  • Interpolating an untrusted, user-controlled string into a css template via unsafeCSS(), reopening the exact injection risk Lit protects against by default.
  • Duplicating the same raw color/spacing values across many components instead of sharing them from a common tokens module.
  • Forgetting static styles can be an array, and instead manually concatenating multiple css results into one large template.

Key Takeaways

  • css is a tagged template function, not a plain string, specifically to prevent CSS injection by default.
  • Only trusted CSSResult values (from css or deliberate unsafeCSS) can be interpolated into a css template.
  • static styles accepts an array, making it easy to share common design tokens/resets across components.
  • unsafeCSS() exists for rare trusted-string cases, but should never be used with untrusted or user-controlled input.

Pro Tip

Set up a small shared tokens.js module as soon as you have more than two or three components in a project — retrofitting shared design tokens later is far more work than starting with them from the very first component.