I had Framer Motion animations broken on my phone while they looked perfect on my Mac. Same Wi-Fi, same project, same commit. Scroll reveals on the homepage stayed invisible. The sticky header on inner pages would not slide away. The mobile menu opened like a static div with no transition. I opened the site on my laptop at localhost:3000 during npm run dev and everything moved. I ran npm run build and npm run start, opened the same LAN URL on my phone, and the animations worked again.

That gap is what makes this bug so annoying. Animations not working in dev but working in production does not point you at Framer Motion first. It points you at your animation code, your viewport settings, Safari being Safari. I spent time in the wrong places before I scrolled my terminal.

What I was seeing

The pattern was consistent:

  • Desktop at localhost:3000 with npm run dev: animations fine, including Chrome DevTools responsive mode.
  • Real phone at http://192.168.1.183:3000 with npm run dev: dead. Sections stuck at opacity: 0. Navbar frozen. Mobile menu with no motion.
  • Same phone, same URL after npm run build and npm run start: everything animated again.

Works on desktop, not on phone. Same device, same network, prod works but dev does not. I kept asking whether whileInView was the problem, or hydration, or iOS quirks. The animation components were not broken. The dev environment was refusing to finish wiring up the client.

If you are googling "framer motion animations broken on phone" or "animations not working on mobile" while testing over your home network, this might be your issue before you touch a single initial prop.

The terminal had the answer

Before you refactor a scroll reveal component or rip out AnimatePresence, scroll your dev server output.

Mine was printing this on every phone refresh:

code
⚠ Blocked cross-origin request to Next.js dev resource /_next/webpack-hmr from "192.168.1.183".
Cross-origin access to Next.js dev resources is blocked by default for safety.
 
To allow this host in development, add it to "allowedDevOrigins" in next.config.js and restart the dev server:
 
// next.config.js
module.exports = {
  allowedDevOrigins: ['192.168.1.183'],
}

That warning is not decorative. Next.js cross-origin HMR blocked from your LAN IP is the whole story. I had been staring at React components when the server was telling me exactly what to add to next.config.

Why Next.js blocks this

Starting in Next.js 15, the dev server moved from warning about cross-origin dev resource requests to blocking them by default. The goal is security: random sites should not be able to hit your local dev server's hot reload endpoint.

When you open the app on your phone at http://192.168.1.183:3000, the browser origin is 192.168.1.183, not localhost. The dev client still tries to open a WebSocket to /_next/webpack-hmr. Next.js treats that as a cross-origin dev resource request and refuses it unless you allowlist the origin.

The official config option is allowedDevOrigins. Docs: allowedDevOrigins. The behavior was tightened as part of the broader dev-server hardening in recent Next.js releases; if you want the historical context, the Next.js GitHub repo and release notes around cross-origin dev access are worth a skim.

On my machine, localhost was trusted automatically. My phone's LAN IP was not.

Why it kills Framer Motion

This is not "HMR is nice to have." When the WebSocket to /_next/webpack-hmr gets blocked, the dev client cannot fully bootstrap. Hydration does not complete reliably. Anything that depends on the client runtime after load sits in a broken middle state.

On a typical Next.js 16 app with Framer Motion 12 and next dev --webpack, that shows up in familiar places:

A scroll reveal using whileInView — a <FadeIn>, <AnimatedSection>, or plain motion.div with initial="hidden" (opacity: 0, y: 20). If the Framer Motion client runtime never mounts properly, those sections stay invisible. On a homepage where the navbar is hidden anyway, this is often the clearest signal. No header to blame. Just sections that never fade in. Classic whileInView not triggering on mobile panic, except the viewport math was fine.

The navbar itself uses motion.header with scroll-driven animate={{ y: ... }} and an AnimatePresence mobile menu. Without a healthy client hydration path, none of that runs. The header looks rendered but static.

Production worked because npm run start does not use the dev HMR pipeline at all. No cross-origin gate on /_next/webpack-hmr. The optimized bundle hydrates normally. That is why npm run dev animations broken on a phone can still look perfect after a local production build on the same device.

The fix

Add your phone's LAN IP to allowedDevOrigins in next.config.ts. Restart the dev server. That is the entire fix for the HMR block.

Plain config:

code
import type { NextConfig } from "next";
 
const nextConfig: NextConfig = {
  allowedDevOrigins: ["192.168.1.183"],
};
 
export default nextConfig;

If your project wraps config with Contentlayer or another plugin, the allowlist goes on the inner nextConfig object, not outside the wrapper:

code
import type { NextConfig } from "next";
import { withContentlayer } from "next-contentlayer2";
 
const nextConfig: NextConfig = {
  allowedDevOrigins: ["192.168.1.183"],
  pageExtensions: ["js", "jsx", "mdx", "ts", "tsx"],
  // ...rest of your config
};
 
export default withContentlayer(nextConfig);

Replace 192.168.1.183 with your machine's current IP. DHCP can reassign it after a router reboot. When that happens, update the array and restart again.

After I added allowedDevOrigins next.config entry and restarted, my phone got the same smooth dev experience as my Mac. No production build required for day-to-day mobile testing.

LAN dev checklist

Find your Mac's LAN IP:

code
ipconfig getifaddr en0
StepWhat to do
Find your IPRun the command above (or check Network settings)
Allowlist itAdd the IP to allowedDevOrigins in next.config.ts
Restart devStop and rerun npm run dev (config changes are not hot-reloaded)
Bind to all interfacesIf the phone cannot reach the server, run npm run dev -- -H 0.0.0.0
Watch the terminalRefresh on the phone; confirm the blocked HMR warning is gone
Test homepageOpen / on the phone; scroll reveal sections should fade in on scroll
Test navbarOpen a page with a sticky header; scroll down and back up; the navbar should hide and return

Use http://192.168.x.x:3000 on the phone, not 0.0.0.0. The bind address is for the server, not the browser.

About npm run build and npm run start

Running a local production build on your LAN is still a valid way to preview prod behavior on a real device. Optimized bundles, no dev middleware, closer to what Vercel serves.

But it bypasses HMR entirely. It does not fix your dev environment. It just avoids the dev pipeline that was failing.

I wrote an earlier post about using npm run build and npm run start -- -H 0.0.0.0 when animations look dead on a phone: Next.js Animations Broken on Mobile? Check the Dev Server. That workflow helped me ship with confidence, but it masked the real issue. The production path worked because it never hit the cross-origin HMR block. Once I found allowedDevOrigins, I could test Framer Motion on my phone during normal npm run dev again.

My split now: npm run dev on the Mac for speed, with allowedDevOrigins set when I test on a phone. Production build on LAN only when I specifically want to validate prod bundles or performance.

If the fix does not fully work

After allowlisting your IP and confirming the terminal warning is gone, most of the "animations not working on mobile" symptoms should disappear in dev. If something still looks off, these are the secondary suspects I keep in my back pocket:

  1. whileInView and Safari's dynamic viewport. Mobile Safari's URL bar changes the visible viewport as you scroll. An Intersection Observer with a tight margin can miss triggers. Something like viewport={{ once: true, margin: '-40px' }} on a fade-in section is a common setup. If sections stay faded only in Safari after HMR is fixed, loosen the margin or test with remote debugging open.

  2. sticky plus transform on iOS. The navbar is sticky top-0 with Framer driving translateY. iOS has a long history of odd compositing with sticky and transforms together.

  3. backdrop-blur plus motion. The navbar uses backdrop-blur-md. Combined with transforms, Safari mobile can look janky even when animations technically run.

  4. Reduce Motion. iOS Settings → Accessibility → Motion → Reduce Motion disables or alters many animations system-wide.

  5. Remote debugging. Safari Web Inspector on iPhone, or chrome://inspect on Android. Look for hydration errors, chunk load failures, or WebSocket errors. If HMR is truly unblocked, you should not see fresh /_next/webpack-hmr blocks on each refresh.

Fix the config first. Then chase browser quirks.

Takeaway

If Framer Motion works on desktop but not on your phone during npm run dev, and a production build on the same phone works fine, read your terminal before you rewrite animation code. Next.js 16 will tell you when cross-origin HMR is blocked. Add your LAN IP to allowedDevOrigins, restart the server, and test again. You might save an afternoon of debugging components that were never the problem.