Stencil.js Guide
Quick Start
Stencil offers three project starters: component (component library), app (full application), and ionic-pwa (Ionic PWA app). Component library is the most common choice.
Component Basics
Key concepts: Stencil components compile to standard Custom Elements with zero runtime dependency. Written in TypeScript + JSX, but output standard browser code. The @Component tag must contain a hyphen (W3C spec requirement).
Decorators Reference
| Decorator | Purpose | Example |
|---|---|---|
@Component | Define tag name, style file, and Shadow DOM mode | @Component({ tag: 'my-cmp', shadow: true }) |
@Prop() | Public property, settable externally, triggers re-render | @Prop() name: string |
@Prop({ mutable }) | Prop that can be mutated internally | @Prop({ mutable: true }) open: boolean |
@Prop({ reflect }) | Sync value back to HTML attribute (for CSS selectors) | @Prop({ reflect: true }) color: string |
@State() | Internal reactive state, triggers re-render on change | @State() items: string[] = [] |
@Watch() | Callback when a Prop or State changes | @Watch('value') onValueChange(n, o) {} |
@Event() | Declare a custom DOM event emitter | @Event() myEvent: EventEmitter<T> |
@Listen() | Listen to DOM events (including window/document level) | @Listen('resize', { target: 'window' }) |
@Method() | Expose public async method, callable via JS | @Method() async open() {} |
@Element() | Get reference to the host HTMLElement | @Element() el: HTMLElement |
Lifecycle Methods
| Method | When Called | Async? | Common Use |
|---|---|---|---|
connectedCallback | Element inserted into DOM | No | Add event listeners |
disconnectedCallback | Element removed from DOM | No | Cleanup (remove listeners, timers) |
componentWillLoad | Before first render (once) | Yes | Fetch data, initialize state |
componentDidLoad | After first render (once) | No | DOM manipulation, init 3rd-party libs |
componentWillRender | Before every render | Yes | Compute derived data |
componentDidRender | After every render | No | Post-render DOM operations |
componentShouldUpdate | When Prop/State changes | No | Perf: return false to skip render |
componentWillUpdate | Before re-render due to changes | Yes | Pre-update preparation |
componentDidUpdate | After re-render due to changes | No | Post-update operations |
componentWillLoad can return a Promise — Stencil will wait for it to resolve before the first render, making it ideal for data fetching.
Props Deep Dive
Prop type mapping: In HTML, string/number/boolean are auto-converted. Objects and Arrays must be passed as JSON strings (HTML attributes) or via JavaScript properties. reflect: true enables CSS selectors like [color="red"] to work.
State
Stencil uses reference comparison to detect changes. For Array/Object State, you must create a new reference (spread operator) to trigger re-renders. Direct push/property mutation won't be detected.
Event System
Emitting Events
Listening to Events
Styling & Shadow DOM
shadow: true
Full encapsulation: external CSS cannot affect internals, internal CSS won't leak. Use CSS Custom Properties to pierce. Recommended for libraries.
scoped: true
Stencil auto-adds data attributes for scope isolation (like Vue scoped). No Shadow DOM, better compatibility but weaker encapsulation.
No encapsulation
No shadow/scoped: CSS is global. Suitable for app-level components, not recommended for shared libraries.
Slots (Content Projection)
Testing
Unit Tests (spec)
E2E Tests
Output Targets
| Target | Description | Use Case |
|---|---|---|
dist | npm-publishable library with lazy loading | Publishing component libraries |
dist-custom-elements | One ES Module per component, no lazy loading | Tree-shaking, bundler integration |
www | Dev server + pre-rendered static site | Development, static site generation |
dist-hydrate-script | Node.js rendering script (SSR) | Server-side rendering |
docs-readme | Auto-generate Markdown docs | Component API docs |
docs-json | JSON format API docs | Integrate with doc systems |
Config Reference
CLI Commands
| Command | Description |
|---|---|
npm init stencil | Interactive project creation |
npm start | Start dev server with hot-reload |
npm run build | Production build |
npm test | Run spec + e2e tests |
npm run test.watch | Watch mode testing |
npm run generate | Interactive component file generator |
stencil build --docs | Build and generate docs |
stencil build --watch | Watch mode build |
Best Practices
Tags must contain a hyphen and be lowercase. Use a consistent prefix: my-button, my-modal. Class names use PascalCase: MyButton.
Modifying a Prop internally without mutable: true triggers a warning. Prefer @State for internal data. Only use mutable props when you need two-way binding.
render() should be a pure function — return JSX only, no state mutations or side effects. Put side effects in lifecycle methods or event handlers.
Prefer shadow: true for npm-published libraries (full style isolation). Use scoped: true for app-internal components (better compatibility).
Wrap render output with <Host> to set classes, attributes, and events on the host element itself.
dist auto-lazy-loads components (best for large libraries). dist-custom-elements has no lazy loading but supports tree-shaking (best for few components or bundled apps).
FAQ
Stencil is a compiler, not a framework. It compiles TypeScript + JSX into standard Web Components (Custom Elements + Shadow DOM) at build time, producing native browser code with zero runtime dependency. Generated components work in any framework or vanilla JS.
Yes. Stencil components are standard Custom Elements and work directly in any framework. Stencil also offers framework wrappers via @stencil/react-output-target, @stencil/vue-output-target, etc. for a more native DX.
① Cross-framework UI component libraries (Ionic Framework is built with Stencil); ② Design systems; ③ Shared components in micro-frontends; ④ Projects needing dependency-free, lightweight Web Components.
Yes. The dist-hydrate-script output target enables pre-rendering in Node.js for SSR and static site generation.
Stencil uses virtual DOM diffing, async render queuing, and lazy loading. Build output is tiny (no framework runtime). Only the components actually used on a page are loaded. Ionic 6+ (100+ UI components) is built entirely with Stencil and used in production at scale.