Most teams are giving Claude Code and similar coding agents the wrong kind of guardrails. They define lint rules, component libraries, TypeScript strictness, maybe a PR checklist, then act surprised when the generated UI is technically valid and still completely wrong for the product.
That happens because UI quality is not just a code problem. It is a constraint problem. A coding agent can infer structure from code, but it cannot reliably infer product intent from scattered components, half-documented Figma files, and a designer's unspoken assumptions. If you let it touch your interface without explicit design constraints, it will optimize for the easiest visible pattern, not the right user experience.
The result is familiar. Buttons appear where links should be. Empty states are missing. Error handling is technically present but emotionally tone-deaf. Tables render correctly on desktop and collapse into nonsense on mobile. Forms validate inputs but ignore recovery paths. Loading states flicker, destructive actions look identical to safe ones, and every screen feels slightly off even when nothing is obviously broken.
If you are using Claude Code to build frontends, the fix is not to tell it to “be more careful.” The fix is to encode the rules your product team already uses but has not written down. In practice, that means three things: constrain the design system, constrain state behavior, and constrain interaction decisions.
Why coding agents get UI wrong even when the code looks right
A coding agent is usually operating with strong local context and weak product context.
It can see:
- component names n- props
- file structure
- nearby patterns
- tests, if they exist
- style tokens, if they are obvious
What it usually cannot see clearly is the reason those patterns exist. It does not know which spacing exception was intentional, which modal pattern caused support tickets last quarter, or which CTA hierarchy was chosen to reduce accidental destructive actions. It sees implementation artifacts, not the product history behind them.
That gap matters more in UI than in backend code. In backend systems, correctness is often binary. A queue either retries or it does not. A database transaction either commits safely or it does not. In interfaces, many failures are product-wrong rather than syntax-wrong. The page compiles. The tests pass. The screen even looks polished. But the user is guided into the wrong action, left without feedback, or blocked in an edge case the system should have anticipated.
This is why “use our component library” is not enough. A component library gives an agent building blocks. It does not tell the agent:
- when a modal is forbidden and a drawer is preferred
- which actions must always expose undo
- what an empty analytics screen should teach the user
- how loading, partial data, timeout, and stale-data states differ
- when a dense table should become filtered cards on smaller screens
- what should happen after a successful action besides showing a toast
Without those rules, the agent fills in the blanks with plausible defaults. Plausible defaults are exactly what create generic SaaS interfaces that look acceptable in a screenshot and fail in production.
Design constraints are product rules, not visual suggestions
A useful way to think about design constraints is this: they are operational rules for interface behavior.
Most teams document design at the wrong abstraction layer. They write token specs, color scales, and typography guidelines, then assume the rest is obvious. It is not. Agents need higher-level constraints that connect presentation to intent.
The best constraint sets usually cover four layers.
1. Structural constraints
These describe how layouts and patterns are allowed to appear.
Examples:
- Settings pages use left navigation only when there are 5 or more stable categories.
- Primary actions sit at the top-right on desktop, but become bottom-sticky only for task completion flows on mobile.
- Use modals only for short confirmation or isolated edit tasks. Anything with navigation, preview, or multiple steps becomes a dedicated page or drawer.
- Never place destructive actions next to primary success actions without separation.
These rules prevent the agent from composing random but valid interfaces.
2. State constraints
This is the part most teams skip, and it is where agents fail hardest.
Every meaningful screen has more than one state:
- initial loading
- refreshing
- empty
- filtered empty
- partial failure
- permission denied
- offline or timeout
- success after mutation
- stale data with background refresh
If these states are not specified, the agent will invent them inconsistently. One page gets skeletons, another gets a spinner, another gets nothing. Some errors show inline, others in toasts, others vanish into console.error.
3. Interaction constraints
These define how the product should feel to use.
Examples:
- Inline validation should appear on blur, not on every keystroke, except for password strength indicators.
- Save operations that complete under 400ms should not show a blocking spinner.
- Destructive actions require either confirmation text entry or undo, depending on blast radius.
- Search filters should preserve URL state so views are shareable.
These are not styling notes. They are product behavior rules.
4. Content constraints
Agents also make terrible microcopy decisions unless guided.
Examples:
- Empty states must explain why the screen is empty and what to do next.
- Error copy must be actionable, not apologetic fluff.
- Button labels should describe the outcome, not generic verbs like “Submit.”
- Success states should confirm what changed and what the user can do next.
That last point matters more than teams admit. A visually correct interface with vague copy is still a broken interface.
What to give Claude Code before it edits a frontend
If you want Claude Code UI constraints to work in practice, do not hand the agent a 60-page brand document and hope for the best. Give it a small, enforceable contract.
A good starting point is a machine-readable or at least agent-readable file that lives near the frontend codebase. Something like docs/ui-constraints.md or docs/product-ui-rules.md works fine. The important part is that it is explicit, current, and opinionated.
Here is a practical structure.
## UI decision rules
- Use existing design system components before creating new primitives.
- Do not introduce new button variants, badge colors, or spacing scales.
- Prefer page layouts over modals for flows with more than one decision.
- All async actions must define loading, success, error, and retry behavior.
- Empty states must include a reason and a next step.
- Destructive actions must be visually separated and require confirmation or undo.
- Filter and sort state must persist in the URL for index/list screens.
- Mobile layouts must preserve key actions without hiding critical information behind hover.
## State patterns
### Tables
- Initial load: skeleton rows
- Empty dataset: educational empty state with CTA
- Empty after filters: compact reset-filters state
- Background refresh: keep stale rows visible, show subtle loading indicator
- Row action failure: inline error at row level, not page-level generic toast
### Forms
- Validate on blur for standard inputs
- Disable submit only for invalid or actively submitting states
- Preserve user input on server validation failure
- Show field errors inline, global errors at top summary
- After success, either redirect with confirmation or update in place, never both
This kind of document does two jobs. First, it narrows the agent's search space. Second, it gives reviewers something concrete to enforce.
The key is specificity. “Make it intuitive” is useless. “For data tables, preserve stale data during refetch instead of blanking the screen” is actionable.
The missing piece: state matrices beat design tokens
If you only do one thing, build state matrices for your important screens.
This sounds boring, which is probably why teams avoid it. But it is the single best way to stop UI drift when coding agents start shipping views quickly.
A state matrix is a compact description of what a screen does across the conditions that matter. Not just how it looks, but how it behaves.
Take a billing page. Most teams specify the happy path: list invoices, show subscription tier, allow payment method updates. That is not enough.
A state matrix forces you to define:
| Condition | Required behavior |
|---|---|
| No invoices yet | Explain why, show expected future behavior |
| Payment provider timeout | Keep current billing info visible, show retry path |
| Subscription canceled but active until date | Emphasize remaining access, de-emphasize upgrade CTA |
| Card update success | Confirm last four digits changed, do not just show generic success toast |
| User lacks billing permission | Show read-only state with escalation path |
An agent can implement this reliably because the ambiguity is gone.
Here is a lightweight JSON version teams can actually keep in a repo.
{
"screen": "billing-overview",
"states": {
"initial_loading": {
"pattern": "skeleton",
"preserve_previous_data": false
},
"refreshing": {
"pattern": "background_refresh",
"preserve_previous_data": true
},
"empty": {
"title": "No invoices yet",
"body": "Invoices appear here after your first successful billing cycle.",
"cta": null
},
"permission_denied": {
"pattern": "read_only_notice",
"action": "contact_workspace_admin"
},
"mutation_success": {
"feedback": "inline_confirmation",
"message_template": "Payment method updated successfully"
},
"provider_timeout": {
"pattern": "non_blocking_error",
"retry": true,
"preserve_previous_data": true
}
}
}
This is not over-engineering. It is cheaper than reviewing the same category of mistakes in every generated PR.
My strong recommendation is simple: for any screen that affects money, permissions, publishing, destructive actions, or multi-step workflows, define a state matrix before you let an agent implement it.
A bad prompt produces generic UI, but a bad constraint file produces dangerous UI
A lot of teams focus on prompt engineering here. Prompts matter, but they are not the foundation.
This is weak:
Build a clean billing settings page using our existing components. Make it responsive and user-friendly.
This is much better:
Implement the billing settings page using the design system components already in /components/ui.
Follow docs/product-ui-rules.md and docs/state-matrices/billing-overview.json.
Requirements:
- No modal for card updates, use inline expandable panel
- Preserve visible billing history during background refetch
- Differentiate empty account state from filtered-empty search state
- Permission-limited users must see read-only info with no destructive controls
- Mutation success must appear inline near the changed section, not as toast only
- Mobile layout must keep current plan and payment method visible above invoice history
The difference is not better adjectives. The difference is behavioral specificity.
This becomes even more important when the agent starts making local extrapolations. If your codebase has three modal-heavy flows and one page-based flow, the agent may choose the wrong precedent unless the rule says when each pattern is allowed.
In other words, agents do not just need examples. They need decision boundaries.
Example: constraining a CRUD screen the right way
Let us take a common Laravel or full stack admin scenario: a user management screen.
The naive implementation generated by an agent usually looks fine:
- table of users
- create button
- edit modal
- delete confirmation modal
- search box
- role badge
But production reality is messier. What happens when the current user cannot edit owners? What happens when a user is invited but has not accepted? What happens when the list is filtered and empty? What happens if role changes fail after optimistic UI updates?
Here is a better implementation contract.
export const userManagementConstraints = {
list: {
emptyState: {
noUsers: "Show invite CTA and explain roles are assigned after invitation",
filteredEmpty: "Show clear filters action, do not repeat invite CTA as primary"
},
responsive: {
mobile: "Switch from table to stacked cards, preserve role and status visibility"
}
},
actions: {
invite: {
feedback: "Inline success banner with invited email",
failure: "Field-level email error when possible"
},
roleChange: {
optimistic: false,
success: "Update row in place",
failure: "Inline row error, preserve previous role badge"
},
delete: {
allowedFor: ["non-owner"],
requiresConfirmation: true,
confirmationStyle: "modal",
copy: "Explain this removes workspace access immediately"
}
},
permissions: {
ownerRows: {
editRole: false,
delete: false,
explanation: "Owners cannot be modified from this screen"
}
}
};
Now the agent has enough context to avoid the usual traps. It knows not to optimistically change roles. It knows owner rows are special. It knows filtered empty and first-use empty are different experiences. That is the difference between UI that demos well and UI that survives real users.
Design systems are not enough unless they encode allowed composition
There is another uncomfortable truth here. A lot of design systems are too primitive for agent-driven development.
They expose tokens and components, but they do not encode allowed composition patterns. So the agent can legally combine pieces into interfaces no designer would approve.
For example, if your system exposes Button, Card, Dialog, Tabs, Badge, and Dropdown, but does not say how these should be combined in settings flows, index pages, or destructive action paths, then you do not really have an enforceable system. You have a parts catalog.
Agents make this weakness obvious.
What most teams need is a layer above the component library:
- approved page archetypes
- state-specific variants
- interaction rules by flow type
- anti-patterns that should never appear
If you are building internal docs for this, include a blunt “never do this” section. Agents benefit from negative constraints more than humans do.
Examples:
- Never hide critical actions behind hover-only controls.
- Never use a success toast as the only confirmation for destructive or financial actions.
- Never blank a data-rich screen during background refetch.
- Never use disabled buttons without adjacent explanation in permission-limited contexts.
- Never treat empty, filtered-empty, and error states as one generic placeholder.
Those rules save more review time than another page of color token documentation.
How to wire this into a real frontend workflow
The practical version is not complicated.
First, store your rules where the agent can read them close to the repo. Do not bury them in a design tool or a wiki nobody updates.
Second, reference them explicitly in implementation tasks, prompt templates, and PR review checklists.
Third, validate the behavior, not just the markup.
A reasonable workflow for teams using Claude Code looks like this:
- Define
ui-constraints.mdwith global interaction and composition rules. - Add state matrices for critical screens.
- Create a small set of page archetypes, such as index page, multi-step form, settings screen, analytics dashboard.
- Require implementation prompts to cite the relevant constraints.
- Review generated PRs against state behavior and edge cases before visual polish.
If you want to go one step further, turn some of these into tests. Not everything can be unit tested, but a surprising amount can be enforced.
For example:
it('preserves visible rows during background refetch', async () => {
render(<UsersPage initialData={seedUsers} />);
expect(screen.getByText('alice@example.com')).toBeInTheDocument();
triggerRefetch();
expect(screen.getByText('alice@example.com')).toBeInTheDocument();
expect(screen.getByTestId('background-loading-indicator')).toBeInTheDocument();
});
it('shows filtered empty state instead of generic empty state', async () => {
render(<UsersPage initialData={seedUsers} />);
await userEvent.type(screen.getByLabelText(/search/i), 'nonexistent');
expect(screen.getByText(/no users match these filters/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /clear filters/i })).toBeInTheDocument();
expect(screen.queryByText(/invite your first user/i)).not.toBeInTheDocument();
});
These tests are not glamorous, but they pin down product behavior. That is exactly where agents need the most help.
What most teams should do first, and what they should stop doing
If your team is early in agent-assisted UI work, do not try to formalize every pixel. That is a waste of time.
Do this first:
- document 10 to 20 global UI decision rules
- create state matrices for your 5 most important screens
- define empty, loading, error, and success behavior consistently
- write down anti-patterns the agent must avoid
Do not do this first:
- obsess over prompt wording while product rules remain implicit
- assume your component library communicates intent by itself
- review only screenshots instead of behavior and edge cases
- let the agent invent empty states and validation flows screen by screen
The highest leverage move is not better prompting. It is reducing ambiguity in the parts of UI work that are currently trapped in people's heads.
Here is the rule I would use in a real team: if a screen can cause user confusion, financial mistakes, permission errors, or irreversible actions, the agent should not build it from components alone. It should build it from explicit constraints plus state definitions.
That sounds strict because it should be. Coding agents are fast, and speed magnifies weak product discipline. If your design logic is undocumented, the agent will expose that gap immediately.
The practical takeaway is simple. Before Claude Code touches your UI, give it more than a style system. Give it boundaries. Tell it which patterns are allowed, which states must exist, which edge cases matter, and which interactions are non-negotiable. Otherwise you are not automating frontend work. You are automating product inconsistency.
Read the full post on QCode: https://qcode.in/claude-code-needs-design-constraints-before-it-touches-your-ui/
Top comments (0)