Vue's Custom Elements API allows you to define Vue components as standard web components that can be used in any HTML document or framework. This provides interoperability and framework-agnostic component distribution.
Create Vue-based custom elements that can be used as standard HTML elements.
/**
* Define a Vue component as a custom element
* @param tag - Custom element tag name
* @param component - Vue component definition
* @param options - Custom element options
* @returns Custom element constructor
*/
function defineCustomElement<T extends Component = Component>(
component: T,
options?: CustomElementOptions
): CustomElementConstructor;
/**
* Define a Vue component as a custom element from SFC
* @param component - Single file component
* @param options - Custom element options
* @returns Custom element constructor
*/
function defineCustomElement<T extends Component = Component>(
component: T extends ComponentOptions
? T
: T extends (...args: any[]) => Component
? T
: ComponentOptionsWithProps<T>,
options?: CustomElementOptions
): CustomElementConstructor;
interface CustomElementOptions {
styles?: string[];
shadowRoot?: boolean;
nonce?: string;
configureApp?: (app: App) => void;
}Usage Examples:
import { defineCustomElement, ref } from "vue";
// Define a simple custom element
const MyButton = defineCustomElement({
props: {
label: String,
disabled: Boolean
},
emits: ['click'],
setup(props, { emit }) {
const handleClick = () => {
if (!props.disabled) {
emit('click');
}
};
return {
handleClick
};
},
template: `
<button
:disabled="disabled"
@click="handleClick"
:style="{ backgroundColor: disabled ? '#ccc' : '#007bff' }"
>
{{ label }}
</button>
`,
styles: [`
button {
padding: 8px 16px;
border: none;
border-radius: 4px;
color: white;
cursor: pointer;
}
button:disabled {
cursor: not-allowed;
}
`]
});
// Register the custom element
customElements.define('my-button', MyButton);
// Now can be used in HTML
// <my-button label="Click me" @click="handleClick"></my-button>Define custom elements that work with server-side rendering.
/**
* Define SSR-compatible custom element
* @param component - Vue component definition
* @param options - Custom element options
* @returns SSR custom element constructor
*/
function defineSSRCustomElement<T extends Component = Component>(
component: T,
options?: CustomElementOptions
): CustomElementConstructor;Usage Example:
import { defineSSRCustomElement } from "vue";
const SSRCounter = defineSSRCustomElement({
setup() {
const count = ref(0);
return {
count,
increment: () => count.value++
};
},
template: `
<div>
<p>Count: {{ count }}</p>
<button @click="increment">+</button>
</div>
`,
styles: [`
div {
padding: 16px;
border: 1px solid #ccc;
}
`]
});
customElements.define('ssr-counter', SSRCounter);Base class for Vue custom elements providing lifecycle and reactivity integration.
/**
* Base class for Vue custom elements
*/
class VueElement extends HTMLElement {
/**
* Vue app instance for this element
*/
readonly _instance: ComponentInternalInstance | null;
/**
* Root element styles
*/
readonly _styles?: HTMLStyleElement[];
/**
* Create Vue element instance
* @param initialProps - Initial properties
*/
constructor(initialProps?: Record<string, any>);
/**
* Connect element to DOM
*/
connectedCallback(): void;
/**
* Disconnect element from DOM
*/
disconnectedCallback(): void;
/**
* Handle attribute changes
*/
attributeChangedCallback(
name: string,
oldValue: string | null,
newValue: string | null
): void;
/**
* Element was adopted to new document
*/
adoptedCallback(): void;
}Access the shadow root within custom element components.
/**
* Get access to the shadow root in custom element context
* @returns Shadow root or null
*/
function useShadowRoot(): ShadowRoot | null;Usage Example:
import { defineCustomElement, useShadowRoot, onMounted } from "vue";
const CustomInput = defineCustomElement({
setup() {
const shadowRoot = useShadowRoot();
onMounted(() => {
if (shadowRoot) {
// Access shadow DOM
const input = shadowRoot.querySelector('input');
input?.focus();
}
});
return {};
},
template: `<input type="text" placeholder="Enter text...">`
});Access the host custom element from within the component.
/**
* Get access to the host custom element
* @returns Host element or null
*/
function useHost(): Element | null;Usage Example:
import { defineCustomElement, useHost, onMounted } from "vue";
const HostAwareComponent = defineCustomElement({
setup() {
const host = useHost();
onMounted(() => {
if (host) {
// Access host element attributes
const theme = host.getAttribute('theme');
console.log('Host theme:', theme);
}
});
return {};
},
template: `<div>Component content</div>`
});Custom elements automatically map HTML attributes to component props with type conversion.
// Component props
const props = {
count: Number, // Mapped from "count" attribute
disabled: Boolean, // Mapped from "disabled" attribute
items: Array, // Mapped from "items" attribute (JSON)
config: Object // Mapped from "config" attribute (JSON)
};
// HTML usage
// <my-component
// count="42"
// disabled
// items='["a", "b", "c"]'
// config='{"theme": "dark"}'
// ></my-component>Custom elements emit standard DOM events that can be listened to with addEventListener.
const CustomButton = defineCustomElement({
emits: ['customClick', 'valueChange'],
setup(props, { emit }) {
const handleClick = () => {
// Emits as 'customclick' DOM event
emit('customClick', { timestamp: Date.now() });
};
return { handleClick };
}
});
// HTML usage
// <my-button></my-button>
// <script>
// document.querySelector('my-button')
// .addEventListener('customclick', (e) => {
// console.log('Custom click:', e.detail);
// });
// </script>Custom elements support both scoped styles and global CSS custom properties.
const StyledComponent = defineCustomElement({
setup() {
return {
primaryColor: 'var(--primary-color, #007bff)'
};
},
template: `
<div class="container">
<button :style="{ color: primaryColor }">
Styled Button
</button>
</div>
`,
styles: [`
.container {
padding: 16px;
border: 1px solid var(--border-color, #ddd);
}
button {
background: var(--button-bg, #f8f9fa);
border: none;
padding: 8px 16px;
cursor: pointer;
}
`]
});interface CustomElementConstructor {
new (...args: any[]): HTMLElement;
}
interface CustomElementOptions {
/**
* Styles to inject into shadow root
*/
styles?: string[];
/**
* Whether to use shadow root (default: true)
*/
shadowRoot?: boolean;
/**
* Nonce for CSP compliance
*/
nonce?: string;
/**
* Configure the Vue app instance
*/
configureApp?: (app: App) => void;
}
/**
* Vue Element instance interface
*/
interface VueElementInstance extends HTMLElement {
_instance: ComponentInternalInstance | null;
_styles?: HTMLStyleElement[];
}