On this page5 sections
When Deframer converts a published Framer site to a Next.js App Router project in TypeScript, each Framer breakpoint (for example Desktop, Tablet and Phone) stays a separate layout, expressed as CSS media queries. Layers that are hidden at a breakpoint get a class such as hidden-phone that a media query hides before any JavaScript runs, so there is no flash of the wrong layout. Shared text styles change size per breakpoint inside their own CSS classes. You edit responsive behavior the way you would in any CSS-modules project: find the layer's class and change the rule inside the right media query.
This post walks through where each piece lives, how to change a layout without breaking the others, and how to test the result.

How the breakpoints are represented
In Framer you design a primary breakpoint and then adjust the others. The converted project keeps that model, but writes it out as ordinary CSS and TypeScript, so there is no editor-only state left. (Deframer is an independent tool, not affiliated with Framer.)
The breakpoint map in each view
Every page component in src/views/ declares its breakpoints by their human names, with the media query each one uses. In a real converted project, the home view starts like this:
const breakpoints = {
Tablet: "(min-width: 810px) and (max-width: 1199px)",
Phone: "(max-width: 809px)",
Desktop: "(min-width: 1200px)",
} as const;
/** Classes that hide a layer at a breakpoint (the kit styles them per media query). */
const hiddenClassNames = { Tablet: "hidden-tablet", Phone: "hidden-phone", Desktop: "hidden-desktop" };The exact widths come from your site. If your Framer project used different breakpoint widths, the converted ranges match those instead.
Layout differences live in the CSS module
Each view has a CSS module next to it (home.tsx and home.module.css). Class names follow the layer names from Framer, and breakpoint-specific rules sit in media queries at the end of the file:
@media (min-width: 810px) and (max-width: 1199px) {
.page .heroSection,
.page .ctaWrap {
padding: 100px 0px 0px 0px;
}
}Padding, gaps, direction, sizes and positions that differ per breakpoint are all here. Components in src/components/ follow the same pattern, each with its own CSS module.
Content differences use breakpoint overrides
Some differences are not CSS: a different image on Phone, a different sizes value, or different props on a component. For those, the converted view wraps the layer in the kit's PropertyOverrides and passes the values per breakpoint name:
<PropertyOverrides
breakpoint={baseVariant}
overrides={{
Phone: {
background: { alt: "Brand Logo", src: "…", srcSet: "…", sizes: "…" },
},
}}
>
{/* the Desktop version of the layer */}
</PropertyOverrides>The default props on the child are the primary breakpoint's; the overrides object lists only what changes.
Hidden layers
A layer that Framer hides at a breakpoint carries that breakpoint's hidden class. The kit, imported as @/deframer, styles those classes inside the matching media query, so the server-rendered HTML already looks right at every width. After hydration, breakpoint changes come from matchMedia, so resizing the window switches layouts as it did on the published site.
Text styles per breakpoint
Shared text styles live in src/styles/text/ as global classes (for example text-body-16 or text-body-2). Each one sets its font size, line height and spacing as CSS variables, then overrides them per breakpoint:
@media (max-width: 809px) and (min-width: 0px) {
.text-body-2-scope .text-body-2:not(.rich-text-wrapper) {
--df-font-size: 15px;
}
}Changing a text style here changes it everywhere that style is used, which matches how text styles work in Framer.
Editing a layout safely
The safest edits are local: one layer, one breakpoint.
- Open the page in your browser with
npm run devand resize to the breakpoint you want to change. - Inspect the element. The
data-nameattribute tells you the Framer layer name, and the class name points to the rule in the CSS module. - Open the view's or component's
.module.cssfile and find that class. - Change the rule inside the matching media query. If there is no rule for that breakpoint yet, add one inside the existing
@mediablock rather than creating a new block with different numbers. - Check the other breakpoints, then commit.
Two habits help. First, keep media query ranges identical to the ones already in the file; a rule written for max-width: 800px next to a block for max-width: 809px leaves a 9-pixel gap that is hard to spot. Second, change shared text styles in src/styles/text/ only when you mean to change them sitewide; for a one-off size, add a rule on the layer's own class.
Hiding or showing a layer at one breakpoint
A layer hidden at some breakpoints has two parts that must agree: the hidden classes on its className, and a small check that decides whether React renders it once the breakpoint is known. In the converted home view it looks like this:
const isDisplayed = () => {
if (!inBrowser()) return true;
if (["Tablet", "Phone"].includes(baseVariant)) return false;
return true;
};
// …
{isDisplayed() && (
<ContainerWithFX className={cx(styles.slideshow, "hidden-tablet", "hidden-phone")} /* … */ />
)}The server renders the layer with its classes, CSS hides it at the right widths, and after hydration the check removes it from the tree. To show a layer on Phone that is currently hidden there, remove hidden-phone and drop "Phone" from the list in its check. To hide a layer at one breakpoint, the simplest safe option is a plain display: none rule on its class inside that breakpoint's media query in the CSS module.
Adding a breakpoint carefully
Adding a whole new breakpoint, say a wide desktop layout above 1600px, touches several places: the breakpoints map in each view, the hiddenClassNames map, the media queries in every CSS module that should change, the text styles, and any PropertyOverrides that need values for it. That is a real refactor, so plan it:
- Decide whether you need a new breakpoint at all. Many wide-screen tweaks are a single media query on a container's
max-widthor padding, added to the relevant CSS module, with no change to the breakpoint maps. - If you do need one, pick a range that does not overlap existing ones and write it down once.
- Add the CSS first (it is visible immediately and easy to review), then the TypeScript maps.
- Update text styles last, and only those that should differ.
If you still design in Framer and plan to convert again later, it can be simpler to add the breakpoint in Framer, republish, and convert again into a separate branch. Converting is free, but a republished site makes a new project, and downloading it uses a credit (see how credits work). The post on maintaining a converted site with Git covers merging a new conversion with your own edits.
Testing widths
When a live preview is available, the Deframer compare view shows the original and converted site side by side at 1440, 768 and 390 pixels wide, for up to 10 minutes. Those are good widths to check after any edit too, in the compare view or with npm run dev, with one caveat: they don't always land in the breakpoint you expect. In the example above, Tablet starts at 810px, so a 768px window shows the Phone layout. Read your view's breakpoints map before deciding which layout a width should show.
A practical checklist:
- Check 1440, 768 and 390 in your browser's responsive mode.
- Check each boundary on both sides: with the ranges above, 809 and 810, then 1199 and 1200.
- Resize slowly across each boundary and watch for layers that appear twice or not at all; that usually means a hidden class or an override is missing.
- Run
npm run buildandnpm start, then repeat on the production build. Development mode is slower and can hide timing differences. - Test on a real phone once. Touch targets, safe areas and font rendering differ from a desktop emulator.
If something looked different before your edit, compare it against the published Framer site at the same width. The post on common conversion issues explains the notices the result page can show. Once layouts look right, move on to URLs and metadata with SEO when moving off Framer.
FAQ
Does the converted project use CSS media queries or JavaScript for breakpoints?
Both, in a defined order. Layouts and hidden layers are handled by CSS media queries, so the server HTML is correct before JavaScript runs; after hydration, breakpoint changes are detected with matchMedia.
Why does my 768px test show the phone layout?
Because the site's Tablet range probably starts above 768px. Check the breakpoints map at the top of the view; in many Framer sites Tablet starts at 810px.
Can I switch the project to Tailwind or another styling approach?
Yes, it's a normal Next.js project, but it's a manual rewrite. The CSS modules are named after Framer layers, which makes them a readable starting point for that work.
Where are the font sizes for each breakpoint?
In src/styles/text/, one file per shared text style, with a media query block per breakpoint. Layer-specific sizes are in that layer's CSS module.
What if a layer looks right at 1440 but wrong at 1280?
Check whether 1280 falls in the same range as 1440 in your breakpoints map, then look for fixed widths in that layer's rules. More general questions are answered in the FAQ.
