On this page12 sections
Most layout differences after converting Framer to Next.js come from a few places: fonts loading late or failing, a width on a breakpoint boundary, fit and fill sizing changed while editing, sticky or fixed layers inside a clipped or transformed parent, viewport units on mobile, image fit modes, and code components that appear after the page renders. Each has a specific file to check and a CSS-level fix. The views and CSS modules are the same if you convert to TanStack Start, Astro, React Router or Vite, so these fixes apply there too.
This post assumes a project converted by Deframer and explains how its layout code is emitted, so you can find the cause instead of guessing. For how breakpoints are written out, read Framer breakpoints and responsive layouts in Next.js first; for notices, build errors and hydration warnings, see common conversion issues. Deframer is an independent tool and is not affiliated with, endorsed or sponsored by Framer.
How is layout emitted in the converted project?
Layout is plain CSS. Each view in src/views/ and each component in src/components/ has a CSS module beside it with the rules Framer published, under class names taken from the layer names. The kit (src/lib/deframer, imported as @/deframer) adds a base stylesheet, base.css, which reproduces the reset, rich text and legacy stack rules every published Framer page receives.
That has a practical consequence: if the converted page differs from the live one before you've edited anything, compare computed styles in DevTools first. The rule is usually the same; what differs is the font, the width, or the moment something rendered.
Symptom | Usual cause | Where to look |
|---|---|---|
Text reflows a moment after load | Web font arrives late or fails | |
Wrong layout at one width | Width on a breakpoint boundary | |
Box too wide or collapsed | Fit or fill sizing changed | The layer's rule in its |
Sticky layer scrolls away | Ancestor | Ancestors' rules, |
Fixed layer moves with content | Transformed ancestor | |
Section too tall on phones | | Rules using |
Image cropped or letterboxed | Fit mode, wrong pixel size | |
Layout jumps after load | Client-only code component | Its container's rule |
Scroll effect starts in the wrong place | Positions measured before a shift | The layer's |

Why does text shift or wrap differently while fonts load?
Because the browser draws text with a fallback font first and swaps in the web font when it arrives. The converted project is set up to make that swap invisible; when it isn't, a font file is failing or arriving late.
What the converter emits:
- One stylesheet per family in
src/styles/fonts/<family>.css, with the@font-facerules and file URLs the published page uses, including the sameunicode-rangesubsets. Each face keeps thefont-displaythe published page gave that file (some Framer faces useswap, others don't). - Metric-matched fallback faces in
src/app/globals.css, copied from the published site: rules such as@font-face { font-family: "Inter Placeholder"; src: local("Arial"); size-adjust: …; ascent-override: … }. They make Arial occupy the same space as the web font, so lines break in the same places before and after the swap. - Preconnects and preloads in the root layout: a preconnect to Framer's font CDN (and Google's, when used) and the font preloads the published page had.
Symptoms and fixes:
- Text visibly reflows on load. Open DevTools, Network, filter by Font, and reload with cache disabled. A font that returns 403 or 404 never swaps in. If you converted with "Assets in the project", check that the files in
src/styles/fonts/*.csspoint at files that exist inpublic/. - The fallback looks wrong (different width or height). Check that
src/app/globals.cssstill has the… Placeholderfaces and that your text styles still list them in their font stack (for example--df-font-family: Inter, Inter Placeholder, sans-serif). If you edited a text style and dropped the placeholder from the stack, the browser falls back to a generic font with different metrics. The placeholders rely on a local Arial; on a device without it, the next family in the stack is used, so test on the devices you care about. - A font changed after you renamed or replaced it. If you add a new font, add its
@font-faceto a file insrc/styles/fonts/and import it where the text style is defined. A family that no rendered component declares is loaded by the kit from the Google Fonts or Fontshare CSS API as a last resort, which can be slower than the published file. - Fonts are slower on the new host. If assets stay on Framer's CDN, nothing changed about the files. If you moved them into
public/, make sure your host serves them with long cache headers.
See Framer fonts and assets: CDN vs. local for the asset choices.
Why does a layout look wrong at one specific width?
Usually because that width sits in a different breakpoint range than you assumed. Each view declares its ranges at the top (const breakpoints = { Tablet: "(min-width: 810px) and (max-width: 1199px)", … }), and those exact ranges come from the Framer site.
What to check:
- Boundaries. Test both sides of each boundary (809 and 810, 1199 and 1200 in that example). A 768px window shows Phone when Tablet starts at 810px.
- Gaps between ranges. If you add media queries by hand with slightly different numbers, a few pixels can match no rule. In the kit, when none of a page's breakpoint queries match, the first breakpoint in the group is used, which may not be the layout you expect. Keep new media queries identical to the ones in the map.
- Hidden layers. Layers hidden at a breakpoint carry a class such as
hidden-phone. The server renders every breakpoint's version, a media-query rule hides the ones that don't apply, and a small inline script removes them from the DOM before React hydrates. If a layer appears twice while resizing, one of its copies is missing its hidden class; if it never appears, it has one too many. TheisDisplayed()check next to the layer must agree with its classes.
The breakpoints post has the full editing guide, including how to show or hide a layer at one breakpoint safely.
Why is a box too wide, too narrow or collapsed?
Framer's sizing modes are written as specific CSS values, and editing one of them by hand changes how the box responds to its content and parent.
In Framer | In the CSS module |
|---|---|
Fixed | |
Fit (content) | |
Fill | |
Relative | a percentage, such as |
Common cases:
- A text layer set to fit width never wraps. Text with auto (fit) width gets
white-space: nowrapfrom the kit's base CSS (.df-fit-text .df-text). That's the Framer behavior, but if a translation or a longer heading makes it overflow on phones, give the layer a fixed or fill width at that breakpoint instead. - A fill child collapses to zero.
flex: 1 0 0pxneeds a parent with a definite size along that axis. If you changed the parent from a fixed width tomin-content, the child has nothing to fill. - A stack's spacing changed. Stack gaps are
gapon the stack's rule (for examplegap: 24px), withflex-direction,justify-contentandalign-itemsalongside. Change the gap there, inside the right media query. Older components use the legacyStack, laid out bydata-stack-*attributes and rules inbase.css; there, gaps along the main axis apply only when items are packed, not when they're spread withspace-between. max-widthdisappears at large screens. Many Framer sections havemax-widthon an inner container (for examplemax-width: 1920px). If content stretches too wide on large monitors, look for that rule before adding a new breakpoint.
Why does a sticky layer scroll away, or a fixed layer move?
Sticky positioning breaks when an ancestor becomes a scroll container; fixed positioning breaks when an ancestor has a transform. Both are standard CSS behavior, and both are easy to trigger while editing.
The converted CSS keeps Framer's values: a sticky layer has position: sticky and a top offset in its module (or the positionSticky props on a kit Frame, which also add will-change: transform and z-index: 1). Clipping ancestors use overflow: var(--overflow-clip-fallback, clip). overflow: clip clips without creating a scroll container, so sticky children keep working; the kit switches the variable to hidden only in browsers that don't support clip.
- Sticky stopped working. Walk up the ancestors in DevTools and look for
overflow: hidden,autoorscroll. If you replacedclipwithhiddenanywhere, putclipback. Also check that the sticky element's parent is taller than the element; sticky has no room to move otherwise. - A fixed header scrolls with the page or is offset. A
transform,filterorwill-change: transformon any ancestor makes it the containing block forposition: fixedchildren. The kit addswill-change: transformto layers with running effects (appear, parallax, scroll transforms), as the original runtime does. Move the fixed element outside the animated layer, or remove the effect from its ancestor. - A sticky layer overlaps the wrong content. Check
z-indexon the sticky element and its siblings; kit frames setz-index: 1for sticky layers.
Why are sections too tall on phones?
Because vh on mobile browsers is the height of the viewport with the browser's toolbars retracted, so 100vh is taller than what you see when the address bar is showing (see MDN on viewport units).
The converted project keeps Framer's units: pages render with min-height: 100vh from the kit's page wrapper (asPage in src/lib/deframer/page.tsx), and layers sized to the viewport keep rules like height: 90vh. The converted site therefore behaves the same as the published one. If you'd rather fit the visible area:
.page .hero {
height: 100vh; /* fallback for older browsers */
height: 100svh; /* the viewport with toolbars shown */
}Use svh for "always fits", dvh to track the toolbar as it moves (it can cause resizing while scrolling), and keep the vh line first as a fallback. Change it per layer rather than globally, so desktop layouts stay as designed.
Why is an image cropped, stretched or letterboxed?
Image layers render an <img> inside an absolutely positioned wrapper, and the fit mode decides object-fit. The names don't match CSS, which causes most surprises.
| Framer setting | CSS |
|---|---|---|
| Fill | |
| Fit | |
| Stretch | |
| Tile | a repeating CSS background |
What to check:
- You replaced an image and it crops differently. The layer's size comes from its CSS rule, not from the image, so a new image with another aspect ratio is cropped to the old box. Change
fit, or change the box's size oraspect-ratio. - A layer sized by its image is the wrong height. Some layers take their height from the image (
fitImageDimension), using thepixelWidthandpixelHeightin thebackgroundprop to setaspect-ratio. When you swap thesrc, update both numbers to the new file's dimensions. - The image looks soft.
next.config.tssetsimages: { unoptimized: true }, so the browser picks from thesrcSetandsizeswritten into the code, which come from Framer's layout. If you made the image much larger than it was designed, thesizesvalue may now ask for a smaller file than needed; update it. - Images below the fold load late. Images further than 1000px down the page are marked
loading="lazy", as on the published site. Setloading: "eager"in thebackgroundprop for an image that must appear immediately.
Why does text wrap at a different word?
When the same font and width are in play, wrapping differences usually come from a text style's wrap setting, a font fallback, or a container that's a few pixels narrower.
- Balanced headings. Text styles and layers can set
--df-text-wrap-override: balance(ornone), which the kit'stext-wraprule reads. Balanced text breaks into lines of similar length, and browsers withouttext-wrap: balancesupport wrap normally. Check that variable on the layer before changing widths. - Fallback font still showing. If the web font failed, the fallback wraps differently despite matching metrics. Check the Network panel as described above.
- Per-breakpoint text sizes. Shared text styles set
--df-font-sizeand line height per breakpoint insrc/styles/text/. A one-pixel size difference changes where lines break; compare the computedfont-sizewith the live site at the same width. - Container width. Compare the text box's computed width with the live site. A changed padding on a parent narrows every child.
Why does the layout jump after the page loads?
Usually because a code component renders only in the browser and takes up space when it appears. The converter detects code that needs the browser to load or render (it reads window or document while rendering, imports a browser-only package, and similar) and loads it with the kit's clientOnly. It renders nothing on the server or during hydration, then mounts once its module has loaded. Overrides that need the browser use lazyOverride: the layer renders without the override first.
Two more timing rules apply:
- During the page's first 3 seconds, the kit's code boundaries wait for web fonts that are still loading before rendering code components and overrides, because many measure their text once at mount. Framer's own bundle usually reaches them after the fonts too.
- The result page lists these components under "Components that render only in the browser".
Fixes:
- Find the component's container in the view or component (the layer wrapping it, often a container with effects) and its rule in the CSS module.
- If the container's size is
min-content, give it the component's final size at each breakpoint: a fixedheight, amin-height, or anaspect-ratio. The space is then reserved before the component mounts. - Measure with the Performance panel or Lighthouse's layout shift diagnostic, before and after.
- If the component doesn't really need the browser to render, see code components and overrides in Next.js for how to make it server-safe.
Why do scroll effects start in the wrong place or not at all?
Scroll-linked effects run in the browser, so their starting positions depend on the layout at the moment they measure. The kit's withFX wrapper reproduces Framer's per-layer effects (appear, scroll transforms, parallax, loops) on motion/react; the server renders each effect's initial state, and the effect starts moving after hydration.
- Parallax is offset. Parallax reads the layer's position the first time it enters the viewport. A layout shift above it after that (a late font, a client-only component, an image without reserved space) leaves the offset stale. Fix the shift first, then recheck.
- Elements stay invisible. Appear effects start from their initial state, often
opacity: 0. Simple appear animations are started by an inline script before hydration, as Framer does; others start at hydration. If JavaScript fails on the page (check the console), those layers never appear. - Effects ignore reduced motion, or always skip it. The kit honors
prefers-reduced-motiononly when the site opted in, as Framer does;src/site/routes.tskeeps the site's reduced-motion setting. - Effects don't restart after client navigation. Effects re-subscribe per route. If one doesn't, reload the page directly to see whether it works on a full load, and note which layer it is.
Keeping Framer animations in Next.js with Motion covers editing the animation values themselves.
A short debugging routine
- Reproduce at the exact width, on
npm run build && npm start, not onlynpm run dev. - Open the live Framer page at the same width, side by side.
- Inspect the layer in both.
data-namein the converted page gives the Framer layer name; the class leads to the CSS module rule. - Compare computed values: font family actually used, width,
position,overflow,transform. - Fix the one rule in the matching media query, then check the other breakpoints.
FAQ
Why does my converted Framer site look different from the original?
Before any edits, differences usually come from fonts loading differently, a test width on a breakpoint boundary, or a browser-only component appearing late. Compare computed styles of the same layer on both pages to find which.
How do I stop layout shift from fonts after converting Framer?
Keep the metric-matched … Placeholder fallback faces in src/app/globals.css and in your text styles' font stacks, make sure every font file loads without errors, and keep the font preloads in the root layout.
Why is 100vh too tall on iPhone in my Next.js site?
Mobile browsers size vh to the viewport with toolbars hidden. Add height: 100svh (or dvh) after the 100vh line on the layers where it matters.
Why does my sticky header not work after editing the CSS?
An ancestor probably has overflow: hidden or auto, which makes it a scroll container. The converted CSS uses overflow: clip, which doesn't break sticky; restore it.
Why does a Framer code component appear late and push content down?
It needs the browser to render, so it mounts after hydration. Give its container a height, min-height or aspect-ratio per breakpoint so the space is reserved.
Where do I change Framer's fill and fit sizing in the converted code?
In the layer's rule in its .module.css: fit is min-content, fill is flex: 1 0 0px or width: 100%, fixed is a pixel value.
