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.
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.
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.
You now understand the css tag deeply. Next, learn how CSS custom properties enable theming across the Shadow DOM boundary.