自定义元素

Svelte 组件也可以使用 customElement: true 编译器选项编译为自定义元素(即 Web Components)。你应该使用 <svelte:options> 元素 为组件指定一个标签名。在自定义元素内部,你可以通过 $host 符文访问宿主元素。

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

<script>
	let { name = 'world' } = $props();
</script>

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

你可以省略任何你不想暴露的内部组件的标签名,并像使用普通的 Svelte 组件一样使用它们。该组件的消费者在需要时可以稍后再为它命名,使用静态的 element 属性——它包含了自定义元素的构造器,并在 customElement 编译器选项为 true 时可用。

// @noErrors
import MyElement from './MyElement.svelte';

customElements.define('my-element', MyElement.element);

一旦自定义元素被定义,它就可以像普通的 DOM 元素一样被使用:

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

任何 props 都会作为 DOM 元素的属性被暴露(并且在可能的情况下,也可以作为属性被读取/写入)。

// @noErrors
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';

注意,你需要显式地列出所有属性,即如果在组件选项 中执行 let props = $props() 而不声明 props,意味着 Svelte 无法知道要将哪些 props 作为属性暴露到 DOM 元素上。

Component lifecycle

Custom elements are created from Svelte components using a wrapper approach. This means the inner Svelte component has no knowledge that it is a custom element. The custom element wrapper takes care of handling its lifecycle appropriately.

When a custom element is created, the Svelte component it wraps is not created right away. It is only created in the next tick after the connectedCallback is invoked. Properties assigned to the custom element before it is inserted into the DOM are temporarily saved and then set on component creation, so their values are not lost. The same does not work for invoking exported functions on the custom element though, they are only available after the element has mounted. If you need to invoke functions before component creation, you can work around it by using the extend option.

When a custom element written with Svelte is created or updated, the shadow DOM will reflect the value in the next tick, not immediately. This way updates can be batched, and DOM moves which temporarily (but synchronously) detach the element from the DOM don't lead to unmounting the inner component.

The inner Svelte component is destroyed in the next tick after the disconnectedCallback is invoked.

Component options

When constructing a custom element, you can tailor several aspects by defining customElement as an object within <svelte:options> since Svelte 4. This object may contain the following properties:

  • tag: string: an optional tag property for the custom element's name. If set, a custom element with this tag name will be defined with the document's customElements registry upon importing this component.
  • shadow: an optional property to modify shadow root properties. It accepts the following values:
    • "none": No shadow root is created. Note that styles are then no longer encapsulated, and you can't use slots.
    • "open": Shadow root is created with the mode: "open" option.
    • ShadowRootInit: You can pass a settings object that will be passed to attachShadow() when shadow root is created.
  • props: an optional property to modify certain details and behaviors of your component's properties. It offers the following settings:
    • attribute: string: To update a custom element's prop, you have two alternatives: either set the property on the custom element's reference as illustrated above or use an HTML attribute. For the latter, the default attribute name is the lowercase property name. Modify this by assigning attribute: "<desired name>".
    • reflect: boolean: By default, updated prop values do not reflect back to the DOM. To enable this behavior, set reflect: true.
    • type: 'String' | 'Boolean' | 'Number' | 'Array' | 'Object': While converting an attribute value to a prop value and reflecting it back, the prop value is assumed to be a String by default. This may not always be accurate. For instance, for a number type, define it using type: "Number" You don't need to list all properties, those not listed will use the default settings.
  • extend: an optional property which expects a function as its argument. It is passed the custom element class generated by Svelte and expects you to return a custom element class. This comes in handy if you have very specific requirements to the life cycle of the custom element or want to enhance the class to for example use ElementInternals for better HTML form integration.
<svelte:options
	customElement={{
		tag: 'custom-element',
		shadow: {
			mode: import.meta.env.DEV ? 'open' : 'closed',
			clonable: true,
			// ...
		},
		props: {
			name: { reflect: true, type: 'Number', attribute: 'element-index' }
		},
		extend: (customElementConstructor) => {
			// Extend the class so we can let it participate in HTML forms
			return class extends customElementConstructor {
				static formAssociated = true;

				constructor() {
					super();
					this.attachedInternals = this.attachInternals();
				}

				// Add the function here, not below in the component so that
				// it's always available, not just when the inner Svelte component
				// is mounted
				randomIndex() {
					this.elementIndex = Math.random();
				}
			};
		}
	}}
/>

<script>
	let { elementIndex, attachedInternals } = $props();
	// ...
	function check() {
		attachedInternals.checkValidity();
	}
</script>

...

[!NOTE] While Typescript is supported in the extend function, it is subject to limitations: you need to set lang="ts" on one of the scripts AND you can only use erasable syntax in it. They are not processed by script preprocessors.

Caveats and limitations

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 (unless you set shadow: "none"). 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 deprecated let: directive has no effect, because custom elements do not have a way to pass data to the parent component that fills the slot
  • Polyfills are required to support older browsers
  • You can use Svelte's context feature between regular Svelte components within a custom element, but you can't use them across custom elements. In other words, you can't use setContext on a parent custom element and read that with getContext in a child custom element.
  • Don't declare properties or attributes starting with on, as their usage will be interpreted as an event listener. In other words, Svelte treats <custom-element oneworld={true}></custom-element> as customElement.addEventListener('eworld', true) (and not as customElement.oneworld = true)