Building Reusable Web Components With Stencil JS

Master the art of creating standards-compliant, framework-agnostic web components. From project setup to npm distribution, learn everything you need to build reusable component libraries.

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.

Why Choose Stencil for Web Components

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.

stencil.config.ts - Output Target Configuration
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.

A Basic Stencil Component
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.

Custom Events in Stencil
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.

Frequently Asked Questions

Ready to Build Your Component Library?

Our team of web development experts can help you design, build, and publish a reusable component library using Stencil or any technology that fits your needs.