A layout technique for designers and front-end developers
The mockup doesn’t reflow. It scales.
Within a breakpoint the page looks like the Figma mockup at any width: same proportions, same line breaks, same composition. Only the scale changes.
Drag the slider or watch it play
01 · The idea
How Pantograph differs from a regular responsive layout
Take a 375 px mobile mockup and open the page on a phone that is 480 px wide.
375 px
480 px
480 px
Responsive layout
Sizes are fixed in px. Between breakpoints blocks stretch and shrink, so every width in between has to be checked separately.
Pantograph layout
Sizes are in rem, and 1rem depends on the viewport width. Any width inside a breakpoint is the mockup at scale.
02 · Algorithm
One stylesheet, one formula
The CSS defines the breakpoints and computes the font-size of html. The whole layout is written in rem, so it grows and shrinks along with it.
Breakpoints: three mockups, three ranges
Each range of widths gets three numbers. --balance-point is the balance point: the width of the Figma mockup, where 1rem is exactly 1px. --min-vw and --max-vw are the range edges. The numbers have no units so they can be divided by each other.
The first media query starts at 0 px, but the math starts at 320: on narrower screens 1rem never drops below 0.853 px.
/* Breakpoints */
@media (min-width: 0px) {
:root {
--min-vw: 320;
--max-vw: 500;
--balance-point: 375;
}
}
@media (min-width: 500px) {
:root {
--min-vw: 500;
--max-vw: 1152;
--balance-point: 768;
}
}
@media (min-width: 1152px) {
:root {
--min-vw: 1152;
--max-vw: 1600;
--balance-point: 1440;
}
}
Computing 1rem
The CSS draws a straight line: at min-vw 1rem is at its smallest, at max-vw at its largest, and in between it grows evenly. Here it is piece by piece; the example numbers are for the tablet range.
:root {
--min-font-size: calc(var(--min-vw) / var(--balance-point));
--max-font-size: calc(var(--max-vw) / var(--balance-point));
Size of 1rem at the range edges
At the mockup width 1rem = 1px. At a range edge it is smaller or larger by the same ratio as the edge is to the mockup.
500 / 768 = 0.651 px
1152 / 768 = 1.5 px
--diff-font-size: calc(var(--max-font-size) - var(--min-font-size));
--diff-vw: calc(var(--max-vw) - var(--min-vw));
--slope: calc(var(--diff-font-size) / var(--diff-vw));
Slope
How many px 1rem gains with each pixel of viewport width.
(1.5 − 0.651) / (1152 − 500)
= 0.001302 = 1 / 768
--max-font-size-px: calc(var(--max-font-size) * 1px);
--min-font-size-px: calc(var(--min-font-size) * 1px);
--min-vw-px: calc(var(--min-vw) * 1px);
--fluid-font-size-px: calc(
var(--slope) * (100vw - var(--min-vw-px)) + var(--min-font-size-px)
);
Line equation
A line through the point (min-vw; min-font-size) with that slope. 100vw is the current viewport width. Multiplying by 1px turns the unitless numbers into pixels.
Viewport 900 px:
0.001302 × (900 − 500) + 0.651 = 1.172 px
--clamp-scaling-fs: clamp(
var(--min-font-size-px),
var(--fluid-font-size-px),
var(--max-font-size-px)
);
}
html {
font-size: var(--clamp-scaling-fs);
}
Clamp and apply
clamp keeps the size inside the range edges. Inside the range it changes nothing; it only acts above 1600 px and below 320 px. The result becomes the font-size of html, and every rem on the page is computed from it.
The formula, simplified
slope = (max-font-size − min-font-size)
/ (max-vw − min-vw)
= 1 / balance-point
fluid = slope × (100vw − min-vw)
+ min-vw / balance-point
= 100vw / balance-point
slope = (max-font-size − min-font-size) / (max-vw − min-vw)
= 1 / balance-point
fluid = slope × (100vw − min-vw) + min-vw / balance-point
= 100vw / balance-point
1rem = viewport width ÷ mockup width
That is all the algorithm does. The other variables let CSS evaluate the formula, and clamp keeps the value inside the range.
- 375 px → 1 px
- 412 px → 1.099 px
- 480 px → 1.28 px
- 1024 px → 1.333 px
Try your own width
The values follow the width from the demo at the top. Pick a width here or go back to the slider.
- Variables
- min-vw 320 · max-vw 500 · balance-point 375
- min-font-size
- 320 / 375 = 0.853 px
- max-font-size
- 500 / 375 = 1.333 px
- slope
- (1.333 − 0.853) / (500 − 320) = 0.002667
- fluid-font-size
- 0.002667 × (412 − 320) + 0.853 = 1.099 px
- clamp
- clamp(0.853, 1.099, 1.333) = 1.099 pxViewport wider than 1600 px: the upper limit applies
- Result
- 1rem = 1.099 px · 24rem = 26.4 px
What happens at the borders
Breakpoint switch
At 1151 px the tablet mockup is scaled ×1.5. At 1152 px the desktop mockup takes over at ×0.8. The layout has changed, and the new mockup starts from a smaller scale. The jump is expected.
Wider than 1600 px
clamp stops the growth: 1rem stays at 1.111 px. Fonts and spacing stop growing, and the extra width goes into the margins.
Narrower than 320 px
1rem never drops below 0.853 px. The mobile mockup stops shrinking.
03 · For designers
What it means for designers
Draw three mockups: 375, 768 and 1440 px
Each mockup covers its own range of widths. There is no need to draw the widths in between: they are the same mockup at scale.
Check the edge widths
The mockup shrinks the most at 500 px, where the tablet mockup is at ×0.65. Check small tablet text at this width.
If the mockup isn’t drawn at the breakpoint width
Different mockup width
Desktop drawn at 1200 and mobile at 360? No need to recalculate numbers by hand. Put your mockup width into --balance-point, and 1rem is again 1px of your mockup. The range edges and media queries stay the same.
/* after the CSS above */
@media (min-width: 0px) {
:root {
--balance-point: 360;
}
}
@media (min-width: 1152px) {
:root {
--balance-point: 1200;
}
}
Side effect: at the breakpoint width 1rem is no longer 1px (at 1440 it is 1.2 px). The mockup becomes the reference.
No tablet mockup
Use the desktop layout at a scale of about 0.75: for the tablet range, --balance-point = 768 / 0.75 = 1024. The honest 1200 gives a scale of 0.64 at 768 px and text of around 15 px, which is too small.
To keep content within 768 px, set container and column widths in percent. Keep fonts, spacing, heights and radii in rem. Shrink or move large illustrations with a separate rule.
@media (min-width: 500px) {
:root {
--balance-point: 1024; /* 768 / 0.75 */
}
}
Write the chosen scales down in the project README so the rule doesn’t get lost.
04 · For developers
How to build
Add the CSS
Once, globally, before the page styles. It sets the font-size of html, so it affects every rem. If only some pages use this layout, include the CSS only on those.
/* Breakpoints */
@media (min-width: 0px) {
:root {
--min-vw: 320;
--max-vw: 500;
--balance-point: 375;
}
}
@media (min-width: 500px) {
:root {
--min-vw: 500;
--max-vw: 1152;
--balance-point: 768;
}
}
@media (min-width: 1152px) {
:root {
--min-vw: 1152;
--max-vw: 1600;
--balance-point: 1440;
}
}
/* Scaling */
:root {
--min-font-size: calc(var(--min-vw) / var(--balance-point));
--max-font-size: calc(var(--max-vw) / var(--balance-point));
--diff-font-size: calc(var(--max-font-size) - var(--min-font-size));
--diff-vw: calc(var(--max-vw) - var(--min-vw));
--slope: calc(var(--diff-font-size) / var(--diff-vw));
--max-font-size-px: calc(var(--max-font-size) * 1px);
--min-font-size-px: calc(var(--min-font-size) * 1px);
--min-vw-px: calc(var(--min-vw) * 1px);
--fluid-font-size-px: calc(
var(--slope) * (100vw - var(--min-vw-px)) + var(--min-font-size-px)
);
--clamp-scaling-fs: clamp(
var(--min-font-size-px),
var(--fluid-font-size-px),
var(--max-font-size-px)
);
}
html {
font-size: var(--clamp-scaling-fs);
}
Figma numbers go into rem
24px in the mockup is 24rem in code. No need to recalculate for each width: the scale changes by itself.
Media queries only for layout changes
Use them when the grid changes or the mockups differ. The boundaries are the same 500 and 1152 px as in the CSS.
Example
/* mobile mockup, 375 */
.hotels {
display: grid;
gap: 12rem;
padding: 0 20rem;
}
.hotels__title {
font-size: 21rem;
}
/* tablet mockup, 768 */
@media (min-width: 500px) {
.hotels {
grid-template-columns: repeat(2, 1fr);
gap: 20rem;
padding: 0 32rem;
}
.hotels__title {
font-size: 28rem;
}
}
/* desktop mockup, 1440 */
@media (min-width: 1152px) {
.hotels {
grid-template-columns: repeat(4, 1fr);
gap: 24rem;
padding: 0 80rem;
}
.hotels__title {
font-size: 36rem;
}
}
Mobile first
The mobile version first, without media queries, then tablet and desktop.
Numbers straight from the mockups
The hotel list title in the demo mockups is 21 px on mobile, 28 px on tablet and 36 px on desktop. The code uses the same numbers, in rem.
No code between breakpoints
At 412 px the title is 21 × 1.099 = 23.1 px, at 900 px it is 28 × 1.172 = 32.8 px. The CSS from step one does this.
05 · AI skill
Teach your AI agent the same technique
The skill is an English SKILL.md file: instructions an AI agent reads before it starts. Put it in your agent’s skills folder and it will build pages by these rules.
What’s inside
- The rule 1rem = 1px of the mockup
- Breakpoints 375 / 768 / 1440 and the 500 and 1152 px borders
- The full CSS and how the formula works
- What to do with mockups drawn at other widths
- Pitfalls and an edge-width checklist
---
name: pantograph-layout
description: Pantograph layout: scale a page with
the viewport so it matches the Figma mockup at any
width inside a breakpoint (1rem = 1px of mockup, …
---
# Pantograph layout
## The idea in one line
html font-size = viewport width / mockup width.
At the mockup width 1rem is exactly 1px of the mockup.
## Breakpoints
| up to 499px | 375 | 0.853px → 1.333px |
| 500–1151px | 768 | 0.651px → 1.5px |
| 1152px+ | 1440 | 0.8px → 1.111px |
## The CSS
## How it works
## Writing styles
## Mockup not at the breakpoint width
## Pitfalls
## Review checklist
06 · Limits
Where Pantograph breaks
Animations in px
JS animation libraries such as GSAP set coordinates and sizes in px directly. Those values don’t scale with the window. After the animation, recalculate the final positions and sizes in rem.
Custom breakpoints
If the grid switches at widths other than 500 and 1152 px while the scale switches there, the tablet grid meets the mobile scale in between. Use the boundaries from the CSS.
Third-party rem
1rem on the page is about 1 px, not 16. Third-party widgets and styles that assume 16 px per rem become tiny. Convert their sizes to px or to the mockup scale.