NEO NOS

Invention · CSS + JS

Square Root.

One design. Every phone. Same proportions. You design once against a single imaginary phone, and Square Root rescales that one design so it lands exactly on whatever screen it finds — nothing reflows, no breakpoint fires, the whole page just gets bigger or smaller as one piece.

CSS + JS MIT No dependencies Free
Square Root — one canonical phone design, solved to fit every screen

Phones agree on nothing. A 320px Android, a 375px iPhone SE, a 430px Pro Max and a folded foldable all have different widths, different heights and different aspect ratios — so a screen built for one looks cramped on the next and empty on the one after. The usual answer is breakpoints: redraw the design two or three times and hope the sizes in between behave. Square Root takes the other route.

The mechanism

The idea in thirty seconds.

Three moving parts. That is the entire framework.

1

There is a canonical device

One reference phone, 360 × 720 CSS pixels, declared in :root — plus a finger unit of 60px, taken from touch-target research: an average fingertip lands around 45–52px, a thumb around 72px. Every layout decision you make is in whole or half fingers, never in pixels.

:root {
  --macro-width: 360;
  --macro-height: 720;

  /* Average finger 45-52 / thumb: 72px */
  --micro-width: 60;
  --micro-height: 60;
}

2

Every utility is authored in rem

0.0625rem is 1/16rem, so the numbers in those variables read as reference pixels at a 16px root. Widths, heights, margins, padding, offsets — all rem. There is not a hard pixel in the layout.

.sqr-w-2 { width: calc(var(--micro-width) * 2 * 0.0625rem); }

3

The script solves for the root font-size

An invisible probe, .sqr-macro-rem, is 22.5rem wide — 360px at a 16px root. The script measures its live width, computes ratio = innerWidth / probeWidth, and writes :root { font-size: (ratio × 100)% } back into a style tag. Because the design is 100% rem, changing the root font-size scales everything in lockstep.

Worked example

The probe measures 360px at a 16px root. Two phones:

 320px phone430px phone
ratio = innerWidth / 360320 / 360 = 0.8889430 / 360 = 1.1944
written root font-size88.89% → 14.22px119.44% → 19.11px
probe re-measures at22.5rem × 14.22 = 320px22.5rem × 19.11 = 430px
a .sqr-w-2 box (7.5rem)106.7px — 33.3% of screen143.3px — 33.3% of screen

Same fraction of the screen on both. That is the whole trick.

Install

Two build outputs and one script.

No bundler required. Take it from npm, or copy three files and be done.

npm install @all1web/square-root

If you compile SCSS yourself, import the source instead and you inherit the :root variables and the generator loops. The main entry already imports the scroll-snap partial, so you get those classes too.

@import "node_modules/@all1web/square-root/src/square-root";

The minimal host page

The script is not optional and not standalone — it needs a style tag to write into and two probe elements to measure.

<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <!-- square-root.js writes the solved :root font-size in here.
       It must exist before the script runs, and must stay empty. -->
  <style id="square-root"></style>

  <link rel="stylesheet" href="/assets/css/square-root.css">

  <!-- Square Root does NOT define these two. The probes need them.
       Skip this block only if you already have them (e.g. Tailwind). -->
  <style>
    .fixed     { position: fixed; }
    .invisible { visibility: hidden; }
  </style>
</head>
<body>
  <!-- the two probes: laid out, never display:none -->
  <div class="fixed invisible sqr-macro-pixel"></div>
  <div class="fixed invisible sqr-macro-rem"></div>

  <main class="sqr-px-1 sqr-mt-1">
    <div class="sqr-w-4 sqr-h-2">a 4-finger by 2-finger card</div>
  </main>

  <!-- LAST: it solves immediately on parse -->
  <script src="/assets/js/square-root.js"></script>
</body>

Things that will bite you if you skip them

  • The probes must be visibility: hidden, never display: none. A display:none probe has offsetWidth === 0, which makes the ratio Infinity and the page collapses.
  • .fixed matters. The probe is measured with offsetWidth, so a narrow parent would shrink it and you would solve for the wrong number.
  • <style id="square-root"> must be the only thing setting :root { font-size }. The script overwrites its contents wholesale, twice per run.
  • There is a visible unscaled moment. The script measures on a 500ms timeout after resetting the root, so first paint shows the unscaled canon for about half a second before it snaps. Hiding the body until the first solve completes is a reasonable thing to do in your own code.

The vocabulary

Think in fingers.

1 = one finger = 60 reference px = 3.75rem. The generated scales run 1 through 8.

ClassResolves toAt the canonical root
.sqr-w-N / .sqr-h-Nwidth / height of N fingersN × 60px
.sqr-mt-N / .sqr-mx-N / .sqr-mr-Nmargin of N fingersN × 60px
.sqr-px-N / .sqr-pl-N / .sqr-pt-Npadding of N fingersN × 60px
.-sqr-l-N / .-sqr-r-Nnegative left / right offset−N × 60px
.sqr-t-1top of one finger — the only sqr-t-* that exists60px
.sqr-h-1/2 / .sqr-mr-1/2half a finger30px
.sqr-pl-1-3/4padding-left of 1.75 fingers105px
.-sqr-h-screen-N100vh minus N fingersfull height − N × 60px
.sqr-macro-rem / .sqr-macro-pixelthe canonical device360 × 720px
.sqr-micro-rem / .sqr-micro-pixelone finger60 × 60px

Use -sqr-h-screen-N for a scroll pane sitting under a fixed N-finger header. Use sqr-macro-* when you want an element that is the canonical viewport — a full-page card in a horizontal scroller, for example.

Scroll snap

A snap layer ships alongside, and also builds on its own. .scrollsnap-horizontal sets scroll-snap-type: x mandatory; .scrollsnap-vertical sets y mandatory. Children marked .snap-x / .snap-y get their alignment and scroll-snap-stop: always, and .invisible-scrollbar hides the scrollbar cross-browser.

Neither snap class is standalone. .snap-x only matches as a descendant of .scrollsnap-horizontal, and .snap-y only as a direct child of .scrollsnap-vertical — a nested .snap-y silently does nothing.

<div class="scrollsnap-horizontal invisible-scrollbar">
  <div class="snap-x sqr-macro-rem">…</div>
  <div class="snap-x sqr-macro-rem">…</div>
</div>

Bigger screens

Tablets tile. Desktop counts columns.

The solve is not the same at every width. The script sorts the viewport into a bucket and applies a cols factor.

Viewport widthBucketcols
below 320xs1
320 – 640sm1
641 – 767md1.2
768 – 1023lg1.61
1024 and upxldesktop path

Below 1024 — the peek cue. The ratio is divided by cols, which makes the solved root slightly smaller than a perfect one-column fit. The design therefore does not quite fill the width, and the next card in a horizontal scroller peeks in at the edge. That sliver is the affordance: it is how a user knows the row scrolls sideways, with no chevron and no hint label.

1024 and up — whole columns. The desktop branch floors the number of canonical columns that fit and re-solves against that. At 1200px it solves for three 400px columns tiling the viewport exactly; at 1440px it lands on 4 × 360 with a ratio of exactly 1. No ragged right edge, no half-column gutter.

Opt-in

The phone peek switch.

Phones get cols = 1, so the design fills the screen exactly and a horizontally-scrolling card deck shows no sign that it scrolls. sqr-peek shrinks the solve slightly so the edge of the next card stays visible.

<html>                                         <!-- peek off (default) -->
<html class="sqr-peek">                        <!-- peek on, factor 0.89 -->
<html class="sqr-peek" data-sqr-peek="0.85">   <!-- peek on, factor 0.85 -->

The factor is clamped to 0.5 … 1.0 and falls back to 0.89 on anything unparseable. It applies to phones only — the multiplication sits inside the cols == 1 branch, so it is inert on tablet and desktop. A mutation observer on <html> re-solves when you toggle it, so you can flip it live in the console — and window.squareRootResolve(), the one function the script puts on window, forces a solve yourself. Give it about a second; the solve is asynchronous.

document.documentElement.classList.toggle('sqr-peek');
document.documentElement.dataset.sqrPeek = '0.85';
window.squareRootResolve();

What peek costs you

With peek on, the canonical width is no longer the viewport width — the headline invariant of the framework, broken deliberately. sqr-w-6 stops being full-bleed and renders at about 89% of the screen, and so does every full-width element built on the canon. A background band, an edge-to-edge image or a sticky footer will leave a bare strip on one side. That strip is the feature; it is also the thing that will surprise you.

What still holds: every ratio between units is preserved, and the design is uniformly scaled rather than reflowed. Nothing re-wraps and no breakpoint fires. Watch the small end though — at 360px with peek on the finger unit measures 53.4px, and at 320px it measures 47.5px. Still inside the cited touch band, but that is the floor.

Use peek on a card deck. Leave it off on a full-bleed page.

Honest limits

Known quirks.

These are real, they are in the shipped source, and you should know about them before you go debugging your own layout.

  • md and lg were divided by cols twice — fixed in 0.2.0. Effective divisors were 1.44 and 2.59. The tell that it was a leftover rather than tuning: a 768px tablet solved to a smaller root than a 320px watch. Those two buckets now scale larger than they did on 0.1.x; phones and desktop are unchanged.
  • window.onload = simulateScreen(); never binds a handler. The trailing parentheses invoke it and assign undefined. In practice it is masked — the bare call above it runs at parse time and resize covers the rest — but a re-solve after images and webfonts finish loading is not happening.
  • .-sqr-h-screen-5 subtracts four fingers, not five; it is currently identical to .-sqr-h-screen-4.
  • screen.orientation.addEventListener is called unguarded, so browsers without the Screen Orientation API will throw at that line.
  • dist/ is checked in and can drift from src/. Rebuild from source if the two disagree.

Source

Small enough to read end to end.

MIT — free to use, including commercially. Attribution appreciated, not required. Square Root came out of building ALL1.AI, where a single interface had to hold its shape across every phone a customer might arrive on. It has been running in production there since before the repository existed.