AI编码
dev.to ·
@DevPreFlight
为什么 AI 编码助手写出的 CSS 上线后会出问题,以及如何修复
文章指出,使用 Cursor、Claude 或 Copilot 生成前端代码时,模型擅长语法但常跳过防御性 CSS 模式,导致部署后出现按钮加载时布局跳动、长用户名溢出容器、禁用按钮 tooltip 失效、小屏文字被截断等视觉 bug。作者列出 AI 编码助手几乎每次都会引入的五类视觉问题,并给出对应的具体代码修复方案。
AI推荐理由 面向使用 Cursor、Claude、Copilot 做前端开发的工程师,指出 AI 生成的 CSS 在本地看似正常但上线后出现布局跳动、长用户名溢出、禁用按钮 tooltip 失效、小屏文字截断等问题,并给出五类视觉 bug 的具体修复代码。属可操作的工程经验,对 AI 编码工具落地有实际参考价值,但为个人经验总结,未提供系统性测试数据。
66
正文
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"}
Enter fullscreen mode
Exit fullscreen mode
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 && (
)}
Enter fullscreen mode
Exit fullscreen mode
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}
Enter fullscreen mode
Exit fullscreen mode
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}
Enter fullscreen mode
Exit fullscreen mode
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
Enter fullscreen mode
Exit fullscreen mode
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
Enter fullscreen mode
Exit fullscreen mode
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
Enter fullscreen mode
Exit fullscreen mode
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
Enter fullscreen mode
Exit fullscreen mode
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)} />
);
}
Enter fullscreen mode
Exit fullscreen mode
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)}
/>
)}
Enter fullscreen mode
Exit fullscreen mode
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.