vuejs/core
Custom elements
defineCustomElement wraps a Vue component as a Web Component (HTMLElement subclass) that can be registered with customElements.define. The implementation lives in a single file, packages/runtime-dom/src/apiCustomElement.ts (~22KB), and ties together a standard Vue component, the Custom Elements API, and Shadow DOM.
Public API
import { defineCustomElement } from 'vue';
const MyButton = defineCustomElement({
props: { label: String },
template: `<button>{{ label }}</button>`,
});
customElements.define('my-button', MyButton);The returned MyButton is a class extending VueElement, which extends HTMLElement. Inside a custom element you can use Composition API as normal; useHost() returns the host element and useShadowRoot() returns the shadow root.
VueElement class
VueElement (defined in apiCustomElement.ts) implements the four Custom Elements lifecycle callbacks plus the attribute-observation contract:
connectedCallback— mounts the wrapped Vue app into either a shadow root (default) or the element itself (shadowRoot: false), then runs theconnectedVue lifecycle hook.disconnectedCallback— unmounts the app on next microtask. The microtask delay handles the case of moving the element across the DOM (it is briefly disconnected and reconnected during a move).adoptedCallback— invoked when the element is moved between documents.attributeChangedCallback— propagates attribute changes into the inner Vue component's props.
Static observedAttributes is computed from the component's props definition: each prop that exists on the host element becomes a watched attribute. Hyphen-cased forms are accepted (my-prop ↔ myProp).
Shadow DOM
By default, the wrapped component mounts into a shadow root with mode: 'open'. defineCustomElement(component, { shadowRoot: false }) disables shadow DOM, in which case styles need extra care since they will leak into the page.
When shadow DOM is enabled:
<style>blocks from the SFC are injected as<style>tags inside the shadow root.- The renderer's
nodeOps.insertis called against the shadow root. - The host element acts as the boundary; events still propagate through retargeting per Custom Elements spec.
Props vs attributes
The wrapper does double duty:
- Attribute → prop: attribute changes are converted using the prop's declared
type(Number/Boolean/Object/Array). Objects/arrays are JSON-parsed. - Property → prop: setting
el.someProp = 42directly bypasses the attribute and writes to the inner component.
The list of declared props determines which keys participate. Anything not declared as a prop is ignored at the custom-element level; you can still listen for attribute changes manually via attributeChangedCallback.
SSR
defineSSRCustomElement is a sister API for components that need server-rendering before being upgraded by the browser. It produces a custom element class whose initial markup is rendered server-side, and connectedCallback hydrates the existing children.
The shadow-root case is more involved because the SSR markup uses Declarative Shadow DOM (<template shadowrootmode="open">).
Composables
useHost()— returns theHTMLElement(the custom element) for the current instance, ornullif not inside a custom element.useShadowRoot()— returns the shadow root, ornullif shadow DOM is disabled or not yet attached.
These are simple inject-style lookups that walk up the component tree looking for a marker provided by VueElement on mount.
Limitations
The implementation calls these out:
- The host element only knows about declared props. Slots are exposed as light-DOM children (in non-shadow mode) or via
<slot>projection (in shadow mode). provide/injectdoes not cross the custom-element boundary by default; you have to pipe context through props.- The wrapped component cannot use
<Teleport to="body">to render into the page from inside a closed shadow root without help.
Files to know
packages/runtime-dom/src/apiCustomElement.tspackages/runtime-dom/src/index.ts(re-exportsdefineCustomElement,useHost,useShadowRoot,VueElement)packages/runtime-core/src/component.ts(ComponentCustomElementInterface)
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.