Skip to main content
Version: v5 (Preview)

Styling Components

Choosing an Encapsulation Strategy​

Styling your components depends partly on their encapsulation choice - shadow, scoped, or none.

my-component.tsx
@Component({
tag: 'my-component',
styleUrl: 'my-component.css',
encapsulation: { type: 'shadow' }, // or { type: 'scoped' }, or omit entirely for 'none'
})
export class MyComponent {}

Here, three components share one stylesheet and differ only in encapsulation. The page's h2 rule turns the scoped and none headings red, but not the shadow one. The none component's h2 rule leaks out and underlines the page's own heading:

demo.css
h2 {
text-decoration: underline;
}
shadow-demo.tsx
import { Component } from '@stencil/core';

@Component({
tag: 'shadow-demo',
styleUrl: 'demo.css',
encapsulation: { type: 'shadow' },
})
export class ShadowDemo {
render() {
return <h2>shadow</h2>;
}
}
index.html
<style>
h2 {
color: red;
}
</style>

<script type="module" src="./shadow-demo.js"></script>
<script type="module" src="./scoped-demo.js"></script>
<script type="module" src="./none-demo.js"></script>

<h2>Page heading</h2>
<shadow-demo></shadow-demo>
<scoped-demo></scoped-demo>
<none-demo></none-demo>
shadowscopednone
Style isolationFull - styles can't leak in or outStencil-applied - a generated class scopes your CSS, but light-DOM styles can still reach inNone by default - pair with native @scope for real scoping
DOM locationShadow root (unreachable from document.querySelector() or external CSS)Light DOM (reachable with document.querySelector() and ordinary CSS)Light DOM
<slot>Native browser slot projectionStencil-managed light-DOM slotting (see Slots Without Shadow DOM)Same as scoped
Styling parts from outside::part(), piercing the shadow boundaryA plain attribute selector (e.g. [part="heading"]) - there's no boundary for ::part() to pierceSame as scoped
Global stylesheets reach in?Only via :host() in a stylesheet registered as a constructable stylesheetYes, like any other elementYes, like any other element

Reach for shadow when you want the browser to guarantee isolation for you - publishing a design system used across teams you don't control, or embedding a component into a page whose CSS you don't trust. Reach for scoped or none when you want your component's DOM to behave like an ordinary part of the page.

shadow​

Full isolation, guaranteed by the browser - not just Stencil. The one addition to your CSS is :host, which selects the host element itself. Enabled by default for components made with stencil generate.

scoped​

Stencil generates a unique class for your component (e.g. sc-my-component), rewrites your stylesheet's selectors to include it, and adds that same class to the component's elements at render time - you write ordinary CSS, Stencil does the scoping. One-directional: page styles can still reach in, even though yours can't leak out.

none​

No scoping at all, and the default. Reach for it when a component should inherit and participate in page styles on purpose (a typography or layout primitive), or when you want to add your own scoping - see Real Scoping With @scope below.

Styling the Host Element​

The host element is your component's own tag, such as <my-cmp>. Which selector styles it from the component's stylesheet depends on the encapsulation:

EncapsulationSelector
shadow:host
scoped:host (Stencil rewrites it to the host's scope class)
nonethe tag name, my-cmp
my-cmp.css
:host {
display: block;
}

In a scoped component, the tag name doesn't select the host: Stencil scopes it like any other selector, so it only matches my-cmp elements rendered inside the component.

Real Scoping With @scope​

type: 'none' doesn't scope your styles at all, so if you want scoping without type: 'scoped''s generated class, write it yourself with the native @scope at-rule - no Stencil mechanism involved, just the browser doing what @scope was designed for:

plain-component.css
@scope (plain-component) {
:scope {
display: block;
}

h1 {
color: blue;
}
}

Everything inside the @scope block only matches within a <plain-component> element's subtree - h1 here won't touch any other h1 on the page. This is the evergreen alternative to type: 'scoped': real scoping, native to the platform, with none of Stencil's own generated-class scoping mechanism involved.

note

@scope reached Baseline "Newly available" as of Firefox 146 (December 2025) - it's also supported in Chrome 118+, Safari 17.4+, and Edge 118+. Choose type: 'scoped' instead if you need to support browsers from before this landed.

More on Styling​

  • Defining a Styling API: CSS custom properties and parts, the styles consumers are allowed to change.
  • Global Styles: document-level stylesheets, co-locating them with components, and preventing a flash of unstyled content.
  • Style Modes: alternative stylesheets for one component, such as iOS and Material Design.