Vue

Use elements-kit custom elements in Vue templates with typed props via GlobalComponents.

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 anywhere
import { 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:

// vite.config.ts
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.

HelperShapeUse when
PropertiesOf<typeof XCounter>Public instance fields, all optional — the HTMLElement surface is dropped; Slot-typed fields accept a NodeTyping property assignments — prop bindings, wrappers, helper functions
AttributesOf<typeof XCounter>Every key of static [ATTRIBUTES], valued string | nullTyping the raw HTML attribute surface — setAttribute, server-rendered markup
EventsOf<typeof XCounter>The static events map, verbatim — event name to event typeTyping 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" /> — typed

Co-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.

// x-counter.ts
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 | null

Worth doing alongside the Vue augmentation.


See also