Skip to content

Svelte

A custom element built with elements-kit is just class extends HTMLElement registered via customElements.define. Svelte renders it fine at runtime in both Svelte 4 and Svelte 5 (runes mode), but you need a small augmentation to get typed props, narrowed bind:this refs, and editor autocomplete.

// 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);

Source the prop shape from the class

Two helpers exported from elements-kit/jsx-runtime:

HelperShapeUse when
InstanceProps<I>Public instance fields only (drops the HTMLElement surface)You want to type just the user-defined props
ElementProps<C>Full elements-kit JSX surface — attrs, fields, events from static events, slots from [SLOTS], childrenYour element declares typed events or slots and you want callers to see them

For most Svelte wiring InstanceProps<XCounter> is enough.

Augment Svelte’s SvelteHTMLElements

import type { HTMLAttributes } from "svelte/elements";
import type { InstanceProps } from "elements-kit/jsx-runtime";
import type { XCounter } from "./x-counter";
declare module "svelte/elements" {
export interface SvelteHTMLElements {
"x-counter": HTMLAttributes<XCounter> & InstanceProps<XCounter>;
}
}
// <x-counter count={5} bind:this={el} /> — el: XCounter

HTMLAttributes<XCounter> provides the standard HTML attribute surface (class, id, event handlers, …) and ensures bind:this resolves to XCounter.

Co-locate the augmentation with the class

Put the declare module "svelte/elements" 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 } from "elements-kit/custom-elements";
import { reactive } from "elements-kit/signals";
import type { HTMLAttributes } from "svelte/elements";
import type { InstanceProps } from "elements-kit/jsx-runtime";
export class XCounter extends HTMLElement {
@reactive() count = 0;
}
defineElement("x-counter", XCounter);
declare module "svelte/elements" {
export interface SvelteHTMLElements {
"x-counter": HTMLAttributes<XCounter> & InstanceProps<XCounter>;
}
}

Make sure the file is referenced in your tsconfig.json’s include array so TypeScript picks up the augmentation.

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 Svelte augmentation.


See also