Why AI coding assistants write clean-looking CSS that breaks in production (and how to fix it)
DEV Community

Why AI coding assistants write clean-looking CSS that breaks in production (and how to fix it)

If you use Cursor, Claude, or Copilot for frontend work, you already know the feeling. You 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. Then you deploy it to staging or open it on your phone, and subtle UI bugs start popping up everywhere. The layout jumps every time a button loads. Long usernames overflow their containers. Tooltips stop working on disabled buttons. Text gets clipped on smaller screens. AI 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. 1. The Collapsing Button Layout Shift When you ask an AI to add a loading state to a button, it almost always writes something like this: // Typical AI code {isLoading ? : "Save Changes"} The 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). The fix Keep the original label in the DOM to lock the button width, and place the spinner in an absolute overlay: // Production fix Save Changes {isLoading && ( )} This keeps the button width fixed regardless of state. 2. Dynamic Text Overflowing Flex Containers AI 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. // Typical AI code {user.name} {user.email} Adding 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. The fix Add min-w-0 to the flex child holding the text: // Production fix {user.name} {user.email} That one utility class tells the browser that the flex child is allowed to shrink below its content width, making text truncation work as expected. 3. The Disabled Button Tooltip Trap When an action is blocked, you usually want a tooltip explaining why (for instance, "You need admin permissions to delete this project"). AI models almost always disable the button directly: // Typical AI code Delete Project When 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. The fix Use aria-disabled and manage the interaction styles manually: // Production fix { if (!isAdmin) { e.preventDefault(); return; } handleDelete(); }} className={btn-primary ${!isAdmin ? "opacity-50 cursor-not-allowed pointer-events-auto" : ""}} > Delete Project The button remains keyboard and screen-reader accessible, mouse events still bubble to the tooltip, and the user gets a clear explanation. 4. Hardcoded Pixel Heights That Clip Mobile Viewports Ask an AI model for a modal, drawer, or settings panel, and it will often set a fixed height: // Typical AI code On 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. The fix Use dynamic viewport units and internal scroll areas: // Production fix The header and footer stay pinned in view, while long form content scrolls cleanly inside ModalBody . 5. Stale State Drift in Modals When generating edit modals, AI tools frequently store form inputs inside component state without resetting them when the target entity changes. // Typical AI code function EditUserModal({ user, isOpen, onClose }) { const [name, setName] = useState(user.name); const [role, setRole] = useState(user.role); if (!isOpen) return null; return ( setName(e.target.value)} /> ); } If 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. The fix Key the modal instance by entity ID at the call site so React tears down and remounts fresh state on entity change: // Production fix at the parent call site {activeUser && ( setActiveUser(null)} /> )} Using a key boundary is cleaner and less error-prone than juggling multiple useEffect reset hooks inside the child component. Stop Fixing the Same Bugs Manually Fixing these edge cases prompt after prompt takes up a lot of code review time. One 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. We put together an open-source collection of accessible flat UI primitives for React 19 and Next.js 16 over at devpreflight.com to save developers from rebuilding these safeguards on every project. Top comments (0)

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.