Skip to content

Boundaries

A boundary is a region of the DOM that has meaning to the application. Not just “this is a card” — that’s a component concern. But “this is the donation form on the campaign page for campaign abc123” — that’s product context.

<kit-boundary surface="campaign-page" feature="donations">
<kit-boundary surface="donation-form" entity-type="campaign" entity-id="abc123">
<kit-button meta-event="donation.started">Donate</kit-button>
</kit-boundary>
</kit-boundary>

A click on Donate produces:

{
type: 'donation.started',
context: {
surfaces: ['campaign-page', 'donation-form'],
surface: 'donation-form',
feature: 'donations',
entity: { type: 'campaign', id: 'abc123' }
}
}

The button doesn’t know any of those values. It just declares its event. The boundary does the rest.

The four primary fields are conventional but not magical — you can add your own:

AttributeMeaning
surfaceThe visible region of UI (“workspace”, “note-editor”, “settings-page”). Stacks into surfaces.
featureThe product area (“notes”, “billing”, “design-system”). Inherited from parent if not redeclared.
entity-type + entity-idThe current entity, becomes entity: { type, id }. Inherited if not redeclared.

You can also add arbitrary context via the boundary’s runtime API:

const boundary = runtime.createBoundary({
context: {
surface: 'editor',
workspace: 'designs',
permissions: ['note:write'],
},
})

Any keys are kept as-is and propagated.

Each surface adds to a stack. The deepest one is the current surface. The full list is in surfaces.

<kit-boundary surface="app">
<kit-boundary surface="workspace">
<kit-boundary surface="note-editor">
<kit-button meta-event="note.saved">Save</kit-button>
</kit-boundary>
</kit-boundary>
</kit-boundary>

Event context:

{
surface: 'note-editor',
surfaces: ['app', 'workspace', 'note-editor'],
}

This lets analytics or audit modules ask either question:

  • What is the user looking at right now?surface
  • What’s the path that got them here?surfaces

feature and entity cascade through the DOM tree. A child boundary that doesn’t redeclare them keeps the parent’s.

<kit-boundary surface="workspace" feature="notes">
<kit-boundary surface="note-list">
<!-- still feature: notes -->
<kit-boundary surface="note-card" entity-type="note" entity-id="42">
<!-- feature: notes, entity: note/42 -->
</kit-boundary>
</kit-boundary>
</kit-boundary>

Redeclaring overrides:

<kit-boundary feature="notes">
<kit-boundary feature="billing">
<!-- feature: billing now -->
</kit-boundary>
</kit-boundary>

A useful test: would I want this context attached to events that happen here?

Add a boundary at:

  • The application root (the <kit-shell> itself usually carries one).
  • Each route or page.
  • Each major surface (sidebar, main, dialog).
  • Each form or widget that handles its own entity.
  • Each list item that represents an entity (so events like note.opened carry which note).

Don’t add a boundary for:

  • Layout-only wrappers.
  • Components that don’t change product context.
  • Performance reasons (boundaries are cheap, but the meaning of a redundant boundary is confusing).

A few well-placed boundaries beat many low-information ones.

If you’re coming from React: a boundary is like a context provider, but it’s a real DOM element. That has consequences:

  • Boundaries are visible in DevTools.
  • Boundaries are inspectable with document.querySelectorAll('kit-boundary').
  • Boundaries are styleable with [surface=note-editor] { ... }.
  • Boundaries don’t trigger framework re-renders when their context changes — there’s no virtual DOM.
  • Boundaries cross framework lines: a Lit component and a React component inside the same boundary see the same context.

The trade-off: you can’t supply context to every descendant the way Context’s value flows through render. You can to the descendants that emit events or commands inside the boundary, which is the relevant case.

The React adapter’s <KitBoundary> works the same way:

<KitBoundary surface="note-editor" entityType="note" entityId="42">
<button data-meta-event="note.saved">Save</button>
</KitBoundary>

Plain HTML buttons with data-meta-* attributes participate. So do <kit-button> web components inside React. Both produce the same event.