Pantograph Layout

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

Viewport412 px
BreakpointMobile · 375
1rem1rem = 1.099 px
24px mockup text26.4 px

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

Figma mockupThe reference.

480 px

Layout in pxFonts and spacing stay as in the mockup, blocks stretch to the width. The proportions are no longer what the designer drew.

480 px

PantographEverything is scaled by 480 / 375 = 1.28. It is the same mockup, only larger.

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.

1

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.

ViewportMockup1rem at the edges
up to 499 px3750.853 → 1.333 px
500–1151 px7680.651 → 1.5 px
1152 px and up14400.8 → 1.111 px

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;
  }
}
2

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.

3

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.

412 px Mobile · mockup 375 px
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 px
Result
1rem = 1.099 px · 24rem = 26.4 px
4

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.

ViewportMockupScale14px text
320375× 0.85311.9 px
499375× 1.33118.6 px
500768× 0.6519.1 px
1151768× 1.49921.0 px
11521440× 0.811.2 px
1600+1440× 1.11115.6 px

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

1

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);
}
2

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.

3

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
SKILL.md
---
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.