@stencil/core, lit, and svelte are all tools for building modern user interfaces, but they target different architectural needs. @stencil/core is a compiler that generates standard Web Components, designed primarily for creating design systems that work across any framework. lit is a lightweight library for building Web Components using standard JavaScript and template literals, focusing on speed and alignment with web standards. svelte is a compiler that shifts work from the browser to the build step, generating highly optimized imperative code, and while it can output Web Components, it is primarily used as a standalone application framework.
When architecting a frontend system, the choice between @stencil/core, lit, and svelte often comes down to one key question: are you building a library for others to use, or an application for users to interact with? All three tools offer component-based development, but they solve different problems regarding distribution, runtime performance, and developer workflow. Let's break down how they handle the core mechanics of modern UI development.
The way you define structure and logic varies significantly across these tools, impacting how developers onboard and how code is maintained.
@stencil/core uses JSX and TypeScript decorators.
// stencil: src/components/my-component.tsx
import { Component, Prop, h } from '@stencil/core';
@Component({ tag: 'my-component' })
export class MyComponent {
@Prop() name: string = 'World';
render() {
return <div>Hello, {this.name}!</div>;
}
}
lit uses JavaScript classes with tagged template literals.
html tag.// lit: src/my-component.js
import { LitElement, html } from 'lit';
import { property } from 'lit/decorators.js';
export class MyComponent extends LitElement {
@property() name = 'World';
render() {
return html`<div>Hello, ${this.name}!</div>`;
}
}
svelte uses a single-file component format with HTML-like syntax.
<script> tag, markup in the template, and styles in <style>.<!-- svelte: src/MyComponent.svelte -->
<script>
export let name = 'World';
</script>
<div>Hello, {name}!</div>
<style>
div { color: blue; }
</style>
How the UI updates when data changes is critical for performance and code clarity. Each tool handles reactivity differently.
@stencil/core uses decorators to track state changes.
@State() triggers a re-render when internal data changes.@Prop() handles external data passed into the component.// stencil: State management
import { Component, State, Prop, h } from '@stencil/core';
@Component({ tag: 'counter-component' })
export class CounterComponent {
@Prop() initialCount = 0;
@State() count = 0;
componentDidLoad() {
this.count = this.initialCount;
}
render() {
return <button onClick={() => this.count++}>{this.count}</button>;
}
}
lit uses reactive properties that trigger updates automatically.
@property are observed for changes.// lit: State management
import { LitElement, html } from 'lit';
import { property, state } from 'lit/decorators.js';
export class CounterComponent extends LitElement {
@property({ type: Number }) initialCount = 0;
@state() count = 0;
firstUpdated() {
this.count = this.initialCount;
}
render() {
return html`<button @click=${() => this.count++}>${this.count}</button>`;
}
}
svelte uses compile-time reactivity with assignment.
$: label for derived state.<!-- svelte: State management -->
<script>
export let initialCount = 0;
let count = initialCount;
$: doubled = count * 2;
</script>
<button on:click={() => count++}>{count} (x2: {doubled})</button>
Encapsulation is a major reason developers choose Web Components. Here is how each tool handles CSS scoping.
@stencil/core enables Shadow DOM by default.
// stencil: Styling
@Component({
tag: 'styled-component',
styleUrl: 'styled-component.css',
shadow: true
})
export class StyledComponent {
render() {
return <div class="container">Scoped Content</div>;
}
}
lit supports Shadow DOM and provides helper functions for styles.
static styles to define CSS within the class.// lit: Styling
import { css } from 'lit';
export class StyledComponent extends LitElement {
static styles = css`
.container { color: red; }
`;
render() {
return html`<div class="container">Scoped Content</div>`;
}
}
svelte scopes CSS by default without Shadow DOM.
<!-- svelte: Styling -->
<div class="container">Scoped Content</div>
<style>
.container { color: red; }
/* Compiler transforms this to .container.svelte-123xyz */
</style>
One of the biggest architectural decisions is whether the component needs to work inside React, Angular, or Vue.
@stencil/core is built specifically for this use case.
// stencil: Output
// Compiles to:
// <my-component name="John"></my-component>
// Works in React, Angular, Vue, or plain HTML without extra deps.
lit outputs standard Custom Elements natively.
// lit: Output
// Compiles to:
// <my-component name="John"></my-component>
// Directly usable in any framework that supports Custom Elements.
svelte primarily targets Svelte applications.
customElements compiler option.<!-- svelte: Output -->
<!-- Requires compiler config: { customElements: true } -->
<!-- Then usable as: <my-component name="John"></my-component> -->
<!-- Without config, it is a Svelte-specific module import. -->
| Feature | @stencil/core | lit | svelte |
|---|---|---|---|
| Primary Goal | Design Systems & Libraries | Web Components | Application Framework |
| Templating | JSX (TSX) | Tagged Template Literals | HTML-like Syntax |
| Reactivity | Decorators (@State) | Reactive Properties | Compile-time Assignments |
| Output | Standard Web Components | Standard Web Components | JS Modules (or WC) |
| Runtime | Small Runtime + Virtual DOM | Minimal Runtime | No Runtime (Compiled) |
| Learning Curve | Moderate (React-like) | Low (Standard JS) | Low (Unique Syntax) |
@stencil/core is the enterprise choice ๐ข. It shines when you need to maintain a component library used by multiple teams working in different frameworks. The JSX support makes it familiar to React developers, and the robust tooling helps manage large-scale design systems. However, it adds build complexity that might be overkill for smaller projects.
lit is the standards choice ๐. It is ideal for developers who want the benefits of Web Components without a heavy abstraction layer. It is lightweight, fast, and stays close to the platform. Choose this when you want maximum compatibility and minimal lock-in to a specific toolchain.
svelte is the application choice ๐. It offers the best developer experience for building complete apps, with incredibly concise code and no virtual DOM overhead. While it can produce Web Components, that is not its primary strength. Use Svelte when you own the entire application stack and do not need to distribute components to non-Svelte projects.
Final Thought: If you are building a library, pick @stencil/core or lit. If you are building an app, pick svelte. Between the two library options, choose @stencil/core for React-like DX and lit for standard JavaScript simplicity.
Choose @stencil/core if you are building a design system or component library that must be consumed by multiple frameworks like React, Angular, and Vue simultaneously. It is ideal for enterprise teams that need strong typing, JSX support, and features like server-side rendering or hydration out of the box for their Web Components. However, be aware that it introduces a build step complexity that is heavier than standard Web Component libraries.
Choose lit if you want to build Web Components with minimal abstraction and maximum alignment with current browser standards. It is perfect for teams that prefer writing standard JavaScript or TypeScript without a heavy compiler toolchain or JSX transformation. Use this when you need small, fast components that integrate easily into any project without requiring a specific framework runtime.
Choose svelte if you are building a complete application and want a developer experience focused on writing less boilerplate code. It is best suited for projects where you control the entire stack and do not strictly require Web Component output for cross-framework usage. While it can compile to Web Components, its primary strength lies in its reactive compiler model for standalone apps rather than distributable component libraries.
A compiler for generating Web Components using technologies like TypeScript and JSX, built by the Ionic team.
Start a new project by following our quick Getting Started guide. We would love to hear from you! If you have any feedback or run into issues using Stencil, please file an issue on this repository.
A Stencil component looks a lot like a class-based React component, with the addition of TypeScript decorators:
import { Component, Prop, h } from '@stencil/core';
@Component({
tag: 'my-component', // the name of the component's custom HTML tag
styleUrl: 'my-component.css', // css styles to apply to the component
shadow: true, // this component uses the ShadowDOM
})
export class MyComponent {
// The component accepts two arguments:
@Prop() first: string;
@Prop() last: string;
//The following HTML is rendered when our component is used
render() {
return (
<div>
Hello, my name is {this.first} {this.last}
</div>
);
}
}
The component above can be used like any other HTML element:
<my-component first="Stencil" last="JS"></my-component>
Since Stencil generates web components, they work in any major framework or with no framework at all. In many cases, Stencil can be used as a drop in replacement for traditional frontend framework, though using it as such is certainly not required.
Thanks for your interest in contributing! Please take a moment to read up on our guidelines for contributing. We've created comprehensive technical documentation for contributors that explains Stencil's internal architecture, including the compiler, runtime, build system, and other core components in the /docs directory. Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms.