I spent an embarrassing amount of time staring at my laptop, convinced my animation setup was broken. The navbar slid away on scroll in Chrome, but animations weren't working on my phone at all when I tested over Wi-Fi. Responsive mode looked perfect on desktop. Then I opened the same URL on a real device and nothing moved. No fade-ins, no slide-out header, just a static page like the JavaScript had given up halfway through.

If that sounds familiar, you are not alone. I have hit this on every new Next.js project I spin up, not just this blog.

DevTools responsive mode showed smooth animations; my phone on the same LAN showed a frozen layout. Same code, same Wi-Fi, completely different result.

The symptom

Here is the pattern, every time:

  • Desktop at localhost:3000, including Chrome DevTools device emulation: animations work.
  • Real phone at something like http://192.168.2.101:3000 while npm run dev is running: animations do not fire, or they feel stuck.
  • Same phone, same URL, but after npm run build and npm run start: animations work again.

In short: animations work on desktop but not mobile when you are testing through the dev server over your local network.

The phone and laptop are on the same network. The code is identical. Only the way the app is being served changes.

To reproduce it yourself, start your dev server, find your machine's local IP, and open that address on a physical device, not emulation. If next dev is not working on a real device the way it does on your Mac, scroll around, toggle menus, and trigger whatever motion you added. Then run a production build locally and try again. If your experience matches mine, the second pass is the one that actually moves.

Why this happens

This one surprised me because I kept tweaking animation code that was never the problem.

On my blog (Next.js 16, Framer Motion 12, next dev --webpack), the issue turned out to be the development server, not a bad initial prop or a hydration mismatch. When you run npm run dev, Next.js keeps a WebSocket open for hot module replacement. It serves JavaScript in chunks and recompiles on the fly. That setup is great at your desk. Over a local IP on a real phone, it is less reliable. The connection can lag or interrupt, and client-side JavaScript (including Framer Motion) may not run the way you expect.

npm run build followed by npm run start gives you an optimized bundle with no HMR and no dev WebSocket in the loop. On the phone, that behaves consistently.

Desktop responsive mode does not catch this because you are still on localhost with a stable dev connection. It is not the same as a phone fetching your app across the LAN while the dev server is juggling hot reload.

Worth noting: a deployed site on Vercel behaves like npm run start, not like npm run dev. If animations work in production but not during local mobile testing, that fits this pattern.

The fix

When I want to check motion on a real device, I stop trusting the dev server and run a local production build instead. From the project root, on the same Wi-Fi as my phone:

  1. nvm use — switches to Node 20 (this repo has an .nvmrc). Newer Node versions have broken next build for me before, so I stick to what the project expects.

  2. npm run build — compiles the app the same way production does: static pages, optimized JavaScript, no dev middleware in the bundle.

  3. npm run start -- -H 0.0.0.0 — serves the production build and listens on all network interfaces, so my phone can reach it. The 0.0.0.0 part is for the server only. On the phone I open http://192.168.x.x:3000 with my Mac's actual IP, not 0.0.0.0.

When it works, the terminal looks something like this:

code
$ npm run build
 Compiled successfully
 Generating static pages (12/12)
 
$ npm run start -- -H 0.0.0.0
 Next.js 16.0.0
  - Local:        http://localhost:3000
  - Network:      http://192.168.2.101:3000
 
 Ready in 412ms

If port 3000 is already taken:

code
# Find what's using port 3000
lsof -nP -iTCP:3000 -sTCP:LISTEN
 
# Kill it (replace PID with the actual process id)
kill -9 <PID>
 
# Or just use a different port
npm run start -- -H 0.0.0.0 -p 3001

On the phone, the navbar now hides smoothly on scroll, exactly like it does on desktop.

My day-to-day workflow is unchanged: npm run dev on the Mac for speed. I only run the build-and-start flow when I need to verify animations, scroll behavior, or anything else that depends on client JavaScript behaving like it will in production.

Takeaway

Responsive mode is useful, but it is not a real phone on your home network. When something works on desktop and dies the moment you type your LAN IP into Safari, try a local production build before you refactor your animation components. You might save yourself an afternoon of debugging code that was fine all along.