Skip to content
BlackFig

Fixing broken mobile layouts in AI-generated UIs

AntPublished Updated 6 min read

Quick answer

AI builders usually design in a wide desktop preview, so the base classes describe the desktop layout and phones get whatever is left. Fix it mobile-first: make the unprefixed Tailwind classes the phone layout, add sm: and md: for wider screens, let wide content scroll inside its own box, and swap h-screen for min-h-dvh.

On this page
  1. Symptom
  2. Why it happens
  3. The fix
  4. How to prevent it
  5. When to get help

Symptom

You open your app on a phone, usually after a friend tells you something looks off. The page wobbles sideways when you scroll. A table runs off the right edge. A modal is taller than the screen and its close button is out of reach. The main button on the last step sits behind the browser toolbar. Nothing crashes, so nothing shows up in the console.

The usual list I see on a phone:

  • The whole page scrolls sideways by a few pixels, or by a lot.
  • Three-column grids squeezed into three columns of one word each.
  • Data tables cut off, with no way to see the last columns.
  • Full-height screens with the bottom cropped under the browser toolbar.
  • Navigation links wrapping onto two or three lines.
  • Buttons and icon links too small to hit with a thumb.
  • Menus that only open on hover, so they never open on a touch screen.

Why it happens

AI builders are good at producing a screen that looks right in the preview. The preview is a wide panel on your laptop, so that is the screen it gets right. Phones are an afterthought, patched in when you ask.

Tailwind is mobile-first. Its responsive design docs are clear that unprefixed classes apply at every screen size, and a prefix like md: applies at that breakpoint and above. The default breakpoints are sm at 40rem (640px), md at 48rem (768px), lg at 64rem (1024px), xl at 80rem (1280px) and 2xl at 96rem (1536px).

Generated code often gets this backwards. It writes the desktop layout as the base classes, then adds a few sm: tweaks because it reads sm: as "small screens". The Tailwind docs warn about exactly this: sm: means "at the small breakpoint and wider", not "on phones". So the phone gets the desktop layout, minus nothing.

Four other causes come up again and again:

  • Fixed widths. Arbitrary values like w-[800px] or min-w-[600px] are quick ways to make a desktop preview look tidy. On a phone that is 390px wide, they force the page to scroll sideways.
  • Flex children that refuse to shrink. A flex item will not shrink below the width of its content by default. One long email address, URL or unbroken string in a card pushes the whole row wider than the screen.
  • h-screen on full-height layouts. In Tailwind, h-screen is height: 100vh. MDN notes that vh is currently equivalent to lvh, the large viewport, which assumes the browser toolbar is hidden. When the toolbar is showing, the bottom of the element is covered.
  • Hover-only interactions. Dropdowns and tooltips that open on hover have no equivalent on a touch screen.

The fix

Work through these in order. Each one is a small change, and together they cover most of what I see in AI-built apps.

1. Find what is too wide

Open the app at phone width and paste this into the browser console. It logs every element that sticks out past the right edge of the screen.

// Run in the browser console at phone width
const viewport = document.documentElement.clientWidth;
document.querySelectorAll("body *").forEach((el) => {
  if (el.getBoundingClientRect().right > viewport + 1) console.log(el);
});

Fix the outermost element in the list first. The rest are often its children and go away with it.

2. Rewrite the base classes mobile-first

Make the unprefixed classes describe the phone, then add columns as the screen gets wider.

// Before: three columns on every screen, including phones
<div className="grid grid-cols-3 gap-8">{cards}</div>
 
// After: one column on phones, two from 640px, three from 1024px
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 lg:gap-8">
  {cards}
</div>

The same pattern fixes side-by-side layouts. Stack them on phones and put them in a row from md up.

<section className="flex flex-col gap-6 md:flex-row md:items-start">
  <aside className="md:w-64 md:shrink-0">{filters}</aside>
  <div className="min-w-0 flex-1">{results}</div>
</section>

3. Remove fixed widths and let flex items shrink

Swap pixel widths for a full width with a maximum. Add min-w-0 to flex children that hold text, and let long strings wrap.

// Before
<div className="w-[800px]">...</div>
 
// After: fills a phone, stops growing at 48rem on wider screens
<div className="w-full max-w-3xl">...</div>
 
// Long emails and URLs inside a flex row
<div className="flex items-center gap-3">
  <Avatar />
  <p className="min-w-0 wrap-anywhere">{user.email}</p>
  {/* wrap-anywhere needs Tailwind v4.1 or later; on v3, use break-words */}
</div>

A quick search for w-[ and min-w-[ across the project finds most of the culprits.

4. Give wide tables their own scroll

Some content really is wider than a phone. WCAG's Reflow criterion makes an exception for data tables, which need two dimensions to make sense. So let the table scroll sideways inside its own box, and keep the page itself still.

<div className="-mx-4 overflow-x-auto px-4 sm:mx-0 sm:px-0">
  <table className="w-full min-w-[640px] text-left text-sm">
    {/* thead and tbody as before */}
  </table>
</div>

For lists people scan rather than compare, like orders or bookings, a stacked card on phones usually reads better than a table. Render cards below md and the table from md up.

<ul className="space-y-3 md:hidden">
  {orders.map((o) => (
    <li key={o.id} className="rounded-lg border p-4">
      <p className="font-medium">{o.customer}</p>
      <p className="text-sm text-muted-foreground">{o.total} · {o.status}</p>
    </li>
  ))}
</ul>
<table className="hidden w-full md:table">{/* full table */}</table>

5. Use dynamic viewport units for full-height screens

Replace h-screen with min-h-dvh on page shells, and cap modals and sheets at a fraction of the dynamic viewport so their content scrolls inside them.

// Page shell: fills the visible screen and can still grow
<main className="flex min-h-dvh flex-col">...</main>
 
// Modal body: never taller than the visible screen
<div className="max-h-[85dvh] overflow-y-auto p-6">...</div>

MDN points out that dynamic units can change while the user scrolls, which can cause content to resize. Use min-h-dvh for shells and modals, not for every element on the page.

6. Make tap targets big enough

WCAG 2.2's Target Size (Minimum) asks for at least 24 by 24 CSS pixels for pointer targets, with some exceptions. The enhanced criterion uses 44 by 44, which is what I aim for on primary actions and icon buttons.

// Icon button: 44px square on every screen
<button
  type="button"
  aria-label="Open menu"
  className="inline-flex size-11 items-center justify-center md:hidden"
>
  <MenuIcon className="size-5" />
</button>

While you are there, turn hover menus into menus that open on a tap or click, and check that every tooltip's information is also available without hovering.

7. Check the viewport tag, then test on a real phone

Next.js sets the viewport meta tag for you, and its docs say the default is usually enough. In a Vite or plain React app, check index.html has the tag the Tailwind docs give:

<meta name="viewport" content="width=device-width, initial-scale=1.0" />

Leave out maximum-scale=1 and user-scalable=no if you find them. They stop people pinching to zoom, which people with low vision rely on.

Then test. Chrome's docs describe device mode as a first-order approximation, not the real thing. Use it at 320px wide, the width WCAG's Reflow criterion uses, and then do the whole journey on your own phone: sign-up, the main task, payment, and any form with a keyboard open.

How to prevent it

Most mobile bugs come from the order things were built in, so change the order.

  • Prompt for the phone first. Ask for the layout at phone width, then ask for the tablet and desktop changes as a second step. Name the breakpoints you want.
  • Preview at phone width every time. If your builder's preview can switch to a phone width, make that the default view, not the final check.
  • Ban fixed pixel widths. Arbitrary width values should be rare enough that each one needs a reason.
  • Share layout components. A single page shell, card and table wrapper, each fixed once, beats forty screens fixed one by one. This is where a small design system pays for itself.
  • Add a phone check to every release. Five minutes on a real phone before each deploy catches most regressions.

When to get help

If the fixes above solve a handful of screens, you probably don't need anyone. If every screen has its own version of the same problem, the issue is structural: no shared layout, no consistent breakpoints and fixed widths everywhere. Patching that one prompt at a time tends to break something else, which is the fix loop I describe in my guide to fixing a vibe-coded app.

Mobile layout sits next to two other problems that usually show up together. Missing error handling is the next most common thing I find, and the vibe-code rescue checklist covers the rest. If you want a quick read on where your app stands, the free vibe-code health check runs a mobile PageSpeed test on your live URL.

When you would rather hand it over, a Rescue Sprint covers mobile layout alongside auth, data, payments and errors, at a fixed price agreed after a technical assessment (from £750).

Sources

The vibe-code rescue checklist, as a PDF

About 40 checks across auth, data, payments, errors, mobile and secrets, on two pages.

You get the PDF now and two short follow-ups over the next week. Reply “stop” and the rest never arrive.

Questions founders ask

Why does my app look fine in the builder but broken on my phone?

The builder preview is usually a wide panel on a laptop, so every screen was judged at desktop width. Phones also have browser toolbars that come and go and a much narrower viewport, and a desktop preview shows neither.

Should I use h-screen or h-dvh in Tailwind?

For full-height layouts on phones, prefer min-h-dvh. h-screen is 100vh, which current browsers treat as the large viewport, so the bottom of the screen can sit behind the browser toolbar. dvh follows the visible area as the toolbar shows and hides.

Do I need a native app to fix mobile problems?

Almost never at this stage. A responsive web app that works properly on a phone is cheaper to build, quicker to change and good enough to test whether people want the product.

Is Chrome's device toolbar enough for testing?

It is a good first pass, but Chrome's own docs call it a first-order approximation. Always finish on a real phone, including sign-up, forms and payment.

Ant

Product designer who builds MVPs, based in Nottingham. 10+ years designing digital products, and the only person at BlackFig. More about me