TL;DR: Frontend component prompts fail in four specific, checkable ways: the model invents props your library doesn't have, ships markup that looks right but fails an accessibility check, ignores your design tokens for hardcoded values, and produces a component that re-renders on every parent update. Each has a real cause and a real fix, verified against React 19, WAI-ARIA, and MDN.
Why do AI-generated components compile but still not match your app?
Because compiling and matching are different bars, and a model only has to clear the first one to look finished. TypeScript checks that a prop exists on a type; it does not check that the type matches the library you actually shipped, that a click target is reachable by keyboard, or that the color you got is the token your designer picked. A component can pass every static check in your pipeline and still be visibly, structurally wrong the first time a real user or a real screen reader touches it.
The pattern behind all four failure modes below is the same: a model resolves anything you didn't specify by pattern-matching against everything it saw in training, not against your actual codebase. Frontend component prompts that name the actual constraints (your library, your tokens, your accessibility bar) close that gap. Prompts that only describe the desired look leave it wide open.
Why does the model invent props that don't exist?
Because your prompt described an outcome, not an API, and the model has seen dozens of component libraries that each solve that outcome differently. Ask for "a button with a loading state" with nothing else specified, and the model has to guess whether that's isLoading, loading, pending, or busy, and whether the spinner replaces the label or sits beside it. Every major UI library (shadcn/ui, Material UI, Chakra, Ant Design) answers that differently, and the model's training data contains all of them mixed together. The same drift shows up in event handlers: asked for a save button with nothing else specified, one pass might wire onSave, another onSubmit, another onConfirm. Each is a reasonable name in isolation, and none is guaranteed to match the callback prop your component actually expects.
The fix is not a smarter model. It's giving the model your real API surface instead of asking it to reconstruct one:
Bad: "Add a loading state to the button."
Good: "Our Button component (src/components/ui/button.tsx) accepts
`variant`, `size`, and `disabled`. Add a `loading` boolean prop
that disables the button and swaps the label for our existing
<Spinner size="sm" /> component. Do not add props that aren't
already on Button's type definition."
The second version can still get details wrong, but it cannot invent a prop that isn't in the type it was just shown, because you removed the ambiguity that caused the guess in the first place.
Why does the markup look right but fail an accessibility check?
Because visual correctness and semantic correctness are unrelated, and a model optimizes for what a screenshot would show. The most common failure is a div or span styled to look exactly like a button, which passes a visual review and fails every other one. MDN's guidance on the button role states the risk directly: "Buttons are expected to be triggered using the Space or Enter key, while links are expected to be triggered using the Enter key." A div has neither behavior by default, and adding the ARIA role alone does not add the keyboard handling.
The WAI-ARIA Authoring Practices Guide is specific about what a real button actually needs: "The button has an accessible label." By default, "the accessible name is computed from any text content inside the button element", or supplied explicitly when there's no visible text. Separately, "When the action associated with a button is unavailable, the button has aria-disabled set to true" so assistive technology can announce it. None of that is optional polish. It's the minimum contract a native <button> element gives you for free, per MDN's own description of the element as "an interactive element activated by a user with a mouse, keyboard, finger, voice command, or other assistive technology."
// Wrong: looks like a button, isn't one
<div className="btn-primary" onClick={handleSave}>
Save
</div>
// Wrong: role added, keyboard support still missing
<div
className="btn-primary"
role="button"
onClick={handleSave}
>
Save
</div>
// Right: use the element that already does this
<button type="button" className="btn-primary" onClick={handleSave}>
Save
</button>
// Right, if you genuinely can't use <button>: role, tabIndex,
// and both interaction paths, handled yourself
<div
className="btn-primary"
role="button"
tabIndex={0}
onClick={handleSave}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") handleSave();
}}
>
Save
</div>
If you do end up with a non-native element carrying role="button", MDN is direct that "the tabindex attribute has to be used to make the button focusable." An icon-only button needs an accessible name from somewhere, since there's no text content to compute one from; that's what aria-label is for. MDN's own definition is precise: "The aria-label attribute defines a string value that can be used to name an element, as long as the element's role does not prohibit naming". It also warns against the common misuse: "If there is visible text that labels an element, use aria-labelledby instead." Not aria-label, since duplicating visible text into an invisible attribute is redundant at best and drifts out of sync at worst.
None of this is verified by asking a model whether its own output is accessible, a question it will answer confidently either way. Run a keyboard-only pass first: tab to every control and trigger it with Space or Enter, not a mouse. Then check with a real screen reader (VoiceOver on macOS, NVDA on Windows) or an automated scanner like axe DevTools before treating any AI-generated interactive element as finished.
Why does a generated component ignore your design tokens?
Because a model can't reuse a token it was never shown, so it produces a plausible-looking hardcoded value instead: #3B82F6 where your theme has --color-primary, 16px where your spacing scale has --space-4. The component still renders correctly in isolation, which is exactly why this defect survives a visual review, it looks fine right up until a designer changes the token and every hardcoded copy silently drifts out of sync.
This is a prompting failure, not a model limitation. A prompt that only describes the desired look ("a card with rounded corners and a subtle shadow") has no token names in it for the model to reuse, so there's nothing to reuse. A prompt that names the actual tokens removes the guesswork entirely:
Bad: "Style the card with rounded corners, a light border,
and generous padding."
Good: "Use `--radius-lg` for the corner radius, `--color-border`
for the border, and `--space-6` for padding. These are
defined in src/styles/tokens.css. Do not introduce new
hex values, pixel values, or spacing numbers."
Why does the component re-render every time its parent updates?
Usually because something new is being created on every render and handed down as a prop, which breaks React's ability to tell that nothing actually changed. An inline arrow function passed as an event handler, an object or array literal built fresh inside the render body, both are a new reference every single time, even when their contents are identical to the last render.
React's own docs name the three tools for this directly. memo is described plainly: it "lets you skip re-rendering a component when its props are unchanged." useMemo "is a React Hook that lets you cache the result of a calculation between re-renders". useCallback, likewise, "is a React Hook that lets you cache a function definition between re-renders." Used together, a memoized child component stops re-rendering once its props stop being new objects every time, and the two hooks are what keep those props stable in the parent.
// Re-renders every time Parent renders, even if `items` didn't change
function Parent({ items }: { items: Item[] }) {
return <ExpensiveList items={items} onSelect={(id) => console.log(id)} />;
}
// Stable props in, memoized child skips the re-render
const ExpensiveList = React.memo(function ExpensiveList({
items,
onSelect,
}: {
items: Item[];
onSelect: (id: string) => void;
}) {
/* ... */
});
function Parent({ items }: { items: Item[] }) {
const handleSelect = React.useCallback((id: string) => {
console.log(id);
}, []);
return <ExpensiveList items={items} onSelect={handleSelect} />;
}
Worth flagging before you reach for memo by hand: React's own docs describe an opt-in compiler that changes this calculus. React Compiler "automatically applies the equivalent of memo to all components, reducing the need for manual memoization." It's a separate build step a project adopts deliberately, not default behavior in React 19 itself, so a codebase without it still needs the manual memo / useMemo / useCallback pattern above. A prompt that asks a model to "add memoization" only helps if you also tell it which of those two situations you're actually in.
Worth naming the version when you ask for this: if you're on React 19, there's a related change worth knowing before you paste in an older pattern. React's own release notes state it plainly: "Starting in React 19, you can now access ref as a prop for function components", with no wrapper required. The same notes are direct that this changes what new code should look like, not what's currently broken: "New function components will no longer need forwardRef", and "we will be publishing a codemod to automatically update your components to use the new ref prop." forwardRef itself keeps working today; the notes say only that "In future versions we will deprecate and remove forwardRef". A prompt that tells the model which major version you're targeting avoids generating a correctly-working pattern that's already a version behind.
| Feature | Description-only prompt | Reference-based prompt |
|---|---|---|
| Prop names guaranteed to exist in your library | ||
| Reuses your actual color, spacing, and radius tokens | ||
| Accessible name follows your project's real pattern | ||
| Model must invent details you didn't specify | ||
| Requires you to already have a component worth mirroring |
That last row is the honest caveat: this technique needs something to point at. A brand-new project with no existing Button component doesn't have a reference to give the model yet, and the first version of anything is where invented props and hardcoded values are hardest to avoid entirely.
What's the one prompt habit that actually fixes most of this?
Reference an existing component and name your real tokens, instead of describing what the new one should look like. None of the four causes are React-specific either; the same invented-prop, ignored-token, and re-render patterns show up in Vue, Svelte, and Solid components generated the same careless way. That single substitution is doing all four jobs above at once: it removes the ambiguity that produces invented props, it gives the model your project's actual accessibility pattern to copy instead of reinvent, it hands over token names instead of leaving the model to guess plausible values, and it usually surfaces the existing memoization pattern your codebase already uses for similar components.
Generate a PriceCard component that mirrors the structure, prop
naming, and token usage of our existing Card component at
src/components/ui/card.tsx. It needs a title, a price, a list of
included features, and a primary action button.
Constraints:
- Reuse Card's existing props where the shape matches; only add
new props for things Card doesn't have.
- Use our design tokens (src/styles/tokens.css) for every color,
spacing, and radius value. No hardcoded hex or pixel values.
- The action button must be a real <button> element with a
visible label, or a `<Button>` from our library. No div or
span standing in for one.
- If any prop or callback is passed inline, wrap it in useCallback
or lift it out of the render body, matching how Card does it.
Stop rewriting prompts. Start shipping.
Works with ChatGPT, Claude, Gemini, Grok, Midjourney, Ideogram, Veo3 & Kling. 4.8★ on the Chrome Web Store.
Create An AccountNone of this is unique to one AI coding tool. If you're generating UI with v0, Bolt.new, or Replit Agent, the same four failure modes apply, because they come from what the prompt did and didn't specify, not from which builder is running it. Our guide to prompting for a genuinely useful code review covers how to get a model to catch a good share of these before a human has to. And if you're managing a growing set of component-generation prompts like the one above without retyping them per project, our companion guide to managing prompts inside your IDE covers where they should actually live.