Quick Start
Kitsune is a modern framework for web-native applications: web components, presentation-only UI, explicit event boundaries, generalized integration layers, and local memory. It is designed for AI-composable product interfaces: surfaces an agent can assemble from meaningful primitives without having to rediscover the application's wiring every time.
The Problem
AI-composable UI needs architecture more than hand-authored UI does.
Modern frontend apps look modular at the component level. They are not modular where it matters: behavior, integrations, local state, accessibility expectations, content boundaries, product context, and event contracts.
Open any production codebase and follow a single button. It imports an analytics SDK, a feature-flag client, a logger, a session-replay tag, a permissions helper, a notification service, and a feature-specific tracking helper that was added the last time legal asked for an audit trail. The button is now coupled to every product capability the company has ever shipped.
Vendor code leaks into UI. Whatever the vendor's docs example showed is now inside your components. Swap PostHog for Statsig, Sentry for Datadog, GA4 for Mixpanel — and you're editing buttons.
Context lives nowhere. "This click happened on the donation form, on the campaign page, for campaignabc123" is information the analytics module needs, but nothing in the DOM expresses it. Every page reconstructs it by hand and gets it slightly differently each time.
Form, dialog, popover, focus, ARIA — reinvented in every framework. The browser ships them. Frameworks wrap them. Teams style the wrappers. Accessibility regresses. Test surface grows.
The framework owns the app. Switching renderers means rewriting the app. Adopting a new SDK means touching components. The thing that should be most durable — your product's behavior — is the thing most tangled with third-party choices.
The result: a modern frontend codebase is a component library draped over a hairball of vendor integrations.
The Workarounds Don't Scale
Teams notice the tangle and try to clean it up.
Context providers. Wrap the app in anAnalyticsProvider, a LoggerProvider, a PermissionsProvider. Now every component imports a hook to read the provider. The coupling moved behind a layer but didn't go anywhere. Test setup grows.
Higher-order components and decorators.withTracking(Button), withAudit(Form),withPermissions(Route). Each capability gets a wrapper. Composing them creates JSX caterpillars. The capability is still framework-specific.
Custom event buses. A team realizes capabilities should observe rather than wrap. Someone writes a `useEventBus`. It works. Two years later it has 14 event types, no discipline about names, no boundary context, and every consumer has to remember the exact shape of every payload.
Yet another design system. Lift the components out, ship them in a separate package, version them, document them. Now the company has a button library. The vendor leak is still inside the buttons.
The workarounds work in the small. They fall over when the app gets large enough to have real product surface area, multiple teams, and a turnover in the vendor stack. They all try to fix the symptom — coupling — without addressing the cause: the app has no architectural layer where capabilities can live separately from the UI that triggers them.
What Kitsune Does
Kitsune adds the missing layer.
Components describe what they are and what happened. Boundaries describe where it happened. Modules decide what to do about it. A small runtime in the middle wires the three together.
<kit-shell name="designer">
<kit-boundary surface="token-editor" feature="design-system"
entity-type="token" entity-id="color.primary">
<kit-button meta-event="token.saved" meta-intent="primary-action">
Save token
</kit-button>
</kit-boundary>
</kit-shell>The button doesn't know about analytics, audit, storage, or notifications. It declares a fact: token.saved. The boundary supplies context: this happened inside thetoken-editor surface of thedesign-system feature, for thecolor.primary token. The runtime normalizes the event. Modules — analytics, observability, audit, storage — decide what they care about.
Nothing in the UI imports a vendor SDK. Swap modules without touching components. Add a new capability by installing one more module. The button never changes.
That is the AI-composable interface answer: give the model primitives with explicit meaning, context, and consequences, then let the runtime handle the application contract.
Components emit meaning. Modules handle consequences. The browser does the rest.
The Core Loop
The whole framework runs one loop:
User interacts with UI
↓
Web-native component handles local behavior
↓
Component or DOM metadata describes intent
↓
Boundary enriches the interaction with context
↓
Runtime normalizes it into an event or command
↓
Modules observe events or handle commands
↓
Modules update state, call services, record telemetry,
enforce policy, or request UI changes
↓
Application state/context changes
↓
UI updates
↓
The loop continuesThat's it. No more layers to learn. Once you can read this loop you can read any Kitsune application.
Web-Native by Default
The browser is already an application platform. Frameworks spend most of their bundle pretending it isn't.
Kitsune uses the platform directly:
Custom elements for components — native registration, native lifecycle, native upgrade behavior, no VDOM tax.
Attributes as a declarative protocol —meta-event, meta-command,meta-surface, meta-intent. Server- rendered HTML can participate without any JavaScript on the client until needed.
Composed events for communication — events cross shadow boundaries naturally, no prop drilling, no global event emitter to maintain.
Forms for input — labels, constraints, submit semantics, validation states, keyboard behavior, accessibility expectations. All free.
Shadow DOM & slots for composition — real style encapsulation, real content projection, no CSS-in-JS runtime.
CSS as a runtime — custom properties, cascade layers, container queries, state selectors. The DOM adapts to context without JavaScript orchestration.
ARIA for semantics — the accessibility tree is the application tree. Screen readers, automation tools, and tests all read from the same source.
Web-native does not mean low-level. Kitsune uses Lit for the authoring layer, so components are short, typed, and reactive. The runtime stays framework-neutral underneath.
Boundaries: Context as a Primitive
A click isn't enough information to be useful. A click inside the donation form on the campaign page for campaignabc123 is meaningful.
Kitsune makes that context a first-class primitive. Akit-boundary wraps a page, surface, modal, form, widget, table, or card and supplies semantic context to every interaction inside it.
<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>The emitted event arrives at the runtime fully contextualized:
{
type: 'donation.started',
context: {
surfaces: ['campaign-page', 'donation-form'],
surface: 'donation-form',
feature: 'donations',
entity: { type: 'campaign', id: 'abc123' }
}
}No component had to know any of that. The DOM treeis the context graph. The boundary is just a way of declaring it.
Events vs. Commands
Kitsune separates two things most apps conflate: facts and requests.
Events are facts. Something happened.token.saved, checkout.started,form.validation_failed,route.changed. Many modules may care. The event is broadcast; observation is decoupled from emission.
Commands are requests. Do something.dialog.open, notification.show,draft.save, form.validate. One handler answers. Commands return results.
const result = await runtime.command({
type: 'form.validate',
payload: { formId: 'profile-form' },
})
if (result.ok) {
// continue
}Keeping the two separate keeps causality understandable. A module observing events never accidentally drives behavior. A handler responding to a command always answers an explicit question. The runtime stays predictable as the app grows.
Capability Modules
Product capabilities are installed, not imported.
const app = createKitShell({
name: 'designer',
modules: [
debugModule(),
notificationModule(),
dialogModule(),
storageModule({ backend: 'localStorage' }),
analyticsModule({ vendor: 'posthog', key: env.POSTHOG_KEY }),
observabilityModule({ vendor: 'sentry' }),
permissionsModule({ source: 'session' }),
],
})
app.mount(document.getElementById('app')!)Each module declares which events it observes and which commands it handles. Names follow subject.verb:storage.write, analytics.track,permission.check. The runtime keeps the wiring.
Storage persists drafts, preferences, and session state. Analytics tracks product events without vendor specifics in your UI.Observability captures errors, breadcrumbs, and performance hints. Permissions gates UI and behavior on the current user's authority.Notifications and dialogsship in the core. New capabilities — feature flags, experiments, audit, session replay — slot in the same way.
Modules don't import each other. They communicate through events, commands, and provider tokens. Installation order doesn't matter. Failure is local: a module that throws doesn't take down the runtime, and the error becomes a diagnostic the next module can observe.
Accessible Components, Built on Lit
Kitsune ships a small library of web-native components authored in Lit. They are not a design system; they are the accessible primitives every Kitsune app starts from.
kit-button — button semantics, loading state, intent attribute, native ARIA. Emitsmeta-event declared on the element.
kit-field — label, control, hint, error, and validation state in one accessible bundle. Works inside native <form>.
kit-dialog — built on the native <dialog> element. Focus trap, backdrop, escape-to-close. Driven by the dialog module.
kit-card — semantic container with optional header, footer, and intent. Pure composition, no internal logic.
kit-toast-region — accessible live region for notifications. Driven by the notification module. Toasts appear from commands, not from imports.
Each component is tested in real browsers via Vitest browser mode and documented in Storybook. They render the same in every framework adapter because they're custom elements.
The Metadata Bridge
The metadata bridge is the protocol that turns ordinary DOM into runtime events. Three styles, one shape.
Plain DOM metadata — works on anything, including server-rendered HTML before any JavaScript has parsed:
<button
data-meta-event="checkout.started"
data-meta-intent="primary-action"
data-meta-prop-location="hero"
>
Start checkout
</button>Custom element metadata — the same protocol on a Kitsune component:
<kit-button
meta-event="checkout.started"
meta-intent="primary-action"
meta-prop-location="hero"
>
Start checkout
</kit-button>Composed custom events — for components that emit events directly:
this.dispatchEvent(new CustomEvent('meta:event', {
bubbles: true,
composed: true,
detail: {
type: 'checkout.started',
payload: { cartId: 'cart_123' },
},
}))The bridge listens at the boundary root, uses event delegation, and enriches each interaction with the surrounding boundary context. No per-button wiring. No framework lock-in. Plain DOM, Lit components, and framework-rendered DOM all speak the same protocol.
Framework Adapters
The runtime is framework-neutral. The shell and boundary ship as custom elements that work anywhere the browser works. Framework adapters let the runtime feel idiomatic inside React, Vue, Svelte, or whatever comes next.
The React adapter ships today:
<KitShellProvider modules={[
analyticsModule(),
notificationModule(),
]}>
<KitBoundary surface="checkout" feature="payments">
<CheckoutForm />
</KitBoundary>
</KitShellProvider>function SaveButton() {
const emit = useKitEmit()
return (
<button onClick={() => emit({ type: 'token.saved' })}>
Save
</button>
)
}Every adapter exposes the same five primitives —KitShellProvider, KitBoundary,useKitRuntime, useKitEmit,useKitCommand — in framework idioms. SSR works: the runtime runs on the server with memory-backed storage and no-op vendors, and the client hydrates the same runtime instance.
Adapters bind to the runtime. They don't ship framework versions of the components — the Kitsune custom elements render natively in every framework that renders DOM. Vue, Svelte, Solid, and Nuxt adapters follow the same shape.
Themed by Tokens
Kitsune's theme layer is CSS, fully. Custom properties, cascade layers, container queries, state selectors. No CSS-in-JS runtime, no class generator, no proprietary token format.
@layer kit.tokens, kit.base, kit.components, app;
@layer kit.tokens {
:root {
--kit-color-bg: oklch(98% 0.01 80);
--kit-color-text: oklch(20% 0.02 80);
--kit-radius-md: 0.5rem;
--kit-space-md: 1rem;
}
}Components consume tokens. Themes override tokens. Container queries adapt them to context. The same component renders differently in a sidebar than in a hero without a single prop changing — the CSS does the work.
For teams that already have a design system, the theme layer is the integration point. Drop your tokens into thekit.tokens layer and every Kitsune component inherits them. No re-skinning. No prop translation. No wrapper components.
Kitsune pairs naturally with the Designer workspace, which generates these tokens from musical- theory ratios — but it works with any token source.
Get Started
pnpm add @atheory-ai/kitsune-core \
@atheory-ai/kitsune-ui \
@atheory-ai/kitsune-theme<script type="module">
import { createKitShell } from '@atheory-ai/kitsune-core'
import { notificationModule, dialogModule } from '@atheory-ai/kitsune-ui'
const app = createKitShell({
name: 'my-app',
modules: [notificationModule(), dialogModule()],
})
app.mount(document.getElementById('app'))
</script>
<div id="app">
<kit-shell name="my-app">
<kit-boundary surface="home">
<kit-button meta-event="hello.world">Hello</kit-button>
</kit-boundary>
</kit-shell>
</div>For React, install @atheory-ai/kitsune-react and wrap your tree in KitShellProvider. Adapters for other frameworks follow the same five-primitive shape.
Or clone the monorepo from GitHub — it ships with Storybook, Vitest browser tests, Playwright acceptance suites, and a full reference application.
Philosophy
The browser solved most of what we keep re-solving in JavaScript. Custom elements, events, forms, focus, ARIA, CSS, slots — each is a small, well-designed system that survives renderer fashions, framework churn, and vendor swaps. A frontend architecture that respects those systems outlives any one framework.
The part the browser doesn't provide is an application layer where product capabilities can live separately from the UI that triggers them. Kitsune adds exactly that layer — and nothing more. A runtime. Modules. Boundaries. A metadata bridge. Accessible components built on the platform.
The point isn't to be smaller than React or faster than Solid. The point is to make the durable part of your app — the product behavior — durable. Components describe what they are and what happened. Modules decide what to do. The framework gets out of the way.