
When an AI agent writes a React component, nothing checks whether the props it generated are actually valid. React never enforced that at runtime by default, and as of React 19, that's even more true: the old PropTypes escape hatch was removed from the package entirely, and passing the wrong type is now silently ignored rather than logging so much as a warning. That's a reasonable choice for React to make. Runtime checks cost something on every render, and TypeScript already catches most of this at compile time for the humans writing components by hand.
For an agent, though, most of the time nothing happens right away. The bad value just sits there until something downstream trips over it, an onClick that turns out to be a string instead of a function, a .map() call on something that isn't an array, and by the time that throws, the error points at the crash site, not the prop that caused it. I build enough with Claude Code and Cursor to hit this constantly: an agent generates a component, mounts it with the wrong prop shape, and either nothing visibly breaks or something breaks three components downstream from the actual mistake.
So I built Solar, a runtime UI framework where a bad prop always throws the same thing, immediately, at the point of the mistake: a structured error object with a fix field the agent can read and act on directly. That's the whole idea. Everything else in the framework exists to support it.
Three ways agent-written UI code breaks differently
AI-generated code fails differently than human-written code, and the difference matters more the larger a share of your codebase an agent is writing. It's correct file by file but inconsistent across files, because nothing enforces the conventions a human would pick up without being told. It doesn't respect conventions it can't see: a developer learns "this codebase always types onClick as a function" from reading the surrounding code, but a model regenerating a file from scratch has no such context unless the framework encodes it directly. And it gets regenerated wholesale far more often than it gets incrementally maintained. Internal tools built by an agent tend to get rebuilt when requirements change, not patched line by line, so mistakes that would get caught in code review during ordinary maintenance ship straight to the browser instead.
React, Vue, and Svelte were designed on the assumption that a human writes, reads, and maintains every file. That assumption is what's breaking. The frameworks aren't wrong for what they were built for.
Every other layer of the stack already solved this
APIs have OpenAPI and JSON Schema. Databases have schemas and constraints. Typed languages have compilers. Each one gives a machine a structured description of what's valid before something breaks, and a structured error after. UI frameworks never needed that, because a human was always in the loop to read whatever surfaced, a warning, a wrong render, an eventual crash, and connect it back to the actual mistake.
Solar treats "a model will read this API" as the primary design constraint, instead of retrofitting typed props onto a framework built for human authors. It's a from-scratch runtime, not a layer bolted onto React.
The build order gives away what mattered first. ContractError, the structured error format, came before anything else, because every other piece depends on having somewhere to report through. Then the component registry, so a model can discover what's valid before it generates code instead of finding out after it already guessed wrong. Then typed slot composition, which extends the same contract model to parent-child relationships. Then the compact h() array notation, once the registry existed to make a token-efficient generation target worth having. Errors first, discovery second, composition third, efficiency last.
How it works
Every component is an explicit contract
defineComponent declares a component's props as a schema, not a convention. Each prop gets a type, whether it's required, a default, and an optional enum restricting it to specific values. Props are validated at the component boundary before render, every time, not just in a dev build.
import { createElement, defineComponent, registry } from './framework/index.js'
const Button = defineComponent({
name: 'Button',
props: {
label: { type: 'string', required: true },
onClick: { type: 'function', required: true },
variant: { type: 'string', enum: ['primary', 'secondary'], default: 'primary' },
},
render({ label, onClick, variant }) {
return createElement('button', { class: variant, onclick: onClick }, label)
}
})
registry.register(Button)A structured error instead of a stack trace
Pass the wrong type and Solar throws a ContractError, a plain object with component, prop, expected, received, and a fix field written as an instruction, not a description.
Button({ label: 42, onClick: () => {} })
// throws ContractError:
{
"error": "ContractError",
"component": "Button",
"prop": "label",
"expected": "string",
"received": "number",
"fix": "Pass a string value for \"label\""
}An agent catches this, reads fix, and retries. No human relays the error back.
A manifest a model reads before it writes anything
Every component self-registers on import. registry.manifest() returns the full catalog as JSON, every component's name, props, types, enums, and defaults, in one call.
registry.manifest()
// →
[
{
"name": "Button",
"props": {
"label": { "type": "string", "required": true },
"onClick": { "type": "function", "required": true },
"variant": { "type": "string", "enum": ["primary", "secondary"], "default": "primary" }
}
},
]Feed this to an agent as system context and it never has to guess an API surface it hasn't seen. It reads the manifest, generates props against that schema, and only reaches ContractError if the schema changed underneath it.
Composition gets the same treatment
A slot prop restricts which component type can fill it. Passing a plain DOM node where a Button vnode is expected throws the same way a wrong string does, at the parent boundary, by name, instead of failing deep inside a render nobody's watching.
const Card = defineComponent({
name: 'Card',
props: {
title: { type: 'string', required: true },
action: { type: 'slot', accepts: 'Button', required: true },
},
render({ title, action }) {
return createElement('div', { class: 'card' },
createElement('h3', {}, title),
action,
)
}
})
// valid
Card({ title: 'Hello', action: Button({ label: 'Go', onClick: () => {} }) })
// throws: Card: prop "action": expected slot(Button), got vnode with no _source
Card({ title: 'Hello', action: createElement('button', {}, 'Go') })A compact notation built as a generation target
h() parses a dense array format into the same vnode tree createElement produces, and it's registry-aware, so a component name in the array resolves and dispatches automatically.
import { h } from './framework/index.js'
h(['Button', { label: 'Save', onClick: handleSave, variant: 'primary' }])
// resolves to the registered Button component, prop validation still runsThe array format reads closer to markup and is cheaper to produce than nested function calls, which is a deliberate design choice: h() exists as something a model can generate directly, not just something a human finds convenient.
Three narrow effect primitives instead of one general one
State uses useState, same shape as React. But instead of a single useEffect that tries to cover data fetching, subscriptions, and lifecycle at once (and is easy to misuse with a wrong or missing dependency array), Solar splits side effects into three purpose-built hooks: useResource for async data with automatic cancellation on key change, useSubscription for event listeners that attach and detach when their source, event, or handler changes, and onMount/onUnmount for lifecycle callbacks that run exactly once.
const UserCard = defineComponent({
name: 'UserCard',
props: { userId: { type: 'number', required: true } },
render({ userId }) {
const [width, setWidth] = useState(window.innerWidth)
const { data, loading, error } = useResource({
key: userId,
fetch: async (signal) => {
const res = await fetch(`/api/user/${userId}`, { signal })
return res.json()
},
})
useSubscription({ source: window, event: 'resize', handler: () => setWidth(window.innerWidth) })
onMount(() => analytics.track('UserCard mounted'))
onUnmount(() => analytics.track('UserCard unmounted'))
if (loading) return createElement('p', {}, 'Loading...')
return createElement('p', {}, `${data.name} - viewport: ${width}px`)
}
})Each hook does one thing, so there's one narrower way to get it wrong. A model that misuses useResource breaks async fetching and nothing else, instead of quietly breaking a subscription cleanup it never touched, the way a wrong dependency array can in a general useEffect.
No compiler, and a rigid file layout on purpose
Solar is runtime-based. It works from a CDN <script type="module"> tag in a plain HTML file, no bundler, no build step. npm create solarbuild@latest my-app scaffolds a full project if you want one. Components live one per file, filename matches component name, default export is always the defineComponent call.
None of this is a convention a model has to infer from surrounding files. It's a rule it can follow directly: one component per file, props schema always second in the config object, event handlers always typed function, no side effects outside the declared-effect primitives.
Two ways an agent finds Solar without reading the docs first
A registry manifest only helps once an agent already knows to call it. Getting Solar in front of an agent in the first place is a separate problem, and it's the one I spent the least time on relative to how much it matters, so I built two direct paths.
The Cursor plugin loads the API straight into the editor: defineComponent, the hooks, the contract model, and the discovery pattern, so a chat session started inside a Solar project already knows the conventions before it writes a single component. Add it from Cursor Directory, no config beyond that.
solarbuild-mcp does the same job for any MCP-compatible client, Cursor, Claude, Windsurf, whatever you're running. Point it at a project's components directory:
{
"mcpServers": {
"solarbuild": {
"command": "npx",
"args": ["solarbuild-mcp", "--components", "./components"]
}
}
}It exposes three tools against that project's live registry. manifest returns the full schema for every registered component. component returns one component's schema by name. validate checks a props object against a schema without mounting anything, returning the same fix-bearing errors ContractError throws at runtime. Instead of reading static docs that might be a version behind, an agent queries the live components in the project itself, wherever the codebase happens to be right now.
There's also a StackBlitz demo that runs the whole loop in the browser with nothing installed: mount a typed Button, catch a ContractError on a bad prop, read the fix field.
Where Solar fits
The strongest fit is AI-generated internal tools: a business user describes what they want, an agent generates components against an explicit contract, and nobody's reviewing every generated file line by line, so catching mistakes at the boundary matters more than it does for hand-maintained code. Past that, it fits no-code and low-code builders (a visual config drives generation on the backend, and the registry lets the platform compose components predictably without knowing their internals), vibe-coded apps built entirely through prompting, configurators and form builders generated from a data schema or API spec, and agent-driven UIs that modify themselves at runtime in response to a user request.
The thread running through all five: someone is directing what gets built, choosing the shape, the data, the constraints, while an agent writes the component code underneath it. Solar is the runtime layer that makes that agent's output trustworthy enough to ship without someone reviewing every line. It's the same shift I wrote about in End of coding, age of building, applied to one specific layer of the stack.
Solar vs React, Vue, and Svelte
| React / Vue / Svelte | Solar | |
|---|---|---|
| Prop validation | Optional and warning-only at best; React removed PropTypes from the package entirely in v19 | Always on, enforced at the component boundary at runtime |
| Failure on a bad prop | Usually nothing at first; a generic JS error later, disconnected from the actual mistake | A structured ContractError object with expected/received/fix |
| Discovering valid components | Source, Storybook, or hand-written docs | registry.manifest(), one JSON call |
| Composition safety | A child can be anything; a wrong one usually fails deep in render | Typed slot props reject the wrong child at the parent boundary, by name |
| Effect model | One general useEffect, easy to misuse with a wrong dependency array |
Three narrow primitives, one job each |
| File structure | Convention, enforced by team norms and review | Rigid by design, nothing to infer and no reviewer required to catch drift |
| Build step | Usually a compiler or bundler | None |
A developer internalizes "don't pass a number where the linter won't catch it" over months of working in a codebase. An agent doesn't, unless the framework tells it in the same turn it made the mistake, in a format it can parse without a person relaying the error back.
Where it sits next to the other AI-first UI systems
Solar isn't the only project chasing this idea. Google's A2UI, open-sourced in December 2025 and already adopted by CopilotKit and Flutter's GenUI work, encodes UI as a flat adjacency list referencing a pre-approved component catalog, which prevents an agent from executing arbitrary code at the architecture level. OpenUI is closest to Solar architecturally: a token-efficient streaming language with Zod schema enforcement on props, the rough equivalent of Solar's props object, also restricted to a pre-registered component library. Storybook's MCP server takes a different angle entirely: it layers contract enforcement on top of an existing component system, with a self-healing loop where an agent generates a component, writes stories, runs interaction and accessibility tests, and fixes what fails, rather than baking contracts into the framework itself.
The academic side is converging on the same idea from a different direction. SpecifyUI extracts structured specifications from UI references so generation stays anchored to intent instead of drifting with each prompt. Generative Interfaces for Language Models, out of Stanford's SALT Lab, found generative interfaces preferred over conversational ones by up to 72% in human evaluation. BISCUIT, from Apple's ML research group, scaffolded LLM-generated code with ephemeral UIs inside Jupyter notebooks back in 2024, which is the earliest version of this argument I've found: structured intermediate representations beat free-form generation once a model is the one producing the output.
Where Solar differs from A2UI and OpenUI: its registry is informational, for discovery and validation, not a hard execution sandbox restricting the model to a pre-approved catalog. That's easier to drop into an existing codebase, at the cost of the stronger safety guarantee those two offer. It also has no dependency on an existing framework or toolchain, where OpenUI and Storybook MCP both sit on top of one. And it's small: the entire core is a handful of files, easier to read end to end than a full generative UI platform, which makes it a legible base for testing the contract-enforcement idea itself rather than a production platform you adopt wholesale. The narrower feature set (no streaming-optimized wire format, no adoption yet by frameworks like CopilotKit or AG-UI, a smaller ecosystem) is the cost of that legibility. I built it that way on purpose, and it's not resolved in Solar's favor yet.
What Solar isn't
It's not a meta-framework. No SSR, no file-based routing, no build pipeline. It doesn't replace React either. It solves a narrower problem, under a different constraint. And it works fine for a human writing components by hand, even though that's not the primary thing it's optimized for.
A few things the project hasn't settled. Whether the prop schema should stay a runtime object or move to a TypeScript interface, trading developer experience for a more reliable target for a model to generate against. How the contract model should extend across schema boundaries as composition gets deeper. And whether a dev server surfacing validation errors before runtime is worth the build-step tradeoff it would reintroduce.
Try it
npm create solarbuild@latest my-app
cd my-app
npm run devOr skip the install entirely and drop this into an empty HTML file:
<script type="module">
import { defineComponent, mountComponent, registry, useState, createElement } from 'https://cdn.jsdelivr.net/npm/solarbuild/framework/index.js'
const Counter = defineComponent({
name: 'Counter',
props: {},
render() {
const [count, setCount] = useState(0)
return createElement('button', { onclick: () => setCount(n => n + 1) }, `Clicked ${count} times`)
}
})
registry.register(Counter)
mountComponent(Counter, {}, document.getElementById('app'))
</script>If you're building anything where an agent writes the UI instead of a person, that's the one decision worth taking even if you never touch Solar itself: make your framework's failure mode a structured error with a fix field an agent can parse, not a generic runtime error three components away from the actual mistake.