Skip to content

Metadata Protocol

The metadata protocol is the bridge from DOM to runtime. The boundary listens at its host element, parses metadata from the click target, and emits events or dispatches commands accordingly.

AttributeOn a custom elementOn plain HTMLMaps to
Event typemeta-eventdata-meta-eventevent.type
Command typemeta-commanddata-meta-commandcommand.type
Payload valuemeta-prop-<key>data-meta-prop-<key>payload[<camelCaseKey>]
Payload objectmeta-payloaddata-meta-payloadJSON object merged into payload
Field change eventmeta-event-on-changedata-meta-event-on-changeevent emitted on native change

Custom elements may use the cleaner non-prefixed form. Plain HTML elements should use data- to comply with HTML’s reserved attribute namespace.

<kit-button meta-event="checkout.started">Pay</kit-button>

Click produces:

runtime.emit({
type: 'checkout.started',
context: <from boundary>,
payload: {},
source: { tagName: 'kit-button' },
})
<kit-button meta-command="dialog.open" meta-prop-target="confirm">
Open confirm
</kit-button>

Click produces:

runtime.command({
type: 'dialog.open',
context: <from boundary>,
payload: { target: 'confirm' },
source: { tagName: 'kit-button' },
})
<kit-button
meta-event="settings.saved"
meta-command="dialog.close"
meta-prop-target="settings-dialog"
>
Save
</kit-button>

Click produces both: the event first, then the command. Both share the same payload.

<button
data-meta-event="comment.posted"
data-meta-prop-thread-id="t_42"
data-meta-prop-character-count="120"
>
Post
</button>

Same shape, with the data- prefix.

  • Keys are kebab-case in the attribute, camelCase in the payload.

    <button data-meta-prop-cart-id="c_42" data-meta-prop-line-items="3">...</button>

    Becomes:

    payload: { cartId: 'c_42', lineItems: '3' }
  • meta-prop-* values are always strings. Attributes can’t carry numbers, booleans, or objects natively. If you need typed payload values, use meta-payload:

    <button data-meta-event="cart.update" data-meta-payload='{"itemCount":3,"isGift":true}'>...</button>
  • meta-prop-* overrides meta-payload. data-meta-payload='{"cartId":"a"}' data-meta-prop-cart-id="b" produces payload: { cartId: 'b' }.

When a click reaches a <kit-boundary>:

  1. The boundary walks event.composedPath() looking for the first element with meta-event, data-meta-event, meta-command, or data-meta-command.
  2. It reads all meta-event, meta-command, and meta-prop-* (and the data- variants) from that element.
  3. It reads its own context (surface, feature, entity) plus any inherited ancestor context.
  4. It calls parseMetadata(element, context) and dispatches.

If no metadata is found in the path, nothing happens.

The boundary handles more than clicks:

  • Composed meta:event — any element can dispatch new CustomEvent('meta:event', { bubbles: true, composed: true, detail: { type, payload } }).
  • Form submit<form data-meta-event="thing.submitted"> emits on native submit and includes payload.values.
  • Field change — fields with data-meta-event-on-change="field.changed" emit on native change and include name / value when available.
  • Attributes on parent elements other than payload props.
  • Stale metadata — attributes are read at dispatch time, not at render time.

If you need to parse metadata outside the boundary:

import { parseMetadata } from '@atheory-ai/kitsune-app'
const element = document.querySelector('button[data-meta-event]')!
const metadata = parseMetadata(element, { surface: 'page' })
// metadata.event and metadata.command are now KitEventInput / KitCommandInput shapes

Or for the raw read without normalization:

import { readDomMetadata } from '@atheory-ai/kitsune-app'
const raw = readDomMetadata(element, { surface: 'page' })
// { eventType, commandType, context, payload, source }

Every dispatched event/command includes a source object that the metadata layer fills in:

source: { tagName: 'kit-button' }

This is useful for diagnostics and for filtering in modules. Future versions may add more fields (e.g., the element’s id, computed accessible name).

The protocol is additive. Existing markup keeps working as new metadata channels are introduced.