The mockup doesn’t reflow. It scales.
Pantograph Layout is a CSS technique for designers and front-end developers. Within a breakpoint, the page looks like its Figma mockup at any screen width: the same proportions, the same line breaks, the same composition. Only the scale changes.
Site: https://promo.github.io/pantograph-layout/ (Russian) · https://promo.github.io/pantograph-layout/en/ (English)
A pantograph is a drawing instrument that copies a picture at a different scale. This layout does the same with a Figma mockup: it carries it over to a screen of any width at the right scale.
Take a 375 px mobile mockup and open the page on a phone that is 480 px wide.
- Layout in px. Fonts and spacing stay as in the mockup, blocks stretch to the width. The proportions are no longer what the designer drew, and every width in between has to be checked separately.
- Pantograph layout. Sizes are in
rem, and1remdepends on the viewport width. Everything is scaled by 480 / 375 = 1.28: it is the same mockup, only larger.
Mobile first. Three mockups, each one covers a range of viewport widths:
| Viewport | Mockup | 1rem at the range edges |
|---|---|---|
| up to 499 px | 375 | 0.853 → 1.333 px |
| 500–1151 px | 768 | 0.651 → 1.5 px |
| 1152 px and up | 1440 | 0.8 → 1.111 px, frozen above 1600 px |
Add it once, globally, before the page styles. The same file is in
assets/pantograph.css.
/* 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);
}Each range of widths gets three unitless numbers:
--balance-point: the balance point, the width of the Figma mockup at which 1rem is exactly 1px;--min-vwand--max-vw: the edges of the range.
The numbers have no units so they can be divided by each other; * 1px turns them into
lengths at the end. Then the CSS draws a straight line:
- Size of 1rem at the range edges:
min-vw / balance-pointandmax-vw / balance-point. Tablet: 500 / 768 = 0.651 px, 1152 / 768 = 1.5 px. - Slope: how many px 1rem gains with each pixel of viewport width. (1.5 − 0.651) / (1152 − 500) = 0.001302 = 1 / 768.
- Line equation: a line through (min-vw; min-font-size) with that slope. At a 900 px viewport: 0.001302 × (900 − 500) + 0.651 = 1.172 px.
- Clamp and apply:
clampkeeps 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 thefont-sizeofhtml, and everyremon the page is computed from it.
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
1rem = viewport width ÷ mockup width. 375 px → 1 px, 412 px → 1.099 px, 480 px → 1.28 px, 1024 px → 1.333 px.
- 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.
clampstops 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.
- 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.
| Viewport | Mockup | Scale | 14px text becomes |
|---|---|---|---|
| 320 | 375 | × 0.853 | 11.9 px |
| 499 | 375 | × 1.331 | 18.6 px |
| 500 | 768 | × 0.651 | 9.1 px |
| 1151 | 768 | × 1.499 | 21.0 px |
| 1152 | 1440 | × 0.8 | 11.2 px |
| 1600+ | 1440 | × 1.111 | 15.6 px |
-
Different mockup width. Desktop drawn at 1200 and mobile at 360? 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; } }
-
No tablet mockup. Use the desktop layout at a scale of about 0.75: for the tablet range,
--balance-point= 768 / 0.75 = 1024. Set container and column widths in percent; keep fonts, spacing, heights and radii inrem.@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.
- Add the CSS. Once, globally, before the page styles. It sets the
font-sizeofhtml, so it affects everyrem. If only some pages use this layout, include the CSS only on those. - Figma numbers go into rem. 24px in the mockup is
24remin code. No need to recalculate for each width. - 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.
/* 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; }
}At 412 px the title is 21 × 1.099 = 23.1 px, at 900 px it is 28 × 1.172 = 32.8 px. No code is needed between breakpoints.
- 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.
- 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.
SKILL.md is an English skill file for AI agents (Claude and other agents
that read skill files). Put it in your agent’s skills folder and it will build pages by
these rules: the 1rem = 1px rule, breakpoints, the full CSS, mockups drawn at other
widths, pitfalls and an edge-width checklist.
The site itself is built with Pantograph Layout: assets/pantograph.css sets the root
font-size, and every size in assets/site.css is a px value from the 375 / 768 / 1440
mockups written in rem.
index.html, en/index.html generated pages (committed, served by GitHub Pages)
assets/pantograph.css the technique
assets/site.css the site styles, in rem
assets/app.js hero demo, live calculation, copy buttons, language menu
assets/fonts/ Onest and JetBrains Mono (SIL Open Font License 1.1)
SKILL.md the AI skill
src/build.mjs page generator (Node.js, no dependencies)
src/serve.mjs local server for checking the pages
src/template.html page template
src/i18n/*.json texts, one file per language
src/content.json code samples shown on the page
.github/workflows/pages.yml deploy to GitHub Pages
Node.js 20 or newer, no npm install needed.
npm run build # node src/build.mjs
npm start # node src/serve.mjs, then open http://localhost:8000Rebuild after changing src/template.html, src/content.json or src/i18n/*.json.
Changes in assets/ need no build.
- Copy
src/i18n/en.jsontosrc/i18n/<code>.jsonand translate the values. - In
lang, setcode,short,labelandpath("<code>/"). - Run
npm run build. The page appears at/<code>/and in the language menu of every page.
Onest and
JetBrains Mono, both under the SIL Open Font
License 1.1 (see assets/fonts/OFL-*.txt).
MIT: use the CSS, the code and the texts in any project, including commercial ones, as long as you keep the copyright notice.
The fonts in assets/fonts/ are not covered by MIT: they are under the SIL Open Font
License 1.1 (see assets/fonts/OFL-*.txt).