Astro
A custom element built with elements-kit is just class extends HTMLElement registered via customElements.define. Astro renders HTML natively and custom elements work without any configuration β no isCustomElement option, no schema allowlist. The main integration point is registering your elements client-side and getting typed DOM references in TypeScript.
// 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);Register elements client-side
Because elements-kit touches the DOM, the module that calls defineElement must run in the browser. Import it from a <script> tag or a client:* island β not from the Astro component frontmatter:
---// x-counter.astro β server-only frontmatter, keep it clean---
<x-counter></x-counter>
<script> // Runs in the browser β safe to import DOM code here import "./x-counter";</script>The environment guard (isBrowser) is only needed for module-level reads of window / document outside of event handlers or lifecycle callbacks.
Source the prop shape from the class
Two helpers exported from elements-kit/jsx-runtime:
| Helper | Shape | Use when |
|---|---|---|
InstanceProps<I> | Public instance fields only (drops the HTMLElement surface) | Annotating setters, wrapper props, or helper functions |
ElementProps<C> | Full elements-kit JSX surface β attrs, fields, events from static events, slots from [SLOTS], children | When you need the full declared surface |
Typed document.querySelector and createElement
Add HTMLElementTagNameMap augmentations to src/env.d.ts (Astroβs ambient type file) so document.querySelector is typed across the whole project:
/// <reference types="astro/client" />
import type { XCounter } from "./x-counter";
declare global { interface HTMLElementTagNameMap { "x-counter": XCounter; }}Then anywhere in client-side scripts:
const el = document.querySelector("x-counter"); // XCounter | nullIslands (React, Solid, Vue)
If youβre using framework islands (client:load, client:visible, etc.) and want typed props when you render a custom element inside a React, Solid, or Vue component, add the JSX IntrinsicElements augmentation for that framework alongside the class definition and import it from the island component file: