Skip to content
BlackFig

v0 mobile layout broken: fixing a v0 app that only works on desktop

AntPublished 4 min read

Checked against v0's documentation on 9 November 2026.

Quick answer

v0 builds with Tailwind and shadcn/ui, which are mobile-first, but layouts prompted and previewed on a desktop often only add classes for wide screens. Rewrite the base classes for a 375px screen, add larger breakpoints on top, contain overflow, and stop layout-critical parts from waiting on JavaScript to detect a phone.

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

Symptom

There is no error message for this. The browser does exactly what the classes tell it to. The fix is in the classes.

Why it happens

Desktop-first prompts, mobile-first CSS

v0's FAQ says it builds with Next.js, React, TypeScript, Tailwind CSS and shadcn/ui. Tailwind is mobile-first: unprefixed classes apply to every screen size, and prefixed ones such as md: apply from that breakpoint up. Tailwind's docs spell out the trap: sm: does not mean "on small screens", it means "640px and wider". The default breakpoints are sm 640px, md 768px, lg 1024px, xl 1280px and 2xl 1536px.

When a layout is described and reviewed at desktop width, the base classes tend to be the desktop ones: grid-cols-3, w-[1200px], flex rows with no wrap. Nothing then tells the phone layout what to do.

The sidebar decides it is on a phone too late

shadcn's Sidebar switches to a sheet on mobile using a useIsMobile hook. In the shadcn source, that hook uses a 768px breakpoint and starts as undefined, returning false until an effect measures the window. On the server and the first client render, the app believes it is on desktop. On a phone that shows up as the desktop sidebar flashing before it collapses.

Design Mode works at desktop width

v0's Design Mode lets you select an element and change spacing, type and colour, and it surfaces Tailwind-compatible values. v0's docs say it is not available on mobile viewports. If a padding change is applied as an unprefixed class, it changes the phone layout too, without you seeing it.

Overflow nobody contained

Wide tables, long unbroken strings such as emails or IDs, and flex children that refuse to shrink all force horizontal scrolling. A flex child's minimum width defaults to its content, so one long word can widen the whole page.

The fix

  1. Look at the real thing. Open the branch preview URL on your phone, and use your browser's responsive mode at 375px wide. v0 can also open the app it builds and send screenshots as it works, so ask it to check a page on a narrow screen and report what overflows, then confirm on a real device.

  2. Rewrite base classes for the phone, then add breakpoints. A typical v0 dashboard grid, before and after:

// Before: three columns everywhere, fixed width
<div className="grid w-[1200px] grid-cols-3 gap-8">
  {stats.map((s) => <StatCard key={s.id} {...s} />)}
</div>
 
// After: one column on phones, two from 640px, three from 1024px
<div className="mx-auto grid w-full max-w-6xl grid-cols-1 gap-4 px-4 sm:grid-cols-2 lg:grid-cols-3 lg:gap-8">
  {stats.map((s) => <StatCard key={s.id} {...s} />)}
</div>

The same pattern fixes headers and toolbars: stack by default, go horizontal at a breakpoint.

<header className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
  <h1 className="text-xl font-semibold sm:text-2xl">Bookings</h1>
  <div className="flex flex-wrap gap-2">{actions}</div>
</header>
  1. Contain overflow where it starts. Give tables their own scroll area, and let flex children shrink:
<div className="w-full overflow-x-auto">
  <table className="w-full min-w-[640px] text-sm">{/* rows */}</table>
</div>
 
<div className="flex items-center gap-3">
  <Avatar />
  <div className="min-w-0">
    <p className="truncate font-medium">{user.email}</p>
  </div>
</div>

For data-heavy screens, a card list below md is often kinder than a sideways-scrolling table.

  1. Use CSS, not the hook, for anything visible on first paint. Keep useIsMobile for behaviour, such as whether a tap opens the sheet. For what renders first, let breakpoints decide:
// components/app-shell.tsx
export function AppShell({ children }: { children: React.ReactNode }) {
  return (
    <div className="min-h-screen md:grid md:grid-cols-[16rem_1fr]">
      <aside className="hidden border-r md:block">
        <DesktopNav />
      </aside>
      <div className="md:hidden">
        <MobileNavSheet />
      </div>
      <main className="min-w-0 p-4 md:p-8">{children}</main>
    </div>
  );
}

Keep custom breakpoints aligned with the 768px the hook uses, or the two will disagree between 768px and your lg width.

If you keep shadcn's Sidebar, know how its phone mode works. Its collapsible prop accepts offcanvas, icon or none, but on mobile the component renders a sheet controlled by openMobile and setOpenMobile from useSidebar. Check whether that sheet closes when someone taps a link. If it stays open, it covers the page they asked for. Close it on navigation:

"use client";
 
import Link from "next/link";
import { useSidebar } from "@/components/ui/sidebar";
 
export function NavLink({ href, children }: { href: string; children: React.ReactNode }) {
  const { isMobile, setOpenMobile } = useSidebar();
  return (
    <Link href={href} onClick={() => isMobile && setOpenMobile(false)}>
      {children}
    </Link>
  );
}
  1. Leave zoom alone. Next.js sets the viewport meta tag automatically, and its docs say manual configuration is usually unnecessary. Search for export const viewport and remove maximumScale: 1 or userScalable: false if they appear; blocking zoom hurts people who need larger text.

How to prevent it

  • Save a v0 Instruction for layout. v0's own docs use this as the example: a title of "Mobile first" with the rule to design for mobile screens first, then adapt for larger screens. Create it from the + menu in the prompt bar under New Instruction, and keep it checked.
  • Put the phone in the prompt. v0's prompting docs suggest asking for a "mobile-first responsive layout" by name.
  • After any Design Mode change, open the diff and check whether the new classes carry a prefix.
  • Review every branch preview on a phone before you select Publish.

My general guide to mobile layouts in AI-built apps covers touch targets, forms and safe areas. Layout classes scattered across many files are often a sign of a wider structure problem, covered in why v0 code gets messy. For styling shadcn so it stops looking generic, see customising shadcn/ui.

When to get help

Mobile problems are rarely one screen. If every page needs the same treatment, it is quicker to fix the shell and shared components once. The free vibe code health check shows how far it goes, and a Rescue Sprint fixes it across the app at a fixed price. For the wider picture, start with the guide to fixing vibe-coded apps.

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

Can I use v0's Design Mode to fix mobile layout?

Only partly. v0's docs say Design Mode is not available on mobile viewports, so you edit from a desktop-sized view. Check the classes it applies, because an unprefixed change affects every screen size.

Why does the sidebar flash open on my phone?

shadcn's useIsMobile hook starts as undefined and only measures the window after the first render, so the server and first client render use the desktop layout. Use CSS breakpoints for anything that must look right on first paint.

Do I need to add a viewport meta tag to a v0 Next.js app?

Usually not. Next.js sets the viewport meta tag automatically. Check that nobody has exported a viewport that disables zoom.

Ant

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