Custom Elements
A custom element is a class that extends HTMLElement, registered with a tag name. The browser instantiates it whenever the parser encounters that tag.
class GreenSquare extends HTMLElement { connectedCallback() { this.style.background = 'limegreen' this.style.inlineSize = '40px' this.style.blockSize = '40px' this.style.display = 'inline-block' }}customElements.define('green-square', GreenSquare)<green-square></green-square>That’s a component. It’s in the browser. There is no framework, no build step, and the entire definition is 30 lines you can read.
What custom elements give you for free
Section titled “What custom elements give you for free”Lifecycle. connectedCallback, disconnectedCallback, attributeChangedCallback, adoptedCallback. The browser calls these when the element joins the DOM, leaves it, has an observed attribute change, or moves between documents. No framework needed.
Encapsulation. Attach a shadow root and your styles can’t leak out, parent styles can’t leak in. Slot composition works the way it does in HTML — nest content, project it, style slotted content with ::slotted().
class GreenCard extends HTMLElement { constructor() { super() this.attachShadow({ mode: 'open' }).innerHTML = ` <style> :host { display: block; padding: 1rem; border: 1px solid limegreen; } ::slotted(h2) { color: limegreen; margin: 0 0 0.5rem; } </style> <slot></slot> ` }}customElements.define('green-card', GreenCard)<green-card> <h2>This is green</h2> <p>The h2 above is colored without ever knowing it's inside a card.</p></green-card>Upgrades. When the parser meets <green-card>, the browser will upgrade it the moment its definition arrives — even if the element was on the page before the script loaded. This is why custom elements have no hydration step. The element is the component.
Composition with native elements. A custom element can wrap, extend, or be a real native element. <kit-button> puts a real <button> inside. <kit-dialog> puts a real <dialog> inside. You inherit the platform’s keyboard handling, focus handling, form participation, screen reader semantics, and constraint validation — for free.
What Lit adds
Section titled “What Lit adds”You can write custom elements in plain JS and many people do. Lit is the smallest reasonable abstraction on top of the platform’s component model. It gives you:
- Decorator-based reactive properties:
@property() name = 'world're-renders when assigned. - Tagged templates that compile to efficient DOM updates:
html`<p>${this.name}</p>`. - A way to express conditional and list rendering without JSX or virtual DOM diffing.
- Roughly 6 KB gzipped.
import { LitElement, html } from 'lit'import { property } from 'lit/decorators.js'
export class GreenCounter extends LitElement { @property({ type: Number }) count = 0
render() { return html` <p>Count: ${this.count}</p> <button @click=${() => this.count++}>+</button> ` }}customElements.define('green-counter', GreenCounter)You write the component. The browser runs it. There’s no virtual DOM. There’s no reconciler. There’s no hydration step.
Where Kitsune fits
Section titled “Where Kitsune fits”Kitsune ships custom elements (built with Lit) for the application primitives that every app needs and none of them should have to write:
<kit-shell>— the application root, owns the runtime<kit-boundary>— a semantic context zone (surface, feature, entity)<kit-button>,<kit-dialog>,<kit-toast-region>,<kit-field>,<kit-card>— accessible UI primitives- More to follow
You’re free to write your own custom elements alongside them. Kitsune doesn’t own your component vocabulary; it owns the coordination vocabulary.
A concrete demo
Section titled “A concrete demo”Save this as an HTML file. Open it. Click the button.
<!doctype html><script type="module"> import { LitElement, html } from 'https://esm.sh/lit'
class TodoItem extends LitElement { static properties = { done: { type: Boolean, reflect: true } } constructor() { super(); this.done = false } render() { return html` <label> <input type="checkbox" .checked=${this.done} @change=${(e) => this.done = e.target.checked} /> <slot></slot> </label> ` } } customElements.define('todo-item', TodoItem)</script>
<todo-item>Write a frontend without React</todo-item><todo-item done>Survive doing it</todo-item>Two custom elements, in plain HTML, talking to the platform. No build step. Save them, ship them, run them in 2034.