Skip to main content

Custom elements API

Svelte components can also be compiled to custom elements (aka web components) using the customElement: true compiler option. You should specify a tag name for the component using the <svelte:options> element.

<svelte:options tag="my-element" />

<script>
  export let name = 'world';
</script>

<h1>Hello {name}!</h1>
<slot />

Alternatively, use tag={null} to indicate that the consumer of the custom element should name it.

ts
import MyElement from './MyElement.svelte';
 
customElements.define('my-element', MyElement);

Once a custom element has been defined, it can be used as a regular DOM element:

ts
document.body.innerHTML = `
<my-element>
<p>This is some slotted content</p>
</my-element>
`;

By default, custom elements are compiled with accessors: true, which means that any props are exposed as properties of the DOM element (as well as being readable/writable as attributes, where possible).

To prevent this, add accessors={false} to <svelte:options>.

ts
const el = document.querySelector('my-element');
 
// get the current value of the 'name' prop
console.log(el.name);
 
// set a new value, updating the shadow DOM
el.name = 'everybody';

Custom elements can be a useful way to package components for consumption in a non-Svelte app, as they will work with vanilla HTML and JavaScript as well as most frameworks. There are, however, some important differences to be aware of:

  • Styles are encapsulated, rather than merely scoped. This means that any non-component styles (such as you might have in a global.css file) will not apply to the custom element, including styles with the :global(...) modifier
  • Instead of being extracted out as a separate .css file, styles are inlined into the component as a JavaScript string
  • Custom elements are not generally suitable for server-side rendering, as the shadow DOM is invisible until JavaScript loads
  • In Svelte, slotted content renders lazily. In the DOM, it renders eagerly. In other words, it will always be created even if the component's <slot> element is inside an {#if ...} block. Similarly, including a <slot> in an {#each ...} block will not cause the slotted content to be rendered multiple times
  • The let: directive has no effect
  • Polyfills are required to support older browsers