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

```ts
// 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:

```ts
// 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](https://vuejs.org/guide/extras/web-components.html#using-custom-elements-in-vue) 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`

```ts
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.

```ts
// 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:

```ts
declare global {
  interface HTMLElementTagNameMap {
    "x-counter": XCounter;
  }
}

const el = document.querySelector("x-counter"); // XCounter | null
```

Worth doing alongside the Vue augmentation.

***

## See also

* [Custom Elements overview](/custom-elements)
* [React integration](/integrations/react)
* [Solid integration](/integrations/solid)
* [Angular integration](/integrations/angular)
* [Svelte integration](/integrations/svelte)
* [Lit integration](/integrations/lit)
* [Astro integration](/integrations/astro)
* [Marko integration](/integrations/marko)
* [Qwik integration](/integrations/qwik)
