Tailwind Sizes: One Variable Behind 435 Uses of w-, h- and size-
Every numeric size is calc(var(--spacing) * n). Measured across 139 files: which families get used, the fractional steps, and the sizes that aren't classes.
Every numeric size class in Tailwind v4 — w-4, h-10, size-6, p-2, gap-8 — is the same multiplication: calc(var(--spacing) * n), where --spacing is 0.25rem. There is one scale, not a width scale and a height scale, and because it is arithmetic rather than a lookup table, steps nobody listed in the docs still work.
The short answer
Learn one rule and the whole family follows:
w-*sets width,h-*sets height,size-*sets both at once.- A number means
0.25rem × n, soh-10is2.5rem/ 40px at the default root size. - Keywords escape the scale:
full(100%),screen(100vw/vh),auto,fit,min,max,px(exactly 1px). - Fractions mean percentages:
w-1/2,w-2/3. min-w-*/max-w-*/min-h-*/max-h-*take the same values and are what you actually want for responsive layout.
The one genuinely new thing in v4 is that the scale is computed, not enumerated. This appears in the built stylesheet as a single custom property:
--spacing: .25rem;
Change that one value and every size, padding, margin and gap in the project rescales together. That is the whole design-token story for dimensions — see Tailwind design tokens for the colour half of it.
The families, and what each is for
| Family | Sets | Reach for it when |
|---|---|---|
w-* | width | The element's width is a decision, not a consequence |
h-* | height | Icons, fixed bars, and anything with a known pixel height |
size-* | width and height | Square things — icon buttons, avatars |
max-w-* | max-width | Text measure and content columns — the responsive default |
min-h-* | min-height | "At least the viewport", hero sections, empty states |
min-w-* | min-width | Stopping a flex child from refusing to shrink (min-w-0) |
aspect-* | aspect-ratio | Media boxes — reserves space before the image loads |
What a real codebase reaches for
Here is the measurement from this storefront: 139 .tsx files under src/, of which 75 size anything at all, carrying 88 distinct sizing classes over 435 uses. Split by family:
| Family | Uses |
|---|---|
w-* | 218 |
h-* | 139 |
max-w-* | 62 |
min-w-* | 9 |
min-h-* | 5 |
size-* | 2 |
max-h-* | 0 |
Four things in that table are worth more than the usual cheat sheet.
w-full is 100 of the 435. Twenty-three percent of every sizing decision in the codebase is "be as wide as your parent." That is what a well-built component tree looks like: children fill, parents decide. If your own count is dominated by specific widths instead, the sizes are in the wrong place.
The next seven values are all icons. h-9 (19), h-4 and w-9 (18 each), h-7 and w-7 (18 each), w-4 (17): matched pairs, in four sizes, because that is what a lucide-react icon needs inside a button. The whole h-* column is mostly this.
size-* has 2 uses. It is the newest and most obviously correct utility here — size-10 replaces h-10 w-10 — and even in a v4-native codebase it has barely displaced the pair, precisely because those icon pairs were written before it. Worth knowing when you read older code: h-10 w-10 is not a mistake, just older.
max-h-* has zero uses across all 139 files. Capping height is almost always the wrong instinct in a document-flow layout; it clips text at a font size you did not predict. The cases that look like they need it usually want overflow-y-auto with a max-h on a scroll container, which is a dialog pattern rather than a layout one.
The steps that are not in the docs
Among those 88 classes, eight are fractional steps — h-0.5 (5 uses), h-4.5 and h-5.5 (4 each), h-6.5 and w-6.5 (3 each), w-4.5 (2), plus one each of w-5.5 and w-7.5 — 23 uses in total. In Tailwind v3 most of those did not exist: the scale was an enumerated table with a few half-steps, and h-5.5 needed a config entry.
In v4 they work because the class is compiled to arithmetic, not looked up. Any numeric step you write resolves against --spacing, which is exactly what the shipped stylesheet from this project's production build contains:
.h-5\.5 { height: calc(var(--spacing) * 5.5) }
.w-4\.5 { width: calc(var(--spacing) * 4.5) }
.size-10 { width: calc(var(--spacing) * 10);
height: calc(var(--spacing) * 10) }
So h-5.5 is 1.375rem, 22px at the default root size, and size-10 really is the pair written once. That is why the icon-sized odd numbers in this codebase are plain classes instead of arbitrary values.
Useful in practice, with one caveat: a fractional step is a hint that the design is between scale stops. Two or three across a codebase is normal — the 22px icon in a 20px slot really does exist. A dozen means the scale and the design disagree, and the fix is in the design file, not the markup.
When to leave the scale, and what that costs
Arbitrary values — max-w-[870px] — are 68 of the 435 uses here, 16%, and 57 of those 68 are a max-w-*. That concentration is the tell. The reason is documented in this project's house rules: the marketing sections are a pixel-faithful port of a source design, so heading columns carry the widths the original had. max-w-[870px] appears 8 times and max-w-[570px] 6.
Honest reading: those are not 57 design decisions, they are a handful of decisions repeated. The lesson is the one generalisable thing about arbitrary values — each one is a number with no name, so nothing else can reuse it and nothing flags it when the design changes. When the same bracket appears three times, it wants to be a token in @theme or a component that owns the width.
Two arbitrary values in this codebase do earn their brackets, and they are the ones worth copying:
// src/components/molecules/PreviewModal.tsx — full-screen preview dialog
className="fixed inset-0 z-[10000] flex h-[100dvh] flex-col overscroll-none …"
// src/components/organisms/DocsSidebar.tsx — sticky docs nav
className="shrink-0 lg:sticky lg:top-24 lg:h-[calc(100vh-7rem)] lg:w-64 …"
h-[100dvh] instead of h-screen because h-screen is 100vh, and on a mobile browser 100vh includes the chrome that slides away — a full-screen dialog sized that way is taller than the visible viewport and its bottom controls sit under the address bar. dvh is the dynamic viewport unit that accounts for it. The sidebar below keeps plain 100vh inside a calc(), and that is fine because the whole declaration is lg:-gated — it never applies on a phone. Same unit, two correct answers, decided by which breakpoints the class is allowed to run at.
Rules that fall out of those two lines:
- A full-height element that users meet on a phone wants
dvh, notvh. calc()inside a bracket is legal and often clearer than a wrapper div, but it must not contain spaces —h-[calc(100vh-7rem)], noth-[calc(100vh - 7rem)].- A viewport-height class behind
lg:is a different risk profile from a bare one.
The sizes that are not classes at all
Two of the most load-bearing dimensions in this project never appear as a utility, and this is the part a utilities cheat sheet cannot tell you.
The page container is hand-written CSS. .container in src/app/globals.css is a ladder of max-width plus padding-inline steps — 425px, 640px, 768px, 1024px, 1280px, with the inline padding widening at 1440px and a final 1536px stop — because it had to match the source design exactly. The @theme block beside it also adds two breakpoints below Tailwind's smallest:
--breakpoint-2xsm: 375px;
--breakpoint-xsm: 425px;
375px is an iPhone, and a design that only starts responding at sm (640px) has nothing to say about the most common phone width there is. If your layout breaks on real devices but passes every breakpoint you test, add the step rather than the override. Tailwind breakpoints covers how custom stops interact with the default ladder.
Image dimensions are decided at build time, not in CSS. The largest bytes on a storefront page are its cover images, and no w-* class makes a 2120px JPEG smaller — it just paints it small. So the sizes live in src/lib/imageVariants.ts:
/** Card thumbnails: 530px CSS wide at DPR 2, so 1060 covers every display. */
export const CARD_VARIANT_WIDTH = 1060;
/** Full-bleed 16:9 slides on the detail page, matching the markup's width. */
export const VIEW_VARIANT_WIDTH = 1600;
The arithmetic in that first comment is the technique: a card paints at 530 CSS px, a 2× display needs 1060 device px, so the generated WebP is 1060 wide and the JPEG that was costing ~255 KiB to deliver roughly 33 KiB of visible pixels stays behind as the SEO copy only. The CSS width and the file width are chosen together, on purpose.
The markup holds up its end with a ratio rather than a height: aspect-[530/330] appears 5 times, which is the same 530×330 box the variant width was derived from. The payoff shows up in a number no class can produce: CLS is 0 on every page measured, because every media box has its dimensions reserved before the image arrives — that is aspect-* and explicit sizing doing structural work rather than decorative work. Tailwind aspect-ratio has the recipe.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
h-full does nothing | The parent has no resolved height | Give the parent a height, or use min-h-*/flex instead of percentage height |
| A flex child refuses to shrink below its text | min-width: auto on flex items | Add min-w-0 (9 uses here exist for exactly this) |
h-screen dialog is taller than a phone screen | 100vh includes retracting browser chrome | Use h-[100dvh] |
calc() in a bracket is ignored | Spaces inside the brackets | Write h-[calc(100vh-7rem)] with no spaces |
w-[var(--x)] produces nothing | The variable is not defined where the class applies | Define it on an ancestor, or move it into @theme |
A new size class generates no CSS after editing @theme | Tailwind v4 + Turbopack does not hot-reload @theme | Restart the dev server |
size-* works for width but not height | An h-* later in the class list wins | Remove the competing class; size-* sets both |
| Class built by string concatenation never appears | The scanner only sees complete class names | Write full literals; never h-${n} |
| Text clipped on a narrow screen | A max-h-* or fixed h-* where content needs to grow | Swap to min-h-*, or let the content size the box |
Frequently asked questions
What is the difference between w-full and w-screen?
w-full is 100% of the parent; w-screen is 100vw of the viewport. Inside any padded container, w-screen overflows and produces a horizontal scrollbar — including one caused by the vertical scrollbar's own width, since 100vw ignores it. The codebase measured here uses w-full 100 times and w-screen never — nor a bare h-screen.
Is size-4 the same as h-4 w-4?
Yes, same computed result, one class instead of two. Prefer it for new square elements. Both exist in this codebase, which is normal for anything written across a version boundary.
How do I change the whole scale at once?
Redefine --spacing in your @theme block. Every numeric size, padding, margin and gap scales with it, because they all compile to calc(var(--spacing) * n). Then restart the dev server — v4 does not hot-reload theme changes. The Tailwind v4 config covers what else moved into CSS.
Should sizes be on the parent or the child?
Parent decides, child fills, as a default. That is what makes w-full the most-used class in a healthy component tree: a card does not know how wide it should be, and the grid that places it does. Push a width down into a child only when the child genuinely owns that dimension — an icon, an avatar, a fixed-height bar.
Templates in this post
ASoc Nexus (a smart-apps SaaS site with a dashboard mock and solutions grid), ASoc Nimbus (a cloud-software site with tiered pricing) and ASoc Nova (a crypto trading page with live price widgets) are built on the container ladder and the build-time image widths described above, so the layout holds from 375px up without per-section overrides.
Browse the full sets: Next.js landing page templates, Tailwind landing page templates. For single-property depth, see Tailwind height and Tailwind max-width.
