DEV Community

Daniel Pertu
Daniel Pertu

Posted on

There is not one pixel size on our big screen: 128 container-query values, and the same component is a card on our homepage

PubTrivia has a screen that goes on a pub television. The host runs the quiz from a laptop, mirrors or screen-shares it to whatever is on the wall, and the room reads the question off it from twenty feet away. The same component also appears, scaled down to about 300 pixels wide, inside the homepage: the demo there is not a screenshot or a video, it is the real host view with a made-up round in it.

One component, two sizes two orders of magnitude apart, and no media queries. The trick is that the component contains no pixel sizes at all.

cqmin, against a size container

The frame declares itself a container, and every size inside it is a fraction of that container rather than of the viewport or of the root font size:

export function StageFrame({ children, className = 'h-[100dvh]' }) {
  return (
    <div className={`relative flex w-full flex-col overflow-hidden [container-type:size] ${className}`}>
      {children}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

container-type: size is the strict one: it establishes containment on both axes, so cqw, cqh and cqmin all resolve against this box. That is why the frame carries a height (h-[100dvh] in the app, whatever the demo's frame is on the homepage) rather than letting content decide it. container-type: inline-size would only give you the width, which is not enough when the constraint you are actually fighting is vertical: a question that wraps onto a third line must not push the leaderboard off the bottom of a television.

Inside, everything is cqmin, which is the smaller of the two container dimensions:

<div className="grid grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center gap-[2cqmin] px-[3cqmin] pt-[2.4cqmin] pb-[1.6cqmin]">
Enter fullscreen mode Exit fullscreen mode

Padding, gaps, border radii, badge diameters, the height of the row the question sits in, and every font size as text-[length:4.4cqmin]. Five files, 128 such values at the commit currently deployed. Icons inside text are sized h-[1em] w-[1em] so they ride the font size they are next to, which means they scale too without being counted.

Choosing cqmin rather than cqw is the part worth arguing about. On a 16:9 television cqmin is the height, so the layout is driven by the dimension that actually runs out. A 4:3 projector, which plenty of pubs still have, gets proportionally larger text rather than a layout that overflows the bottom. The cost is that an unusually wide, short window makes everything small, which a laptop in full screen never is.

What it buys

Three things that would otherwise each be their own piece of code:

  1. The demo is the product. The marketing page passes a different className to the same frame and gets a faithful miniature. There is no second set of styles to drift, and when the real screen changes, the homepage changes with it.
  2. No breakpoints for screen size. A 13-inch laptop, a 55-inch TV and a 4K display are the same layout at the same proportions. There is nothing to test at 1080p that differs from 4K except the pixel density.
  3. Alignment survives. One exported constant, STAGE_TOP_ROW = 'h-[8cqmin]', fixes the height of the top row across the question screen and the final screen, so the leaderboard beside them does not jump by a few pixels when the quiz ends. In pixels that constant would have to be a different number per screen size.

The rule that breaks it

Container units resolve against the nearest ancestor container, not the one you had in mind. So the frame's own doc comment carries a warning that is really an architectural constraint:

Put nothing in between that is a container itself, or the units inside it measure the wrong box.

Tailwind's @container utility, which is handy for component-level responsive design, is exactly the thing you must not use inside this subtree. One well-meaning @container on an intermediate wrapper and every cqmin below it silently starts measuring a card instead of the stage: nothing errors, the text just comes out wrong on the television and right on the laptop.

There is a second, quieter trap. container-type: size applies containment, which means the box no longer sizes itself from its contents. Give the frame no height and it collapses, and the error message for that is "the screen is blank", which is a long way from "you forgot a height".

Then I replaced all 128 of them with one letter

The version in my working tree now does the same thing through a single custom property, because the stage had to work on a phone-shaped window as well (a host running the quiz from their own phone, with no TV involved):

<div className="... [container-type:size] [&>*]:[--u:1cqmin] @max-3xl:[&>*]:[--u:1.8cqw]">
Enter fullscreen mode Exit fullscreen mode

Every size then reads calc(N * var(--u)):

className="px-[calc(3*var(--u))] pt-[calc(2.4*var(--u))] text-[length:calc(2.6*var(--u))]"
Enter fullscreen mode Exit fullscreen mode

The numbers are identical. What changes is that the unit is now a variable: 1cqmin on a wide stage, and 1.8cqw below 48rem, where the window is tall and narrow and the height is no longer the dimension that runs out. 128 cqmin literals became 143 references to var(--u) across the same five files, and the entire phone adaptation is two declarations on one element.

Two details made that work. The custom property is set on [&>*], the frame's children, rather than on the frame itself, because a property whose value is a container unit resolves against the element's own nearest container, and on the frame that is the frame's parent. And the code is noticeably uglier: py-[calc(1*var(--u))] is not as nice to read as py-[1cqmin]. The uglier version is the one that works on both shapes, so it wins.

Check it yourself

Open pub-trivia.app, find the big-screen demo, and drag the window narrower and wider. Every bit of that panel, text included, scales continuously rather than snapping at breakpoints, because nothing in it is expressed in pixels. The feature page for the screen itself is here.

You can also count the technique from outside. The deployed homepage carries 38 cqmin classes in its HTML, 34 of them distinct:

$ curl -s https://pub-trivia.app/ | grep -oE '[a-z-]+-\[[^]]*cqmin[^]]*\]' | sort | uniq -c | sort -rn | head -5
   3 gap-[2cqmin]
   2 text-[length:1.9cqmin]
   2 gap-[0.8cqmin]
   1 text-[length:4.4cqmin]
   1 w-[26cqmin]
Enter fullscreen mode Exit fullscreen mode

And the stylesheet it loads contains 160 of them, with exactly one container-type: size rule in it:

$ curl -s https://pub-trivia.app/ | grep -o '/_next/static/chunks/[^"?]*\.css' | sort -u \
    | while read f; do curl -s "https://pub-trivia.app$f" | grep -o cqmin | wc -l; done
0
160
Enter fullscreen mode Exit fullscreen mode

One rule, because there is one stage.

Top comments (0)