Component API and custom elements

Svelte · Expert · 13 min

Embedding Svelte in other apps, shipping web components and migrating legacy code all depend on the low-level component API.

In Svelte 5 a component is a plain function, not a class. You drive it with functions from svelte:

  • mount(Component, { target, props }) creates and inserts it, returning its exports; hydrate does the same over server-rendered HTML.
  • unmount(instance, { outro: true }) removes it, optionally playing outro transitions (returns a promise).
  • Effects created by mount don't run synchronously; call flushSync() when you need the DOM updated immediately, for example in tests.
  • To change props from outside, pass a $state object as props and mutate it; the component sees the changes.
  • Components can expose functions or values with export function in the script; they appear on the object mount returns.

Custom elements: with <svelte:options customElement="my-counter" /> and the customElement: true compiler option, a component compiles to a web component usable from any framework or plain HTML. Props become attributes and properties (configure types and reflection with the props option), styles are encapsulated in shadow DOM, and $host() gives access to the host element, e.g. to dispatch DOM events. Caveats: the inner component is created a tick after the element is connected, passed-in content uses native <slot> elements rather than snippets, and context does not cross element boundaries.

Legacy interop: Svelte 5 still compiles Svelte 4 syntax (export let, $:, on:click, slots) per component in legacy mode, so you can migrate gradually; npx sv migrate svelte-5 automates most of it. Runes mode is enabled per file as soon as you use a rune.

Example

// embed.svelte.js - mounting Svelte inside a non-Svelte page or a test
import { mount, unmount, flushSync } from 'svelte';
import Counter from './Counter.svelte';

const props = $state({ start: 5 });              // reactive props object
const counter = mount(Counter, { target: document.querySelector('#widget'), props });
flushSync();                                     // DOM is now rendered
console.log(document.querySelector('#widget').textContent);   // e.g. "count: 5"

props.start = 10;                                // component reacts to prop change
counter.reset?.();                               // call a function the component exported
await unmount(counter, { outro: true });         // play outros, then remove

// Counter.svelte compiled as <my-counter> (compilerOptions.customElement = true):
// <svelte:options customElement={{ tag: 'my-counter', props: { start: { type: 'Number' } } }} />
// <script>
//   let { start = 0 } = $props();
//   let count = $state(start);
//   export function reset() { count = start; }
//   const host = $host();
//   const inc = () => { count++; host.dispatchEvent(new CustomEvent('change', { detail: count })); };
// </script>
// <button onclick={inc}>count: {count}</button>

Remember

  • Components are functions: mount, hydrate, unmount, flushSync drive them
  • Pass a $state object as props to update a mounted component from outside
  • customElement compiles to web components; $host() reaches the host element
  • Svelte 4 syntax still compiles per component, so migration can be gradual

Watch out for

  • Using new Component({ target }) from Svelte 4; in Svelte 5 use mount().

Check yourself

  1. After `mount(App, { target })` in a test, the DOM is still empty when you assert. Why, and what fixes it?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: svelte.dev/docs/svelte/custom-elements

Builds on

Unlocks

Nothing builds on this yet.