AetherUI: From Component Library to Public Release
I published AetherUI, a headless Web Component library with 25 core components and a validated path for agent-generated UI. Here is what the launch made me solve.
When I prepared AetherUI for its first npm release, I hit a problem that had nothing to do with components: the scope I had built the packages around was unavailable. Six packages, their internal imports, examples, docs, and release checks pointed at a name I could not publish. I moved them to the @aetherui-kit scope while keeping the AetherUI name and the ae- tag prefix. The repository passed its checks, but that did not tell me whether anyone could install the result. I installed the registry versions in a clean project and ran the consumer suite against those artifacts.
That is the boundary a library has to survive. A green source tree says little about the package people actually install. AetherUI is now public on GitHub and published on npm. Here is what I built, and what it took to get from a working repository to a usable release.
The library has two audiences
For application developers
AetherUI provides application developers with 25 core components built with Lit, including buttons, forms, overlays, navigation, and feedback. They are custom elements, so the same tags can be used from plain HTML or a framework application. Components expose properties, events, slots, CSS parts, and design tokens as their public interface. The library offers light and dark themes and leaves room to style the elements yourself.
Headless describes the separation between behavior and appearance here. Import @aetherui-kit/tokens/light.css or dark.css for a ready-made theme, then override its tokens where you need to. For complete control, leave out the theme CSS, set unstyled on a component, and style its exposed CSS parts. The component still provides its structure and interaction. The theming guide covers both approaches.
For an application developer, registration is explicit and idempotent. Import the component, then use its tag in markup:
import { defineAeButton } from '@aetherui-kit/core';
import '@aetherui-kit/tokens/light.css';
defineAeButton();<ae-button variant="primary">
Save changes
</ae-button>You can start with npm install @aetherui-kit/core @aetherui-kit/tokens, then inspect the component docs or try the playground. A standalone data table, accordion package, agent renderer, and MCP server are also published under the same scope.
That browser-native interface also gives agent-generated UI a finite target: named custom elements with lifecycle and events, regardless of the host framework. Shadow DOM keeps internals contained, while slots, CSS parts, and the component catalog describe what the host can expose.
For agent hosts
Agent hosts need to understand the component library as data. AetherUI publishes a component catalog, an agent UI schema, and llms.txt alongside human-facing docs. The @aetherui-kit/mcp package exposes component discovery and UI validation to agent hosts. These references come from the component source and its annotations, so the machine-facing description follows the same interface that developers use.
That shared contract matters when a model produces UI. The @aetherui-kit/agent package validates a JSON document before rendering allowed components. For URL-valued properties, the default policy accepts http:, https:, mailto:, tel:, and relative paths; it rejects javascript: and network-path URLs such as //example.com. By default, the validator caps the document at 100 element or text nodes and component nesting at 12 levels, root included. Hosts can configure these limits and should bound JSON size before parsing it. On validation failure, renderAgentUi throws and leaves the existing surface untouched (validator, renderer). Actions are identifiers the host handles, rather than JavaScript supplied by the model.
The agent can target that same button by returning this JSON document:
{
"version": "1",
"root": {
"component": "ae-button",
"id": "save",
"props": { "variant": "primary" },
"children": ["Save"],
"actions": { "ae-button-click": "save-profile" }
}
}With that JSON in modelDocument and an existing DOM element in surface, the host wires the action to its own function:
import { defineAeButton } from '@aetherui-kit/core';
import { renderAgentUi } from '@aetherui-kit/agent';
defineAeButton();
renderAgentUi(surface, modelDocument, {
allowedComponents: ['ae-button'],
onAction({ actionId }) {
if (actionId === 'save-profile') saveProfile();
},
});The JSON renders an AetherUI Save button. When it emits ae-button-click, the host receives save-profile and calls its own saveProfile function. The model supplies no handler code. The host chooses its allowed components; invalid documents leave the existing surface untouched. See Generating UI safely for the full contract.
These examples initialize in the browser. renderAgentUi creates nodes with DOM APIs, and defineAeButton() registers the element in the browser; neither call produces server-rendered HTML. For server rendering, the React integration guide notes that declarative Shadow DOM needs extra Lit setup; it does not yet provide a Next.js or Remix integration recipe.
Shipping meant testing the boundary
A component test can prove that keyboard interaction, focus, or an event works in a browser. It cannot prove that a tarball includes the right files or that a fresh project can resolve its exports. The committed consumer smoke test packs the packages, installs them in a scratch application, then exercises public imports and browser behavior. A missing export would fail at the consumer build or import boundary even if tests against workspace source passed. Before publication I also ran cross-browser, Storybook, end-to-end, package, and audit checks. After publication I repeated the consumer checks against registry-installed versions in a clean project. That registry run verified the release artifacts; it is separate from the committed tarball test.
I also ran the published MCP executable and checked that it initialized and exposed its tools. That step sounds mundane until you realize how easy it is for a package to pass source tests but ship a broken binary path. The public artifact, rather than the workspace copy, is the final product.
There is one release detail I want to keep visible: the initial packages were published locally after reviewing their packed contents and completing npm authentication. Those first publications did not have CI provenance. Trusted publishing is now configured for subsequent releases through the publish workflow, but that does not retroactively change the first one. The first CI release, @aetherui-kit/tokens@0.1.1, now carries that provenance. The workflow also skips package versions that are already on npm, since npm will not accept the same version twice, so a tag publishes only the packages whose versions changed.
What I would keep from this launch
A design system's public interface is more than its TypeScript exports. It includes the tags people put in markup, the events they listen for, the tokens they override, and the accessibility behavior they rely on. For agent-generated UI it also includes the schema and validation rules that decide what can appear on screen. Those surfaces need to describe the same product.
The other lesson is about release confidence. Test the source, inspect the package, and then install what the registry serves. Each catches a different class of failure. The scope migration made that distinction concrete for me, and the consumer run is what let me call this a public release rather than a green build.
AetherUI is open source under MIT. If you are building an application, try a component in the playground. If you are building agent-generated UI, inspect the document schema and runtime validator. The repository has the examples and contribution guide.