Skip to content
Chapter 20Lesson 1

The box model and the inline/block axis

The CSS box model and logical inline/block axis, sized and spaced through Tailwind utilities, as the foundation for every layout you build in this chapter.

You give a card a fixed width, add some padding, draw a border, and check the result: w-64 p-4 border. It renders exactly 256 pixels wide. Not 256 plus the padding, not 256 plus the border, just 256. If you ever wrote CSS before this course, that should feel slightly wrong, because the rule you half-remember says padding adds to width. This lesson explains why it doesn’t. You’ll learn the four boxes every element is made of, the border-box model that makes widths compose by addition, the spacing scale that feeds every p-* and m-* you write, and the inline/block axis that gives you right-to-left layouts for free.

Most of this builds on a single line from Preflight, the deliberately blank canvas: the one where Tailwind set box-sizing: border-box on every element before you wrote a thing. You inherited that line, and now you’ll see what it gave you. By the end you’ll be able to read any combination of p-*, m-*, and border and predict the rendered size in your head, and you’ll reach for logical utilities like ps-* by reflex.

Every element the browser lays out is not one rectangle but four, nested one inside the next. Learning their names, and which one your width actually targets, is the foundation for everything that follows. We’ll work through them from the inside out.

margin outside space · pushes neighbors border the visible edge padding inside space · takes the background content children + text
The four boxes, from the inside out. These are the same colors Chrome DevTools paints when you inspect an element.

The content box is the innermost rectangle, where your text and child elements actually render. Under the legacy sizing model, this is the box your width and height target. Hold that thought, because it’s the source of the surprise we’re about to undo.

The padding box wraps the content. Padding is space on the inside, breathing room between your content and the element’s edge, and it takes on the element’s background color. When you set bg-white p-4, the white fills the content and the padding.

The border box is the styled, visible edge. Every border-* utility draws here, on the ring between the padding and the outside world.

The margin box is the outermost band: transparent space outside the border that pushes neighboring elements away. Margin is the odd one out in two ways. It’s the only box that doesn’t take the background, so it’s always see-through, and it’s the only one that participates in a quirk called margin collapse, which you’ll meet shortly.

That last contrast is worth committing to memory, because every other rule hangs off it: background and border paint the content, padding, and border together, while margin is always transparent and always outside. That’s why padding feels like space on the inside and margin feels like space on the outside. The background stops at the border, and everything past it is margin.

Now for the surprise from the top of the lesson, and the rule that resolves it. There are two ways a browser can interpret the width you set, and which one is active changes your arithmetic completely.

Under the old default, called content-box, width sizes only the content box. Padding and border are then added on top of it. So that same w-64 p-4 border card, 256px wide with 16px of padding on each side and a 1px border, would render at 256 + 32 + 2 = 290px. You asked for 256 and got 290. This is the model most stale tutorials assume, and it’s the model most AI-generated CSS still reaches for, because that code learned from a decade of pages written before the better default took over. If you paste in a snippet and an element comes out mysteriously too wide, content-box is the first thing to suspect.

Under border-box, your project’s default set for you by Preflight , width sizes the border box. Content, padding, and border all fit inside the number you declared. w-64 p-4 border is 256px, full stop: the content box quietly shrinks to absorb the 32px of padding and the 2px of border. You name the outer size, and the browser does the subtraction.

That difference looks academic until you put it in production terms. Under border-box, widths compose by addition, and one of the most common layout bugs in legacy CSS simply doesn’t exist for you. Picture a child set to w-full (100% of its parent) with p-6 of padding. Under content-box, that element is 100% + 48px: wider than its parent, so it spills out and triggers a horizontal scrollbar. Under border-box, it’s exactly 100%, padding folded inside, no overflow. This is why modern frameworks switched their global default to border-box.

Don’t take the rule on faith. In the following playground, drag the padding slider up and watch the two readouts. Under border-box, the rendered outer width refuses to move past what you declared. Then flip the box-sizing toggle to content-box and drag padding again: now the rendered width climbs right past your declared number.

Slide padding under each box-sizing mode. Under border-box, the rendered outer width never exceeds what you declared, because the content shrinks to make room. Under content-box, padding and border push the element wider.

Watch the two readouts as you drag. Under border-box they stay locked together no matter how much padding you pile on, and under content-box they diverge the instant padding is nonzero.

Notice the one thing you never did in any of this: write the box-sizing rule yourself. You inherited it from Preflight, globally, on every element. That’s the intended state, and it’s worth protecting.

The spacing scale feeds every box-model utility

Section titled “The spacing scale feeds every box-model utility”

So far the numbers in p-4 and m-2 have probably looked like arbitrary labels you memorize. They aren’t. Every spacing utility in Tailwind is arithmetic over a single variable, and once you see the formula, the whole scale stops being magic.

p-*, m-*, gap-*, space-*, and the inset utilities all compile to calc(var(--spacing) * n), where n is the number in the class name. In Tailwind v4 the default --spacing is 0.25rem, which is 4px. So p-4 is calc(0.25rem * 4) = 1rem = 16px, and p-2 is 0.5rem = 8px. The number is a multiplier, not a pixel count.

That indirection is the point. Because every utility routes through one variable, you can rescale your entire spacing system by editing a single line in @theme, the same block where you defined your color tokens in Custom properties and the three-tier token model. This is the spacing tier of that same design-token system.

@theme {
--spacing: 0.25rem;
}

Change that 0.25rem to 0.2rem and every p-*, m-*, and gap-* in the app tightens at once, in proportion, with no find-and-replace. That single source of truth is what keeps a UI feeling consistent. It also encodes a convention you’ll see everywhere: the 4px grid. Material, Carbon, Tailwind, and shadcn all space things in multiples of 4. Staying on that grid is what makes a layout look intentional, even when no one consciously decided it should.

Tailwind does let you escape the scale with an arbitrary value like p-[17px] for a genuine one-off, but reach for it sparingly. A codebase peppered with p-[17px], mt-[13px], and gap-[5px] is a codebase with no system. The experienced habit is to stay on the scale, and if the scale genuinely doesn’t fit your design, change its base rather than litter magic numbers through your code. Treat off-grid sizing as a warning sign, not a tool you reach for.

To check that the formula has landed: given the default --spacing: 0.25rem, resolve each utility to its pixel value.

With the default --spacing of 0.25rem (4px), pick the rendered pixel value for each utility. Pick the right option from each dropdown, then press Check.

p-4 resolves to of padding on every side, px-2 puts of padding on the left and right, and m-6 is of margin.

Margin has one behavior that catches everyone the first time. You’re not learning margin collapse so you can use it. You’re learning it so that when it shows up in someone else’s CSS, you recognize what’s happening instead of losing an afternoon to it. Treat it as something to spot, not something to reach for.

The rule shows up in two shapes. The first is adjacent siblings: when two block elements stack vertically, the bottom margin of the upper one and the top margin of the lower one don’t add together. They collapse to the larger of the two. Put an mb-8 element above an mt-4 element and they sit 32px apart, not 48px. The browser keeps the bigger margin and throws the smaller one away.

mb-8 mt-4 expected 48px 32 + 16, the sum actual 32px the larger margin
Adjacent vertical margins collapse to the larger value, never the sum. 32px wins; the 16px is discarded.

The second shape is parent and first or last child: a block parent’s top margin can collapse together with its first child’s top margin (and its bottom with its last child’s bottom). The child’s margin appears to escape the parent and push the whole parent down instead. That’s how you get a phantom gap above a section that you can’t find by inspecting the parent, because the margin causing it lives on the child.

Two facts scope this trap tightly, and they’re what makes the modern approach work. Margin collapse happens only between block-level elements in normal flow , and only on the vertical (block) axis. It never happens between flex items, it never happens between grid items, and it never happens horizontally.

That scoping points to the better approach: don’t fix margin collapse, sidestep it. The instant a parent is a flex or grid container, its children stop collapsing, and you space them with gap instead of margins, so there are no margins left to collapse in the first place. The habit you’ll build over this chapter is flex flex-col gap-4, not a stack of mb-* utilities. You get the full treatment of gap in a later lesson of this chapter. For now just register the trade-off: legacy code stacks blocks with margins and pays the collapse cost, while your code uses gap and never sees it.

This section introduces a quiet shift in how you name directions, and it’s the one that earns you right-to-left support for free. So far you’ve thought in physical terms: left, right, top, bottom. Underneath, CSS thinks in logical terms tied to how text actually flows. Matching that model is what lets a layout mirror correctly in Arabic or Hebrew without a second stylesheet.

CSS names two axes for the box model. The inline axis is the direction text flows: in English that’s left-to-right, so the inline axis runs horizontally. The block axis is perpendicular, the direction block elements stack, which in English is top-to-bottom, so it runs vertically.

These names exist instead of just “horizontal” and “vertical” because they follow the writing system. In a RTL language the inline axis runs right-to-left; in a vertical writing mode the axes rotate ninety degrees. “Inline start” means wherever a line of text begins: the left edge in English, the right edge in Arabic. The physical edge changes with the language, but the logical concept doesn’t.

That’s why CSS grew logical properties: padding-inline-start instead of padding-left, margin-inline-end instead of margin-right. They resolve against the writing direction, so when the document flips to dir="rtl", a layout authored in logical properties mirrors itself automatically. Your “start” padding moves from the left edge to the right with no code changes.

Tailwind ships these as first-class utilities, and these are the ones you’ll actually type:

  • ps-* / pe-*: padding inline-start / inline-end (the logical twins of pl-* / pr-*).
  • ms-* / me-*: margin inline-start / inline-end.
  • pbs-* / pbe-* and mbs-* / mbe-*: the block-axis forms, for the rarer vertical-writing or block-direction cases. Worth knowing they exist, though you’ll reach for them seldom.

There’s a logical inset family too: inset-s-*, inset-e-*, inset-bs-*, and inset-be-*. It belongs to this same system and replaced the older start-* / end-* shorthands. It’s for positioned elements, so its behavior waits for the position-and-inset lesson later in this chapter; for now, file it under “same logical idea, applied to position.”

To see why these slightly less familiar names are worth it, watch a single utility produce two mirrored results. The following tabs render the same box with the same ps-8 class. The only thing that changes between them is the document’s writing direction.

ps-8 → start here
inline-start inline axis
ps-8 puts the padding on the left, which is inline-start in English. The inline axis runs left-to-right, so a line of text begins at the left edge.

The same class produces two opposite results, and you wrote it once. The same payoff scales to an entire interface built this way.

So here’s the default to adopt: ship logical utilities from day one in any project that might ever be localized to a right-to-left language. It costs you nothing over physical utilities and hands you RTL support the moment you flip a dir attribute. In a project that will only ever be English (or only ever LTR languages), physical utilities are perfectly fine, and the later migration to logical is purely mechanical. This is why the course’s conventions read “logical properties (ps-*, pe-*, inset-s-*) over physical (pl-*, pr-*) so RTL works for free,” and now you know what the “for free” is buying.

Here’s one drill to wire the physical-to-logical translation into your fingers. Match each physical utility on the left with its logical equivalent on the right.

Match each physical utility to its logical, direction-aware equivalent. Click an item on the left, then its match on the right. Press Check when done.

pl-4
ps-4
pr-2
pe-2
ml-6
ms-6
mr-auto
me-auto

mx-auto, the one centering case where margin still earns its weight

Section titled “mx-auto, the one centering case where margin still earns its weight”

Step back and look at how little margin you’ve actually needed. In 2026 you write padding constantly (space inside an element) and gap constantly (space between siblings), and margin almost never. The conventions go further and forbid sibling margins outright, because that’s gap’s job, and you just saw why margins between siblings invite collapse anyway. So it’s fair to ask: when does margin still earn a spot in your code at all?

The durable answer is centering one specific kind of element. mx-auto, which compiles to margin-inline: auto, centers a block element that has a defined width inside its parent. The mechanism is worth understanding rather than memorizing: when both left and right margins are auto, the browser takes whatever horizontal space is left over after the element’s width and splits it equally between the two sides. Equal margins on both sides means the element sits dead center.

That “defined width” part is essential. A full-width block has no leftover space to distribute, so mx-auto on its own does nothing; you have to constrain the width first. The canonical 2026 pattern pairs the two as a unit:

<article className="mx-auto max-w-3xl">
<h1>Pricing</h1>
<p>Pick the plan that fits your team.</p>
</article>

max-w-3xl caps the article’s width, and mx-auto centers what’s left. You’ll write this exact pairing for centered content columns and reading surfaces constantly. (max-w-3xl and the rest of the sizing model get their own lesson later in this chapter; here it’s just mx-auto’s required partner.)

The boundary matters too, because mx-auto is easy to overuse. It is specifically for a standalone block sitting in normal flow, with no flex or grid parent to do the centering instead. The moment the parent is a flex or grid container, centering becomes that container’s job through justify-center or place-items-center on the parent, and reaching for mx-auto on the child is the wrong tool. You’ll learn those container-centering utilities in the flexbox and grid lessons later in this chapter. The rule of thumb: use mx-auto for a lone block in flow, and flex or grid centering for everything inside a flex or grid parent.

Try the pairing once so it sticks. The card below is capped to a width but pinned to the left. Add the one class that centers it.

The card is capped with max-w-md but stuck to the left. Add one utility class so it centers horizontally in its parent.

Target
Your output LIVE

You now have a vocabulary for every band of an element, and that vocabulary pays off the next time a layout is “off by a few pixels.” The slow way to find the culprit is to start guessing and bisecting CSS. The fast way is to open DevTools and read the box model directly, because the browser will tell you the exact resolved numbers if you ask.

Hover any element in the Elements panel and the browser paints a color-coded overlay on the page: content in blue, padding in green, border in yellow, margin in tan-orange. Look back at the diagram that opened this lesson; those are the same four colors, on purpose. You already know how to read the overlay, because you’ve been reading it since the first figure.

For the exact numbers, the Computed panel shows the box-model diagram: the content’s measured size in the middle, surrounded by the padding, border, and margin values on each of the four sides. That’s where you confirm what border-box actually resolved to. Inspect your w-64 p-4 border card and you’ll see 256 in the center with 16 of padding and 2 of border folded inside it, exactly as the model promises.

This is where the lesson pays off. “Why is this element wider than I expect?” and “where is this phantom gap coming from?” become questions you answer in seconds by reading the panel, instead of editing CSS at random and hoping.

When you want to go deeper than the lesson covered, these four are worth bookmarking: two interactive explainers and two canonical references.