Svelte inside Aurelia
Libception. Learn how to use Svelte inside of your Aurelia applications.
Aurelia’s interoperability story makes it straightforward to embed islands from other ecosystems. This tutorial walks you through wiring Svelte 5 components (and legacy Svelte 4 builds) into an Aurelia 2 application, covering tooling, wrappers, and lifecycle management.
Install Dependencies
Add Svelte and the official Vite plugin (which also ships the recommended preprocessors):
npm install svelte npm install --save-dev @sveltejs/vite-plugin-svelte vite svelte-checkExtend your workspace TypeScript configuration with the community preset so the compiler understands
.svelteand.svelte.tsmodules:npm install --save-dev @tsconfig/svelte
Svelte already ships its own type definitions—you do not need an @types/svelte package. Keep svelte-check alongside your existing npm run lint to catch template type regressions during CI.
Configure Vite
Make sure Aurelia’s Vite plugin and the Svelte plugin both run so .svelte and .svelte.ts assets are compiled before Aurelia consumes them:
// vite.config.ts
import { defineConfig } from 'vite';
import aurelia from '@aurelia/vite-plugin';
import { svelte, vitePreprocess } from '@sveltejs/vite-plugin-svelte';
export default defineConfig({
plugins: [
aurelia(),
svelte({
extensions: ['.svelte', '.svelte.ts'],
preprocess: vitePreprocess(),
compilerOptions: {
hydratable: true
}
})
]
});The extensions setting ensures rune-enabled helper modules such as .svelte.ts files go through the Svelte compiler rather than the plain TypeScript loader.
TypeScript and Tooling Setup
Augment tsconfig.json (or the workspace override that targets your Aurelia app) so runtime files and the new .svelte.ts helpers stay type-safe:
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"moduleResolution": "bundler",
"types": ["svelte"],
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true
},
"include": [
"src/**/*.ts",
"src/**/*.svelte",
"src/**/*.svelte.ts"
]
}Add a convenience script for local validation:
"scripts": {
"check:svelte": "svelte-check --tsconfig ./tsconfig.json"
}Run this after npm run build when touching shared Aurelia + Svelte code to prevent stale declaration issues.
Build a Svelte 5 Component
Svelte 5 introduces runes such as $state and $props. Here is a simple counter component that exposes a callback for Aurelia to hook into:
<!-- src/components/my-svelte-widget.svelte -->
<script lang="ts">
export interface WidgetProps {
name?: string;
initialCount?: number;
onIncrement?: (value: number) => void;
}
let { name = 'World', initialCount = 0, onIncrement }: WidgetProps = $props();
let count = $state(initialCount);
const increment = () => {
count += 1;
onIncrement?.(count);
};
</script>
<style>
.widget {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 1rem;
border-radius: 0.5rem;
border: 1px solid #d0d7de;
}
</style>
<div class="widget">
<span>Hello from Svelte, {name}!</span>
<button onclick={increment}>Count: {count}</button>
</div>Because Svelte 5 components are functions, props are accessed via $props() and local state uses $state().
Share Reactive Props with .svelte.ts
.svelte.tsWhen Aurelia needs to update props after a component mounts, create the prop state inside a .svelte.ts helper so it can use runes while still being imported from plain TypeScript:
// src/svelte/create-props-state.svelte.ts
export function createPropsState<T extends Record<string, unknown>>(initial: T) {
const props = $state({ ...initial });
return props;
}Because this file ends with .svelte.ts, the Svelte compiler transforms $state before the TypeScript compiler sees it. The allowImportingTsExtensions flag from the previous section lets you import this module verbatim.
Create the Aurelia Wrapper (Svelte 5)
// src/resources/elements/svelte-wrapper.ts
import { customElement, bindable } from '@aurelia/runtime-html';
import { flushSync, mount, unmount, type Component, type ComponentType } from 'svelte';
import { createPropsState } from '../../svelte/create-props-state.svelte.ts';
@customElement({ name: 'svelte-wrapper', template: '<div ref="container"></div>' })
export class SvelteWrapper {
@bindable public svelteComponent?: ComponentType;
@bindable public props?: Record<string, unknown>;
private container!: HTMLDivElement;
private instance: Component | null = null;
private readonly propsState = createPropsState<Record<string, unknown>>({});
public attached(): void {
if (!this.container || !this.svelteComponent) {
return;
}
this.instance = mount(this.svelteComponent, {
target: this.container,
props: this.propsState
});
this.syncProps();
flushSync();
}
public propertyChanged(): void {
this.syncProps();
}
private syncProps(): void {
if (!this.props) {
return;
}
Object.assign(this.propsState, this.props);
}
public detaching(): void {
if (this.instance) {
unmount(this.instance, { outro: true });
this.instance = null;
}
}
}mount/unmountkeep parity with Svelte 5’s imperative API.flushSync()ensuresonMounthooks and pending effects inside the Svelte component finish before Aurelia continues.Object.assignpushes Aurelia’s bindable updates into the$stateproxy created earlier, so Svelte reruns effects without rebuilding the component.
Register and Use the Wrapper
// src/main.ts
import { Aurelia } from 'aurelia';
import { MyApp } from './my-app';
import { SvelteWrapper } from './resources/elements/svelte-wrapper';
Aurelia.register(SvelteWrapper).app(MyApp).start();<!-- src/my-view.html -->
<svelte-wrapper
svelte-component.bind="mySvelteComponent"
props.bind="svelteProps">
</svelte-wrapper>// src/my-view.ts
import MySvelteWidget from './components/my-svelte-widget.svelte';
export class MyView {
public mySvelteComponent = MySvelteWidget;
public svelteProps = {
name: 'Aurelia User',
initialCount: 5,
onIncrement: (value: number) => console.log('Svelte count', value)
};
}Error Handling and Lifecycle Tips
Wrap
mountcalls intry/catchand swap the Aurelia host contents with a fallback string when instantiation fails.Tie subscription cleanup to
detaching()so lingering intervals or stores inside your Svelte component do not leak.If the wrapped component dispatches data through callbacks, surface those as Aurelia events or pass functions through the
propsobject, as shown in the example.
Supporting Legacy Svelte 4 Components
If you still rely on new Component({...}) and $set, keep those builds in compatibility mode:
// svelte.config.js
export default {
compilerOptions: {
compatibility: {
componentApi: 4
}
}
};With compatibility enabled, you can keep the class-based wrapper:
// src/resources/elements/legacy-svelte-wrapper.ts
import { customElement, bindable } from '@aurelia/runtime-html';
import type { SvelteComponent } from 'svelte/legacy';
@customElement({ name: 'legacy-svelte-wrapper', template: '<div ref="container"></div>' })
export class LegacySvelteWrapper {
@bindable public svelteComponent?: typeof SvelteComponent;
@bindable public props?: Record<string, unknown>;
private container!: HTMLDivElement;
private instance: SvelteComponent | null = null;
public attached(): void {
if (!this.container || !this.svelteComponent) {
return;
}
this.instance = new this.svelteComponent({
target: this.container,
props: this.props ?? {}
});
}
public propertyChanged(): void {
this.instance?.$set?.(this.props ?? {});
}
public detaching(): void {
this.instance?.$destroy?.();
this.instance = null;
}
}As you migrate Svelte 4 code to runes, you can drop the compatibility wrapper and move callers to the new svelte-wrapper.
Performance Considerations
Prefer passing stable object references from Aurelia to avoid needless
Object.assignchurn inpropertyChanged.For frequent prop changes, shift more state into the Svelte component itself (or a shared
$statemodule) and only forward primitive inputs from Aurelia.Svelte 5 no longer exposes
beforeUpdate/afterUpdate; if you depend on those hooks, move DOM work into$effectblocks or Aurelia lifecycle callbacks.
Following these steps keeps both frameworks aligned with their modern APIs while letting you reuse any bespoke Svelte UI inside Aurelia. Once everything is wired, run npm run build && npm run check:svelte before shipping to ensure the Aurelia and Svelte toolchains agree on the generated output.
Last updated
Was this helpful?