Vue
A custom element built with elements-kit is just class extends HTMLElement registered via customElements.define. Vue renders it fine at runtime, but the Vue template compiler needs to be told the tag is a custom element, and the GlobalComponents interface needs to be augmented to get typed props in templates and SFCs.
// shared element — built once, used anywhereimport { defineElement } from "elements-kit/custom-elements";import { reactive } from "elements-kit/signals";
export class XCounter extends HTMLElement { @reactive() count = 0;}
defineElement("x-counter", XCounter);Tell the template compiler it’s a custom element
Vue treats unknown lowercase tags as components by default, which produces a warning for custom elements. Whitelist your tag prefix in the build config:
import vue from "@vitejs/plugin-vue";
export default { plugins: [ vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith("x-"), }, }, }), ],};See the Vue docs on using custom elements for the full set of options.
Source the prop shape from the class
Three type helpers exported from elements-kit/custom-elements read the raw platform surface off the class. All three take the constructor (typeof XCounter); PropertiesOf also accepts an instance type.
| Helper | Shape | Use when |
|---|---|---|
PropertiesOf<typeof XCounter> | Public instance fields, all optional — the HTMLElement surface is dropped; Slot-typed fields accept a Node | Typing property assignments — prop bindings, wrappers, helper functions |
AttributesOf<typeof XCounter> | Every key of static [ATTRIBUTES], valued string | null | Typing the raw HTML attribute surface — setAttribute, server-rendered markup |
EventsOf<typeof XCounter> | The static events map, verbatim — event name to event type | Typing addEventListener, framework event bindings, HTMLElementEventMap augmentations |
For most Vue wiring PropertiesOf<typeof XCounter> is enough.
Augment Vue’s GlobalComponents
import type { DefineComponent } from "vue";import type { PropertiesOf } from "elements-kit/custom-elements";import type { XCounter } from "./x-counter";
declare module "vue" { interface GlobalComponents { "x-counter": DefineComponent<PropertiesOf<typeof XCounter>>; }}
// <template> <x-counter :count="5" /> — typedCo-locate the augmentation with the class
Put the declare module "vue" block in the same file as the element class. Importing the class then brings the augmentation along — consumers don’t have to remember to write it themselves.
import { defineElement, type PropertiesOf } from "elements-kit/custom-elements";import { reactive } from "elements-kit/signals";import type { DefineComponent } from "vue";
export class XCounter extends HTMLElement { @reactive() count = 0;}
defineElement("x-counter", XCounter);
declare module "vue" { interface GlobalComponents { "x-counter": DefineComponent<PropertiesOf<typeof XCounter>>; }}Typed document.querySelector and createElement
Independent of any framework, augmenting the global HTMLElementTagNameMap gives you typed lookups in plain DOM code:
declare global { interface HTMLElementTagNameMap { "x-counter": XCounter; }}
const el = document.querySelector("x-counter"); // XCounter | nullWorth doing alongside the Vue augmentation.