There is an important distinction between using AI to generate UI code during development and letting AI generate executable UI at run time.
If a developer uses ChatGPT, GitHub Copilot, Cursor, or another AI coding assistant to generate a component, template, stylesheet, test, or even an entire feature, that can be a productive workflow. The generated code still enters the normal software development process. A developer can review it, edit it, test it, commit it, and maintain it like any other source code.
That is not the problem this article is about.
The risk begins when a live application asks a model to generate HTML, JavaScript, styles, or event handlers in response to an end user’s request and then renders that output directly inside the running product. In that scenario, AI is no longer just helping a developer write code. It is participating in the run-time behavior of the application.
This article is for developers and front-end architects building AI-driven applications where the interface adapts based on user intent. These systems may render charts, tables, forms, summaries, confirmation flows, or workflow steps dynamically. The goal is not to discourage the use of AI-generated, executable UIs in production. The goal is to use AI-generated, executable UIs in production safely, with the proper checks.
This is not a hypothetical concern. Developer platforms are already exploring generative UI patterns where model output, tool calls, or structured responses determine which components appear in an application. The pattern is still early, but the direction is clear: AI responses are moving from plain text into task-specific interface composition. That makes the front-end boundary more important, not less.
A useful example is a cloud cost dashboard.
Imagine a user asks:
Why did our cloud bill spike this week, and what can I do about it?
A plain-text response may explain that compute usage increased, storage costs rose, and one service had unusual traffic. That answer may be technically correct, but it is not the best interface for the task. The user probably needs a cost comparison chart, a service breakdown, a list of anomalies, links to affected resources, and a set of safe remediation actions.
This is where generative UI becomes valuable. The application can assemble an interface around the user’s information needs and the actual task to be performed, instead of forcing every answer through a chat transcript.
But the architecture matters.
Generative UI will not fail because AI cannot draw charts or suggest buttons. It will fail because teams let models own behavior they should only be allowed to describe.
The model may propose intent, but the application must own execution.
As AI applications move beyond chat boxes, developers are starting to build interfaces that adapt to what the user is trying to do. That is the right direction. A user investigating a spike in the cloud bill should not have to read a long paragraph and then manually navigate to five different dashboards. The application can present the monthly comparison, identify the services that changed most, surface the suspicious resources, and offer next steps in one task-specific view.
That is the promise of generative UI: interfaces assembled dynamically around the user’s task.
But there is a dangerous shortcut behind that promise. If the model generates arbitrary HTML, JavaScript, styles, and event handlers at run time, the application gives up one of the most important boundaries in front-end architecture: control over what the interface can do.
A production application should not let a model invent executable UI during a user session. It should let the model describe interface intent, then render that intent through trusted application components.
The most obvious version of generative UI is also the most fragile. A developer building an AI-powered product asks the model to produce some HTML, injects the output into the page, and treats the result as an adaptive interface.
For example, the model might return something like this:
Cloud spend increased by 38%
The biggest increase came from compute usage in us-east-1.
Shut down unused instances
This looks useful at first glance, but it creates several problems at once.
The model has generated markup, assumed a visual structure, and attached executable behavior. It has decided that shutting down instances should be available. It has assumed a function named shutdownInstances
exists. It may also have bypassed the design system, accessibility rules, analytics conventions, authorization checks, approval workflows, and the application’s normal state model.
This is very different from an AI coding assistant generating a component that a developer reviews before shipping. In this case, the model output is entering the live application at run time. The user sees it immediately, and the application may execute behavior based on it immediately.
Even if this works in a demo, it is not a maintainable front-end architecture.
Generated HTML bypasses the component system. Generated JavaScript expands the application’s attack surface. Generated event handlers make behavior harder to test. Generated UI state can drift away from real application state. Generated forms can ask for fields the product does not support. Generated buttons can imply actions the user is not allowed to perform.
The issue is not that models are useless at shaping interfaces. The issue is that an executable UI is the wrong run-time output boundary.
In a production system, the model should not create the interface directly. It should describe the kind of interface the application should assemble.
A safer approach is to define a controlled vocabulary of UI blocks the model is allowed to request. The application validates the model response, rejects anything it does not understand, and maps valid blocks to trusted components.
Instead of returning HTML, the model returns structured data:
{
"blocks": [
{
"type": "cost-summary",
"props": {
"period": "current-week",
"comparisonPeriod": "previous-week"
}
},
{
"type": "service-cost-breakdown",
"props": {
"period": "current-week"
}
},
{
"type": "anomaly-list",
"props": {
"severity": "high"
}
},
{
"type": "remediation-options",
"props": {
"category": "compute"
}
}
]
}
This response is still dynamic. The model can decide that the user needs a cost summary, a service breakdown, a list of anomalies, and remediation options. But it is no longer inventing arbitrary UI. It is choosing from capabilities the application already understands.
In TypeScript, that vocabulary might look like this:
type UIBlock =
| {
type: 'cost-summary';
props: {
period: 'current-week' | 'current-month';
comparisonPeriod: 'previous-week' | 'previous-month';
};
}
| {
type: 'service-cost-breakdown';
props: {
period: 'current-week' | 'current-month';
};
}
| {
type: 'anomaly-list';
props: {
severity: 'medium' | 'high';
};
}
| {
type: 'remediation-options';
props: {
category: 'compute' | 'storage' | 'network';
};
}
| {
type: 'confirmation';
props: {
message: string;
actionId: string;
};
};
This type defines the interface contract. The model can request a cost summary, a service breakdown, an anomaly list, remediation options, or a confirmation prompt. It cannot invent a new component type, add random props, attach JavaScript, or decide how the component behaves internally.
The model chooses from a vocabulary. The application owns the vocabulary.
Once the model response has been converted into structured UI intent, the application can render it through a component registry.
const componentRegistry = {
'cost-summary': CostSummary,
'service-cost-breakdown': ServiceCostBreakdown,
'anomaly-list': AnomalyList,
'remediation-options': RemediationOptions,
'confirmation': Confirmation
};
function renderBlock(block: UIBlock) {
const Component = componentRegistry[block.type];
if (!Component) {
return null;
}
return ;
}
This example is intentionally simple, but the architectural shift is significant.
The model does not decide how the cost summary is implemented. It does not decide how billing data is loaded, how errors appear, how states work, how accessibility is handled, or how analytics are recorded. Those responsibilities remain inside the application.
That matters because a mature component system is not just a visual toolkit. It is where front-end teams encode product consistency, interaction rules, performance decisions, accessibility behavior, telemetry, and testing strategy.
Generative UI should not bypass that system. It should compose from it.
A component registry gives the model flexibility without giving it authority. The model can influence which trusted components appear. It cannot create new executable behavior outside the application’s control.
TypeScript helps developers define the expected shape of the UI contract, but TypeScript does not validate model output at run time. A model response is external data. It should be treated the same way we treat data from a network request, file upload, webhook, or third-party API.
It is untrusted until validated.
The validation layer is where the application turns probabilistic model output into something deterministic enough to render. Whether a team uses JSON Schema, Zod, Valibot, or another validation library, the application should receive the model response as unknown data and validate it before anything reaches the screen. That validation step should reject unknown component types, malformed props, unsupported actions, and any structure the application does not explicitly understand. Only after the response passes that boundary should it be rendered through the component registry.
With a schema library, the validation boundary might look like this:
const CostSummarySchema = z.object({
type: z.literal('cost-summary'),
props: z.object({
period: z.enum(['current-week', 'current-month']),
comparisonPeriod: z.enum(['previous-week', 'previous-month'])
})
});
const AnomalyListSchema = z.object({
type: z.literal('anomaly-list'),
props: z.object({
severity: z.enum(['medium', 'high'])
})
});
const UIBlockSchema = z.discriminatedUnion('type', [
CostSummarySchema,
AnomalyListSchema
]);
function parseUIResponse(response: unknown): UIBlock[] {
const result = z.array(UIBlockSchema).safeParse(response);
if (!result.success) {
return [];
}
return result.data;
}
In a real application, the schema would likely cover layout rules, component limits, allowed nesting, action references, and versioning. The point is not the specific library. The point is the boundary.
The model does not get to decide whether its output is safe. The application does.
A fallback path is also essential. If validation fails, the application should not attempt to improvise. It should show a safe fallback, ask the user to rephrase, or return a conventional text response. AI-driven interfaces need graceful failure. A malformed UI description should never become a broken or unsafe screen.
The most important boundary in generative UI is not rendering. It is execution.
A dynamic interface may include buttons, forms, confirmations, or workflow steps. Those controls may request real operations: shut down an instance, resize a database, open a support ticket, approve a deployment, update a policy, or change account settings.
The model should not execute those actions. It should not decide that an operation is allowed simply because the user asked for it. Instead, action execution should flow through an application-owned action registry.
For example, a model may request a confirmation component:
{
"type": "confirmation",
"props": {
"message": "Do you want to open a remediation task for the unused compute instances?",
"actionId": "create-remediation-task"
}
}
But the action itself should be defined and executed by the application:
type UIAction =
| {
type: 'create-remediation-task';
resourceIds: string[];
}
| {
type: 'open-support-ticket';
category: 'billing' | 'performance' | 'security';
};
const actionRegistry = {
'create-remediation-task': createRemediationTask,
'open-support-ticket': openSupportTicket
};
async function executeAction(action: UIAction, user: CurrentUser) {
if (!isActionAllowed(action, user)) {
throw new Error('Action not allowed');
}
return actionRegistry[action.type](action);
}
Before an operation runs, the application has to make deterministic decisions that the model should not control. The action must exist in the application’s registry, the current user must be authorized to perform it, the target resources must belong to a context the user can access, and the operation must still be valid in the current state. Some actions may require confirmation, auditing, approval routing, or a final server-side permission check before anything changes.
These questions cannot be delegated to the model. They belong to the application and, ultimately, to the back-end systems that enforce the business rules.
The model can help generate the path. It cannot become the authority.
Generative UI also creates a subtle state-management problem.
In a traditional application, the front end knows where state lives. Billing data, user permissions, resource metadata, anomaly status, remediation tasks, and workflow progress are loaded, cached, invalidated, and updated through known application paths.
An AI-driven interface can blur that boundary. The model may summarize state, infer state, remember conversation context, or describe a screen based on previous messages. If teams are not careful, the generated interface becomes a second hidden state system.
That is dangerous.
The UI may say a compute instance is unused even though its status has changed. It may show a remediation option based on stale billing data. It may produce a confirmation message that no longer matches the current workflow. It may remember something from the conversation that the application itself has not verified.
The application must always remain the authority on state.
The model can help decide which components to display, but those components should read real state from the application and its APIs. A CostSummary
component should fetch or receive billing data through the same trusted path as any other part of the product. A remediation action should update state through the normal application flow. A confirmation component should not become the source of truth for whether an operation is possible.
Generative UI should be a projection of application state, not the owner of it.
This distinction becomes even more important in agentic applications, where interfaces may change over several turns of conversation. A user may ask a question, inspect a result, request an action, change their mind, and return later. The application needs a reliable model for what happened, what is pending, what failed, and what still requires human approval.
That cannot live only in the model’s context window.
The future of generative UI is not arbitrary run-time code generation. It is controlled composition.
The model should be able to assemble experiences from trusted capabilities: components, layouts, actions, validation rules, and state transitions that the application exposes intentionally.
That gives developers the best of both worlds.
The interface can adapt to the user’s goal, but the system remains testable. The model can choose the right UI blocks, but the design system stays intact. The user can move through dynamic workflows, but permissions and business rules remain deterministic. The application can feel intelligent without becoming unpredictable.
This is also a better mental model for front-end teams. Generative UI is not a replacement for front-end architecture. It increases the need for front-end architecture.
Teams still need component systems. They still need run-time validation. They still need state ownership. They still need accessibility standards. They still need action boundaries. They still need server-side authorization. AI does not remove these concerns. It makes weak boundaries easier to expose.
In this model, the user expresses intent and the model responds with structured UI intent. The application validates that response, renders it through a component registry, and routes any requested behavior through an action registry. Application state remains the source of truth, while the server remains responsible for final authorization.
That is the boundary production systems need.
AI can help developers generate complete features during development. That code can and should go through review, testing, and normal delivery. But when AI participates in a running application, the run-time contract needs to be much narrower. The live model should describe what the user interface should express, not generate unchecked code that the product executes.
The better approach is to give AI a component system.
Let the model compose. Let the application control. Let the user experience become more dynamic without sacrificing the architecture that makes software reliable.