Skip to content

The Metadata Protocol

The metadata protocol is how DOM elements declare what they mean to the application. The boundary parses these declarations and feeds them to the runtime.

There are three flavors. They produce the same runtime shape.

For raw HTML, server-rendered pages, framework-rendered DOM, or anywhere you don’t have custom elements:

<button
data-meta-event="checkout.started"
data-meta-command="analytics.flush"
data-meta-prop-location="hero"
data-meta-prop-cart-id="cart_123"
>
Start checkout
</button>

The data- prefix is required for ordinary HTML elements (it’s the prefix HTML reserves for app-specific attributes). React, Vue, Svelte, plain HTML — anything that renders elements — can participate.

For Kitsune custom elements, drop the data- prefix:

<kit-button
meta-event="checkout.started"
meta-command="analytics.flush"
meta-prop-location="hero"
meta-prop-cart-id="cart_123"
>
Start checkout
</kit-button>

Custom element tags can carry whatever attributes they like, so the cleaner form is allowed.

AttributeMaps to
meta-event / data-meta-eventevent.type
meta-command / data-meta-commandcommand.type
meta-payload / data-meta-payloadJSON object merged into payload
meta-prop-<key> / data-meta-prop-<key>payload[<key>] (camelCased)

Property names are kebab-case in the attribute and camelCase in the payload:

<button data-meta-prop-cart-id="cart_123">...</button>

Becomes:

{ payload: { cartId: 'cart_123' } }

For typed payloads, use JSON:

<button
data-meta-event="checkout.started"
data-meta-payload='{"lineItems":3,"gift":true}'
data-meta-prop-cart-id="cart_123"
>
Start checkout
</button>

meta-prop-* values override keys from meta-payload, so the final payload is:

{ cartId: 'cart_123', lineItems: 3, gift: true }

Each <kit-boundary> has a delegated click listener. When a click fires:

  1. Walk the click’s composedPath() looking for the nearest element with metadata.
  2. If found, parse:
    • meta-event → emit an event of that type
    • meta-command → dispatch a command of that type
    • meta-prop-* → fold into the payload
  3. Add the boundary’s accumulated context.
  4. Add a source: { tagName: 'kit-button' } for diagnostics.
  5. Emit the event (if any) first, then dispatch the command (if any).

If both meta-event and meta-command are present on the same element, both fire. Event first.

Anything that’s a click target. Most commonly:

  • <kit-button> and built-in <button>
  • <kit-card interactive> (for clickable cards)
  • <kit-disclosure> (when added)
  • Plain <a>, <div>, etc. as long as they receive a click

If the element doesn’t normally take focus or receive keyboard activation, you’ll want to add tabindex="0" and role for accessibility — or use a real <button>.

The boundary also handles:

  • Native form submit when the form has meta-event / data-meta-event or meta-command / data-meta-command.
  • Native field change when the field has meta-event-on-change / data-meta-event-on-change.
  • Composed meta:event custom events for programmatic dispatch from inside any element.

Programmatic dispatch from inside a custom element uses a composed event:

this.dispatchEvent(new CustomEvent('meta:event', {
bubbles: true,
composed: true,
detail: {
type: 'editor.cursor_moved',
payload: { line: 42 },
},
}))

The nearest boundary adds its context and emits the runtime event.

Three reasons declarative attributes beat imperative onClick={() => emit(...)}:

It keeps UI components free of runtime imports. A <kit-button> doesn’t import the runtime to declare its meaning.

It works across rendering systems. Plain HTML, Lit, React, Vue, server-rendered HTML — they all produce DOM. The DOM has attributes. The boundary parses them.

It’s inspectable. Open DevTools, see what an element means. No need to find the imperative handler in the source.

  • meta-prop-* payloads are strings. Use meta-payload for JSON values and meta-prop-* for simple string overrides.
  • Metadata is captured at click. Setting metadata after a click doesn’t replay. Set it before.
  • Boundaries delegate at their root. If a click is stopPropagation’d below the boundary, metadata won’t be parsed. Avoid stopPropagation in click handlers unless intentional.