{"slug": "why-ai-coding-assistants-write-clean-looking-css-that-breaks-in-production-and", "title": "Why AI coding assistants write clean-looking CSS that breaks in production (and how to fix it)", "summary": "A developer has documented five recurring CSS bugs that AI coding assistants such as Cursor, Claude, and Copilot introduce in frontend code, including layout shift from loading spinners, flexbox overflow from missing min-width bounds, and tooltips that fail on natively disabled buttons. The writeup pairs each failure mode with a production-ready fix, such as keeping the original label in the DOM to lock button width and using aria-disabled instead of the disabled attribute so hover events still fire.", "body_md": "If you use Cursor, Claude, or Copilot for frontend work, you already know the feeling.\n\nYou prompt the model to generate a dashboard card or a modal component. It spits out fifty lines of clean TypeScript and Tailwind classes in two seconds. It compiles without errors. In your local browser with sample data, it looks ready to ship.\n\nThen you deploy it to staging or open it on your phone, and subtle UI bugs start popping up everywhere.\n\nThe layout jumps every time a button loads. Long usernames overflow their containers. Tooltips stop working on disabled buttons. Text gets clipped on smaller screens.\n\nAI models are great at syntax, but they tend to skip defensive CSS patterns. Here are five visual bugs AI coding assistants introduce on almost every prompt, and the exact code fixes to prevent them.\n\nWhen you ask an AI to add a loading state to a button, it almost always writes something like this:\n\n```\n// Typical AI code\n<button disabled={isLoading} className=\"px-4 py-2 bg-blue-600 text-white rounded-md\">\n  {isLoading ? <Spinner className=\"w-4 h-4\" /> : \"Save Changes\"}\n</button>\n```\n\nThe problem is that a 16px spinner is much narrower than the text \"Save Changes\". The moment the user clicks, the button width shrinks. Adjacent inputs and buttons shift across the screen, causing a jarring Cumulative Layout Shift (CLS).\n\nKeep the original label in the DOM to lock the button width, and place the spinner in an absolute overlay:\n\n```\n// Production fix\n<button disabled={isLoading} className=\"relative px-4 py-2 bg-blue-600 text-white rounded-md\">\n  <span className={isLoading ? \"invisible\" : \"visible\"}>Save Changes</span>\n  {isLoading && (\n    <span className=\"absolute inset-0 flex items-center justify-center\">\n      <Spinner className=\"w-4 h-4 animate-spin\" />\n    </span>\n  )}\n</button>\n```\n\nThis keeps the button width fixed regardless of state.\n\nAI tools love flexbox, but they rarely include defensive width bounds. When a user has a long name or uploads a file with a fifty-character title, the layout breaks.\n\n```\n// Typical AI code\n<div className=\"flex items-center gap-3\">\n  <Avatar />\n  <div className=\"flex flex-col\">\n    <span className=\"font-medium truncate\">{user.name}</span>\n    <span className=\"text-sm text-slate-500 truncate\">{user.email}</span>\n  </div>\n</div>\n```\n\nAdding `truncate` does nothing here because flex children default to `min-width: auto`. If the container gets squeezed, the text refuses to shrink and pushes the parent element beyond the viewport.\n\nAdd `min-w-0` to the flex child holding the text:\n\n```\n// Production fix\n<div className=\"flex items-center gap-3\">\n  <Avatar />\n  <div className=\"flex flex-col min-w-0\">\n    <span className=\"font-medium truncate\">{user.name}</span>\n    <span className=\"text-sm text-slate-500 truncate\">{user.email}</span>\n  </div>\n</div>\n```\n\nThat one utility class tells the browser that the flex child is allowed to shrink below its content width, making text truncation work as expected.\n\nWhen an action is blocked, you usually want a tooltip explaining why (for instance, \"You need admin permissions to delete this project\").\n\nAI models almost always disable the button directly:\n\n```\n// Typical AI code\n<Tooltip content=\"Admin permissions required\">\n  <button disabled={!isAdmin} className=\"btn-primary\">\n    Delete Project\n  </button>\n</Tooltip>\n```\n\nWhen HTML buttons have the native `disabled` attribute, browsers stop firing all mouse events on that element. Hovering over the button will not trigger mouseEnter, and the tooltip never renders. Users click a dead button with no idea why it is inactive.\n\nUse `aria-disabled` and manage the interaction styles manually:\n\n```\n// Production fix\n<Tooltip content={!isAdmin ? \"Admin permissions required\" : \"\"}>\n  <button\n    aria-disabled={!isAdmin}\n    onClick={(e) => {\n      if (!isAdmin) {\n        e.preventDefault();\n        return;\n      }\n      handleDelete();\n    }}\n    className={`btn-primary ${!isAdmin ? \"opacity-50 cursor-not-allowed pointer-events-auto\" : \"\"}`}\n  >\n    Delete Project\n  </button>\n</Tooltip>\n```\n\nThe button remains keyboard and screen-reader accessible, mouse events still bubble to the tooltip, and the user gets a clear explanation.\n\nAsk an AI model for a modal, drawer, or settings panel, and it will often set a fixed height:\n\n```\n// Typical AI code\n<div className=\"fixed inset-0 bg-black/50 flex items-center justify-center\">\n  <div className=\"w-[500px] h-[600px] bg-white rounded-lg p-6 flex flex-col justify-between\">\n    <ModalHeader />\n    <ModalBody />\n    <ModalFooter />\n  </div>\n</div>\n```\n\nOn a desktop monitor, 600px looks fine. On a smaller mobile screen or a laptop with browser toolbars open, the modal footer gets pushed off the bottom edge. Users cannot see or tap the submit button.\n\nUse dynamic viewport units and internal scroll areas:\n\n```\n// Production fix\n<div className=\"fixed inset-0 bg-black/50 flex items-center justify-center p-4\">\n  <div className=\"w-full max-w-lg max-h-[90dvh] bg-white rounded-lg flex flex-col\">\n    <div className=\"p-6 border-b border-slate-200\">\n      <ModalHeader />\n    </div>\n    <div className=\"p-6 overflow-y-auto flex-1\">\n      <ModalBody />\n    </div>\n    <div className=\"p-6 border-t border-slate-200\">\n      <ModalFooter />\n    </div>\n  </div>\n</div>\n```\n\nThe header and footer stay pinned in view, while long form content scrolls cleanly inside `ModalBody`.\n\nWhen generating edit modals, AI tools frequently store form inputs inside component state without resetting them when the target entity changes.\n\n```\n// Typical AI code\nfunction EditUserModal({ user, isOpen, onClose }) {\n  const [name, setName] = useState(user.name);\n  const [role, setRole] = useState(user.role);\n\n  if (!isOpen) return null;\n  return (\n    <Modal onClose={onClose}>\n      <input value={name} onChange={(e) => setName(e.target.value)} />\n    </Modal>\n  );\n}\n```\n\nIf the user edits \"Alice\", closes the modal, and clicks edit on \"Bob\", the inputs will still show \"Alice\". The state initialized on the first mount and never refreshed.\n\nKey the modal instance by entity ID at the call site so React tears down and remounts fresh state on entity change:\n\n```\n// Production fix at the parent call site\n{activeUser && (\n  <EditUserModal\n    key={activeUser.id}\n    user={activeUser}\n    isOpen={Boolean(activeUser)}\n    onClose={() => setActiveUser(null)}\n  />\n)}\n```\n\nUsing a key boundary is cleaner and less error-prone than juggling multiple `useEffect` reset hooks inside the child component.\n\nFixing these edge cases prompt after prompt takes up a lot of code review time.\n\nOne way to solve this is to define strict CSS rules directly inside your `.cursorrules` or `AGENTS.md` file so the model stops reaching for fragile patterns. Another is maintaining a dedicated set of accessible UI primitives that already handle layout shifts, touch targets, and viewport bounds out of the box.\n\nWe put together an open-source collection of accessible flat UI primitives for React 19 and Next.js 16 over at [devpreflight.com](https://devpreflight.com) to save developers from rebuilding these safeguards on every project.", "url": "https://wpnews.pro/news/why-ai-coding-assistants-write-clean-looking-css-that-breaks-in-production-and", "canonical_source": "https://dev.to/devpreflight/why-ai-coding-assistants-write-clean-looking-css-that-breaks-in-production-and-how-to-fix-it-4mk1", "published_at": "2026-09-21 21:05:40+00:00", "updated_at": "2026-09-21 21:25:01.808769+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "ai-products"], "entities": ["Cursor", "Claude", "GitHub Copilot", "Tailwind CSS", "TypeScript"], "alternates": {"html": "https://wpnews.pro/news/why-ai-coding-assistants-write-clean-looking-css-that-breaks-in-production-and", "markdown": "https://wpnews.pro/news/why-ai-coding-assistants-write-clean-looking-css-that-breaks-in-production-and.md", "text": "https://wpnews.pro/news/why-ai-coding-assistants-write-clean-looking-css-that-breaks-in-production-and.txt", "jsonld": "https://wpnews.pro/news/why-ai-coding-assistants-write-clean-looking-css-that-breaks-in-production-and.jsonld"}}