What Is Stencil and Why Use It
Modern web development increasingly demands components that work across frameworks and platforms. Web components provide a standardized way to create reusable UI elements, but writing them from scratch requires significant boilerplate. Stencil, created by the Ionic team, solves this by offering a compiler that generates standard-compliant web components with optional framework features.
The Web Components Standard
Web components are a set of browser-native technologies that allow you to create custom, reusable HTML elements. They consist of three main building blocks: Custom Elements for defining new HTML tags, Shadow DOM for encapsulation, and HTML Templates for efficient rendering.
Unlike pure vanilla web components, Stencil provides features like TypeScript decorators, reactive data binding, and a virtual DOM that developers expect from modern frameworks. However, Stencil outputs pure, standards-compliant custom elements that work in any browser and with any framework. For teams building comprehensive web applications, this approach eliminates framework lock-in while maintaining excellent developer experience.
Key Advantages of Stencil
Stencil components are significantly smaller than framework components because they lack runtime framework overhead. The compiler optimizes during build time, removing unused code through tree-shaking.
Standards Compliant
Output is pure web component standards with no framework runtime
TypeScript Support
Full TypeScript integration with decorators and type safety
Lazy Loading
Components load only when needed, optimizing bundle size
Cross-Framework
Works with React, Angular, Vue, or plain HTML
Setting Up Your Stencil Project
Installation and Project Structure
Getting started with Stencil requires Node.js version 18 or higher. Create a new Stencil project using the stencil CLI, which scaffolds a project with sensible defaults for component library development. The CLI offers starter templates for different use cases including component libraries, apps, and doc sites.
A typical Stencil project structure includes a src directory with components organized in folders, each containing the component's TypeScript file, CSS file, and unit test. The stencil.config.ts file controls compilation targets, output destinations, and plugin configurations.
Configuration for Component Libraries
When building a component library for distribution, configure Stencil to output in multiple formats. The dist target creates files for npm distribution with type definitions. The docs-readme target generates API documentation. The dist-custom-elements output produces tree-shakeable individual component files.
1import { Config } from '@stencil/core';2 3export const config: Config = {4 namespace: 'my-components',5 outputTargets: [6 {7 type: 'dist',8 esmLoaderPath: '../loader',9 },10 {11 type: 'dist-custom-elements',12 },13 {14 type: 'docs-readme',15 },16 {17 type: 'www',18 serviceWorker: null,19 },20 ],21};Creating Your First Component
Component Decorators and Structure
Every Stencil component starts with the @Component decorator, which provides metadata about the element. The decorator specifies the tag name users will employ in their HTML, along with optional settings for shadow DOM, styles, and scoped CSS.
The @Prop decorator marks class properties as reactive attributes that users can set via HTML or JavaScript. When a prop changes, Stencil automatically re-renders the component. Props can be of various types including strings, numbers, booleans, and objects.
Properties and Attributes
Properties in Stencil serve as the public API for your components. Stencil automatically converts between JavaScript property names and kebab-case HTML attributes. For complex objects or arrays, use the attribute option to specify a different attribute name and reflect to sync the value back.
1import { Component, Prop, h } from '@stencil/core';2 3@Component({4 tag: 'my-button',5 styleUrl: 'my-button.css',6 shadow: true,7})8export class MyButton {9 @Prop() type: 'primary' | 'secondary' | 'danger' = 'primary';10 @Prop() disabled: boolean = false;11 @Prop() size: 'small' | 'medium' | 'large' = 'medium';12 13 render() {14 return (15 <button16 class={{ btn: true, [`btn--${this.type}`]: true, [`btn--${this.size}`]: true }}17 disabled={this.disabled}18 >19 <slot></slot>20 </button>21 );22 }23}State Management and Reactivity
Internal State With @State
While @Prop handles external data passing, @State manages internal component state. When a @State property changes, Stencil triggers a re-render, efficiently updating only the parts of the DOM that changed. This reactive system eliminates manual DOM manipulation.
Watchers and Change Detection
The @Watch decorator responds to prop or state changes with side effects. Watchers receive both new and previous values, enabling conditional logic based on what changed. Avoid heavy computations directly in watchers.
Events and Component Communication
Custom Events With @Event
Components communicate upward through custom events dispatched via the @Event decorator. Events bubble through the DOM, allowing parent components to listen and respond without tight coupling. Stencil handles event creation and dispatching with a clean API.
Event Options and Configuration
Events support bubbles for DOM propagation, composed for crossing shadow DOM boundaries, cancelable for preventing default behavior, and detail for payload data. Name events consistently to help consumers understand your component's API.
1import { Component, Prop, Event, EventEmitter, h } from '@stencil/core';2 3@Component({4 tag: 'search-input',5 styleUrl: 'search-input.css',6 shadow: true,7})8export class SearchInput {9 @Prop() placeholder: string = 'Search...';10 @Event() search: EventEmitter<string>;11 12 private handleInput = (e: Event) => {13 const value = (e.target as HTMLInputElement).value;14 this.search.emit(value);15 }16 17 render() {18 return (19 <input20 type="text"21 placeholder={this.placeholder}22 onInput={this.handleInput}23 />24 );25 }26}Slots and Content Projection
Basic Slot Usage
The <slot> element provides a mechanism for projecting parent content into your component. Named slots allow multiple content insertion points within a single component. Components can provide fallback content that displays when no content is provided for a slot.
Usage Example
<card title="Welcome">
<p>This content goes in the default slot.</p>
<p slot="footer">This appears in the footer slot.</p>
</card>
Styling and Shadow DOM
CSS Architecture
Shadow DOM provides style encapsulation, preventing external styles from affecting your component and keeping component styles from leaking out. The :host selector targets the component element itself.
CSS Variables and Theming
Design systems built with Stencil expose CSS custom properties that allow users to customize component appearance without modifying source CSS. These variables cascade through Shadow DOM when defined on the host element. This approach is essential for building scalable design systems that maintain consistency while allowing customization.
Lifecycle Methods
Understanding the Lifecycle
Stencil components progress through specific lifecycle phases: componentWillLoad for one-time setup before first render, componentDidLoad for post-render operations, componentDidUpdate for responding to changes, and disconnectedCallback for cleanup.
When to Use Each Method
componentWillLoad is ideal for fetching initial data. componentDidLoad suits DOM manipulations. componentDidUpdate responds to prop changes. disconnectedCallback cleans up event listeners and timers.
Building and Distributing Components
NPM Package Configuration
Distributing Stencil components to npm requires proper package.json configuration. The files array specifies build artifacts, while main, module, and types fields point to entry points. Setting sideEffects: false enables tree-shaking.
Versioning and Documentation
Use semantic versioning for releases. Auto-generated documentation from Stencil's docs-readme output provides API reference. Publish documentation through Storybook or Stencil's docs site.
Integration With Frameworks
React Integration
Stencils outputs work with React through the @stencil/react-output-target that generates React wrappers. These wrappers convert native custom element events to React props and handle prop serialization. This capability is valuable for organizations running multi-technology stacks that need to share components across different framework applications.
Angular and Vue Integration
Angular uses CUSTOM_ELEMENTS_SCHEMA to suppress warnings. Vue registers custom elements as global components. Both frameworks handle Stencil events as native DOM events.
Best Practices for Reusable Components
API Design Principles
Design component APIs around usage patterns rather than implementation details. Props should have sensible defaults. Avoid exposing internal implementation details as public API. Well-designed component APIs reduce maintenance overhead and improve adoption rates across development teams.
Performance Considerations
Keep base components lightweight by extracting optional features. Minimize DOM updates using the reactivity system efficiently. Use @Watch to short-circuit unnecessary renders. These practices ensure your component library scales well as you expand your web platform.