Svelte has been steadily expanding, and its popularity has surged, according to the feedback gathered in the latest State of JS survey, over the past several years:
After going through the extensive guide, it's natural to consider pushing Svelte further and employing it in a production application.
Yet, converting an entire codebase in one sweeping move isn't necessarily a gentle introduction. Instead, you could build isolated Svelte elements and bring them in as Web Components.
While reading the documentation, a particular line caught my attention:
Your whole application can be built with Svelte, or you can bring it in piece by piece to an existing project. Components can likewise be packaged as standalone units that function in any context, free from the weight of a typical framework dependency.
Being an Angular developer myself, I was curious to test how Svelte components are authored, compiled into plain JavaScript, and then embedded as custom elements within an already-running Angular project to witness the workflow firsthand.
This piece walks through the steps for placing Svelte components into an active Angular application, treating them as Web Components.
✋ Attention - Svelte 4 has just been released but this guide is using Svelte 3: the package used here are still using Svelte 3 as well so migrating now would be a bit premature.
If you would like to learn more about Svelte 4, I just blogged about it:
Table of Contents
- What are Web Components?
- Our Svelte project
- Integrating Svelte Web Components into Angular
- Takeaways
What are Web Components?
Before we dive head-first into how Svelte and Angular work together, let's examine the element that connects them: Web Components.
According to the MDN web docs:
Web Components is a suite of different technologies allowing you to create reusable custom elements — with their functionality encapsulated away from the rest of your code — and utilize them in your web apps.
At its heart, this approach involves isolating a snippet of HTML along with its behavior, then reusing it in other places without worrying about it clashing with the surrounding environment.
The backbone of Web Components rests on three main pillars:
- 🧪 Custom Elements allow us to define our own HTML tags, complete with unique behavior, styling, and optionally templates
- 🌑 Shadow DOM serves as a component-specific DOM, enabling all its elements to live in complete separation from the global DOM
- 🧩 HTML Templates offer a mechanism to define and reuse HTML fragments by injecting them into the DOM
In our case, this translates to taking a Svelte component — with its markup, styles, and logic — and placing it into an Angular app.
Now the picture is getting clearer. Time to move on to the implementation!
Our Svelte Project
Before we can export any Svelte component, our first step is building the component library itself.
For this, we will set up a new library project with SvelteKit.
Setting up a New Project
Svelte acts purely as a compiler, so we’ll start with a plain JavaScript setup.
Inside a fresh svelte-web-components directory, add a package.json file with the configuration shown below:
{
"name": "svelte-web-components",
"version": "1.0.0",
"scripts": { },
"devDependencies": {
"svelte": "^3.59.1"
},
"type": "module"
}
Now we can move on to the next step.
Creating a Custom Svelte Component
In this example, we'll set up a counter that takes an initial value and supports incrementing, decrementing, or resetting it.
Create a new file named components/Counter.svelte and write the component's code there:
<!-- components/Counter.svelte -->
<script>
export let initialValue = 0;
let count = initialValue;
$: isInitialValue = count === initialValue;
const increment = () => (count += 1);
const decrement = () => (count -= 1);
const reset = () => (count = initialValue);
</script>
<div>
<span>{count}</span>
<button type="button" on:click={decrement}>-</button>
<button type="button" on:click={increment}>+</button>
<button type="button" on:click={reset} disabled={isInitialValue}>Reset</button>
</div>
Feel like experimenting? Svelte offers an interactive REPL you can use right in your browser
Your output will look quite alike to this:
Because we're creating a library, we need to make sure it's part of the public API as well:
// components/index.js
export { default as Counter } from './Counter.svelte';
To leverage Svelte’s strengths, we’ll rely on a store for tracking the counter’s value.
Strictly speaking, this isn’t required for this example, but it gives us a chance to verify that store-related features remain functional once we export the component.
The revised code differs only marginally:
<!-- components/Counter.svelte -->
<script>
import { writable } from 'svelte/store';
export let initialValue = 0;
let count = writable(initialValue);
$: isInitialValue = $count === initialValue;
const increment = () => count.update((n) => (n += 1));
const decrement = () => count.update((n) => (n -= 1));
const reset = () => count.set(initialValue);
</script>
<div>
<span>{$count}</span>
<button type="button" on:click={decrement}>-</button>
<button type="button" on:click={increment}>+</button>
<button type="button" on:click={reset} disabled={isInitialValue}>Reset</button>
</div>
Since we will be using our component elsewhere, let's also style it a little so that it will be more pleasant to use
✨ Additional CSS
<!-- components/Counter.svelte --> <style> div { display: flex; align-items: center; gap: 5px; border: 1px solid #999; width: fit-content; padding: 5px; border-radius: 5px; } div span { font-size: 18px; font-weight: bold; margin: 0 10px; } div button { padding: 5px 10px; border: 1px solid #ccc; background-color: #f0f0f0; color: #333; font-size: 16px; transition: background-color 0.3s ease; border-radius: 5px; } div button:hover { background-color: #e0e0e0; } div button:active { background-color: #ccc; } div button:disabled { opacity: 40%; } </style>
Our component continues to operate correctly, relying on its store.
The results are so compelling that I'd like to reuse them beyond this library—let's figure out how.
Transforming Svelte Component into a Web Component
Earlier, we established that turning the component into a Web Component lets us run it outside this setup.
Taking our checklist, here's what's already good to go:
- ❌
🧪 Custom Elements - ✅ 🌑 Shadow DOM
- ✅ 🧩 HTML Templates
We're right at the finish line!
For our custom HTML element to work, we must register one for the component.
That's achievable through the dedicated <svelte:options> element, which lets us pick the tag name:
<!-- components/Counter.svelte -->
<svelte:options tag="svelte-counter" />
<!-- Counter component code here -->
Our Counter component now meets all three criteria required for a Web Component.
Compiling Svelte Component to Pure JavaScript
With the component ready for export, we proceed by compiling it into pure JavaScript.
To achieve this, we use esbuild to bundle our component. Two additional dependencies are required: esbuild and esbuild-svelte:
npm i -D esbuild esbuild-svelte
Next, we add a script that reads the library’s entry point and emits the resulting JavaScript output:
// esbuild-bundle.js
import esbuild from "esbuild";
import sveltePlugin from "esbuild-svelte";
esbuild
.build({
entryPoints: ["./components"],
bundle: true,
outfile: "dist/web-components.js",
plugins: [
sveltePlugin(),
],
logLevel: "info",
})
.catch(() => process.exit(1));
That said, eslint needs to know we're producing web components rather than just compiling our application:
esbuild
.build({
entryPoints: ["./components"],
bundle: true,
outfile: "dist/web-components.js",
plugins: [
sveltePlugin({
+ compilerOptions: {
+ customElement: true,
+ },
}),
],
logLevel: "info",
})
.catch(() => process.exit(1));
For convenience, let’s toss a new script into the package.json scripts block so the whole process is simpler to kick off:
{
"name": "svelte-web-components",
"version": "1.0.0",
"scripts": {
+ "build": "node esbuild-bundle.js"
},
"devDependencies": {
"esbuild": "^0.18.2",
"esbuild-svelte": "^0.7.3",
"svelte": "^3.59.1"
},
"type": "module"
}
At this point, running npm run build will produce the following output:
The built artifact should now contain your custom element, exposed as dist/web-components.js 📦
Putting Svelte Web Components to Work in Angular
Once the bundle is in place, the only remaining step is to import it.
Scaffolding an Angular Project
To get your Angular app off the ground, execute this:
ng new angular-wrapper --standalone --defaults
Go ahead and swap out the automatically produced app.component.ts file with the snippet provided below:
import { Component } from "@angular/core";
@Component({
selector: 'app-root',
standalone: true,
template: `
<h1>Svelte in Angular!</h1>
`,
})
export class AppComponent {}
With the Angular application set up and the Svelte Web Component ready, it's time to connect the two.
Bringing Svelte Web Components into Angular
Angular must be informed about our external JS bundle, so we’ll register it as a project dependency.
To do this, take the web-components.js file we built earlier and place it in a fresh directory at the project’s root, named src/scripts:
Then make sure this path is registered inside the scripts array of the angular.json configuration—specifically under angular.json > projects > angular-wrapper > architect > build > options > scripts:
"styles": [
"src/styles.css"
],
"scripts": [
+ "src/scripts/web-components.js"
]
Now that Angular has bundled our Web Component together with our application, the counter is ready to be used.
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
standalone: true,
template: `
<h1>Svelte in Angular!</h1>
+ <svelte-counter />
`,
})
export class AppComponent {}
... or can we?
Angular examines the HTML tags we write to make sure they’re valid—either built-in elements or components it knows how to resolve—so we don’t end up with errors. In this scenario, though, the tag doesn’t match either category.
Thankfully, there’s a built-in escape hatch for exactly this situation.
If 'svelte-counter' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@Component.schemas' of this component to suppress this warning.
+ import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
@Component({
selector: "app-root",
standalone: true,
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<h1>Svelte in Angular!</h1>
<svelte-counter />
`,
})
export class AppComponent {}
At this point, the Svelte Web Component is live within the Angular application.
Key Points
Throughout this guide, we covered what Web Components are, the steps to transform a Svelte component into one, and the proper way to use it inside an Angular project.
Thanks to the available community packages and Svelte's compiler-based design—which natively produces isolated JavaScript bundles—the entire integration remains uncomplicated.
For a look at the final implementation, feel free to browse the dedicated GitHub repository.
Hopefully, this walkthrough gave you a new, practical insight into mixing these technologies!







