Skip to content
Chapter 19Lesson 2

What flows down the DOM tree

The second half of how CSS resolves a value: which properties inherit down the DOM tree, and the rule it gives you for where each Tailwind utility belongs.

Here is a layout you’ve written a dozen times: a <body> that sets the font and text color once and trusts every element beneath it to pick them up.

app/layout.tsx
<body className="font-sans text-foreground">
<p>Your trial ends in 3 days.</p>
<button>Upgrade now</button>
</body>

Open this in a browser and the paragraph behaves: your sans font, your foreground color. The button does not. It renders in the browser’s default font and often a flat black or faded grey, ignoring the text-foreground set right above it. Same parent, same two utilities, opposite outcomes.

That gap hides two questions that turn out to be one. Why do the body’s styles flow down to the paragraph on their own, and why does the button opt out? By the end you’ll be able to answer both, and you’ll have a rule for where every styling utility belongs: which ones you set once high up and let fall, and which you write on each element by hand. We’ll return to the button, because how it escapes is the most instructive part.

The mechanism behind “the paragraph picked up the font” has a name: inheritance. Most people picture it as a global “styles flow downward” behavior. It isn’t. Inheritance is a flag the CSS spec sets property by property: every property is either inherited: yes or inherited: no. color says yes. padding says no. There’s no master switch over the top.

That flag decides what happens when an element has no declared value for a property, because no rule touched it after the cascade ran. The browser can’t leave the property blank, so it fills one in:

  • If the property is inheriting, the browser copies the computed value from the element’s parent.
  • If the property is non-inheriting, the browser uses the property’s initial value , the same everywhere and unrelated to the parent.

Note the word computed in that first bullet. The browser copies not the value you typed on the parent but the value the parent resolved to after its own cascade and unit math. That distinction is the entire reason for one of the classic CSS bugs you’ll meet near the end of this lesson.

Inheriting properties have reach. A value set on <html> or <body> falls all the way down, element to element, to every descendant unless a rule overrides it on the way. That’s why font-sans on <body> is enough for your whole app: you set it once at the top, and inheritance carries it to the deepest nested <span> for free, with no wrapper component forwarding it.

flows down color · font
DOM tree
stays put padding · width
< html >
set here
< body >
padding: 2rem
does not descend
inherits
< main >
inherits
< p >
Inheriting properties fall down the DOM tree to every descendant. Non-inheriting ones stay on the element that set them.

It flows down the DOM tree, not the JSX tree

Section titled “It flows down the DOM tree, not the JSX tree”

One caveat trips up almost everyone coming from React: inheritance walks the rendered DOM tree, from a real parent element to a real child, not your nested JSX components.

The two trees aren’t the same shape. A <Card> might sit ten layers deep in your JSX, wrapping <CardHeader> wrapping <CardTitle>, yet contribute only a single <div> to the DOM. Inheritance sees that <div>, not the component. So wrapping a subtree in a styling component does nothing on its own; you set an inheriting property on an ancestor DOM node, and it reaches the descendant DOM nodes below. When you reach for inheritance, picture the rendered page, not your component file.

You don’t need two lists to memorize. One heuristic generates both:

If a property describes the text, it inherits. If it describes the box (its size, position, background, or visual effects), it doesn’t.

The split follows from what each kind of style is for. Text styling is something you usually want to apply to a whole region at once: pick a font, color, and line height for an article, and every paragraph, list, and inline <em> inside should follow without repetition. Inheritance is exactly that “apply to everything underneath” behavior. Box geometry is the opposite. No two elements share a width by coincidence, and a child inheriting its parent’s padding would make every nested box balloon. So text-ish properties inherit and box-ish ones don’t, and you can predict the answer for a property you’ve never met.

Inherits — text-ish Describes the text, so it flows to every descendant.
Typography
color font-family font-size font-weight font-style line-height letter-spacing text-align text-transform white-space direction
Lists
list-style
Edge cases
cursor visibility
Design tokens
--*
Doesn't inherit — box-ish Describes the box — size, position, background, effects — so it stays local.
Box model
padding margin border width height min/max-*
Layout
display position top/right/bottom/left z-index flex grid gap
Background
background-color background-image
Effects
box-shadow opacity transform filter
Text-ish properties flow to descendants; box-ish properties stay local. The heuristic — does this describe the text, or the box? — predicts almost every property's family. (cursor and visibility, dotted, are the two that inherit despite feeling box-ish.)

Two rows are worth singling out.

cursor and visibility both inherit even though neither feels like text. cursor inherits so a pointer set on a clickable region covers the text and icons inside it; visibility inherits so hiding a container hides its contents. They are the heuristic’s edge cases, and they show up in the exercise.

The other is the one that powers your theme: CSS custom properties inherit. Write --color-foreground, override it in a .dark block on <html>, and the override reaches every component in the app, because <html> is the ancestor of everything. That is the substrate your design tokens are built on, covered later in this chapter. For now, one fact: text styles flow from <body>, tokens flow from :root.

This is where the two families meet Tailwind. Tailwind puts one declaration on one element, which suits the non-inheriting half exactly: write px-4 on each element that needs padding, and there is nothing to cascade. The inheriting half is what keeps those utility lists short. Set font-sans text-foreground once on <body> and inheritance carries it everywhere, so your leaf elements aren’t drowning in repeated typography classes. Knowing a property’s family is the same as knowing whether to hoist its utility to an ancestor or keep it local.

Decide each one by asking whether it describes the text or the box, not by recalling a list. At least one is a trap that feels like it belongs in the other bucket.

Predict each property's family using the text-vs-box heuristic — does it describe the text, or the box? Drag each item into the bucket it belongs to, then press Check.

Inherits Text-ish — flows to descendants
Doesn't inherit Box-ish — stays local
color
line-height
cursor
visibility
text-shadow
padding
width
box-shadow
background-color

Three catch people. text-shadow is a visual effect despite its name, so it doesn’t inherit; the word “text” is a red herring. And cursor and visibility inherit despite feeling box-ish. Hold those three and you have the whole classification.

Try it: change the parent, watch the children

Section titled “Try it: change the parent, watch the children”

The playground below is a small DOM subtree: a parent box wrapping a heading, a paragraph, and a nested label. Every control changes the parent.

Drag the font size up and all three text nodes grow together, because font-size inherits and each child reads the parent’s value. Change the color and they all shift at once; switch the font family and the whole subtree re-renders in the new face. One change at the top, and the entire subtree follows.

Now drag the padding slider. Only the parent box moves, its walls pushing outward, while the children stay put. padding does not inherit: it stops at the element that declares it.

Typography flows from the parent to every child; the box-model stays put on the parent. Drag font-size or color — every child moves together. Drag padding — only the parent's box grows.

Recognizing the global keywords: inherit, initial, unset, revert

Section titled “Recognizing the global keywords: inherit, initial, unset, revert”

Inheritance has so far been the fallback when no rule applies. You can also request it explicitly: a handful of global keywords override whatever the cascade decided and force a specific resolution. You’ll rarely write these on a Tailwind project, but you’ll read them in other people’s CSS and DevTools, so learn to recognize them.

.a { color: inherit; }
.b { margin: initial; }
.c { font-size: unset; }
.d { display: revert; }

Keep initial and revert straight. initial wipes a property to the spec’s default, often not what’s on screen: display: initial is inline even for a <div>, since the browser’s own stylesheet is what made it block. revert rolls back to that browser stylesheet instead. (revert-layer rolls back just to the previous cascade layer, an even rarer tie to the layer model.)

On a real project you write the utility form and let Tailwind emit the declaration:

<span className="text-inherit"></span> {/* → color: inherit */}
<div className="bg-transparent border-transparent"></div>

text-inherit, bg-transparent, border-transparent, and the occasional [all:revert] cover essentially every case. So a raw inherit or initial in hand-written CSS is a warning sign: usually someone is fighting a cascade they should have organized with layers instead.

Why form controls have their own font and color

Section titled “Why form controls have their own font and color”

Now we can solve the opening mystery: why the paragraph picked up the body’s font and color while the <button> didn’t.

This is inheritance working exactly as specified. Every browser ships its own stylesheet, the user-agent stylesheet , and that stylesheet sets font-family and color directly on form controls: <button>, <input>, <textarea>, and <select>. An inherited value is the weakest source in the cascade, the mere absence of a declaration, so any real declaration beats it. The body’s font-family reaches the button, finds the value the user-agent stylesheet already declared there, and loses. The button isn’t ignoring inheritance; it just isn’t relying on it, because something already set the value.

Browsers do this because form controls were once drawn by the operating system as native widgets, and pinning their typography keeps them looking like controls instead of inheriting whatever a page throws at them.

/* Browser's user-agent stylesheet — you don't write this */
button, input, select, textarea {
font-family: system-ui;
}

The control already carries a declared font-family, so the body’s font, an inherited value, never wins against it.

Tailwind’s Preflight, the reset that loads with @import "tailwindcss", ships exactly that rule, setting the controls’ font and color back to inherit so your <body> font reaches your buttons after all. The next lesson, Preflight, covers the rest of what Preflight does.

So on a real Tailwind project you don’t hand-set fonts on form controls: Preflight already inherited them for you. The one time you’d reach for a per-element utility is a stubborn browser-specific holdout that Preflight didn’t catch.

Without Preflight, a bare <button> inside <body className="text-red-500"> still renders with the browser’s default text color, not red. Open DevTools, select the button, and look at its Computed color: it traces to a rule on button in the user-agent stylesheet — and next to it, dimmed and struck through, sits an Inherited from body entry carrying red. What does that picture tell you?

The text-red-500 utility silently failed to apply — that struck-through Inherited from body entry means the class never reached the <body>.
The button would take the red if you marked it !important; without that, an inherited color can’t override a plain one.
The button is already carrying a color declared straight on it by the browser’s stylesheet, so the inherited red — the weakest source there is — gets struck through and loses.
color simply doesn’t reach descendants on its own, so the body’s red was never a candidate for the button in the first place.

currentColor: piggybacking on the inherited text color

Section titled “currentColor: piggybacking on the inherited text color”

currentColor is a CSS keyword that resolves to the element’s computed color: wherever you write it, it means “whatever the text color is here.” Because color inherits, that text color usually flowed down from an ancestor, so currentColor lets any property ride along with the inherited text color.

The canonical use is SVG icons. An inline icon with fill="currentColor" paints itself in the text color wherever you drop it, so a check icon next to body text matches with no effort. Set text-blue-500 on a button and both the label and the icon turn blue from that one utility, because both read currentColor. Lucide and shadcn icons default to this, which is why their color just works without styling the icon directly.

The Tailwind utilities are fill-current and stroke-current, but you’ll often write neither: the icon already defaults to currentColor, so setting text-* on the parent is enough. Make the icon below follow its label’s color.

The button colors its label with text-indigo-600, but the check icon is pinned to a hardcoded red — it ignores the label. Make the icon follow the text color so it turns indigo too, by pointing the SVG's fill at the current color (or using the fill-current utility). Match the target.

Target
Your output LIVE

currentColor also hides in borders: Preflight resets them to border: 0 solid currentColor, which is why a fresh border utility tints to your text color until you set a border-* color. The next lesson picks up that thread.

Effects that look like inheritance but aren’t

Section titled “Effects that look like inheritance but aren’t”

Four effects look like inheritance, a parent changes and the children visibly respond, but the mechanism is something else. Mistaking them leads to confusing bugs, because the fix depends on what’s actually happening.

opacity is not inherited. Set opacity: 0.5 on a parent and every child fades, so people conclude it inherits. It doesn’t. The parent and its whole subtree flatten into a single composited group, and that group is made half-transparent as one unit. The children fade as a side effect, not because each got an opacity value. The tell: a child cannot set opacity: 1 to become solid again, because it’s painted inside the already-transparent group with nothing to opt out of. A genuinely inherited property like color, by contrast, a child can freely override.

parent · opacity: 0.5
child A declares nothing → faded
child B opacity: 1 → still faded
The parent is one half-transparent group; the child's opacity: 1 does nothing — it can't escape the group, so both children stay equally faded.

visibility: hidden is inherited, and a child can reverse it. Hide a parent and its children vanish too, because visibility inherits. But unlike most inherited values, a child can set visibility: visible to reappear, while still taking up its layout space. That reversibility is what separates it from display: none, which does not inherit and removes the element from layout entirely. You can’t bring back a single child of a display: none ancestor, because the whole subtree is gone from the render.

Transparent backgrounds are passthrough, not inheritance. A child with no background of its own shows the parent’s, which looks inherited. It isn’t: background-color doesn’t inherit. Its default is transparent, so the child is simply see-through, and you’re looking at the parent’s background through it. The tell: change the parent’s background and what shows changes, but inspect the child and its background-color is still transparent. It never took the parent’s color, it just isn’t covering it.

em font-size compounds, so prefer rem. Recall that a child inherits the parent’s computed font-size. Now nest elements that each set font-size: 1.25em. The first is 1.25× its parent, the second 1.25× that (1.56×), the third 1.95×. Each level multiplies against the computed result above it, and the text runs away. The fix, and the course default, is rem: it’s relative to the root font-size, not the parent, so it never compounds. Tailwind’s text-* scale emits rem, so you get the non-compounding behavior just by using the scale.

Each statement below targets one of the tells above.

Each claim is about a case where the page's behavior either is, or only looks like, inheritance. Mark each statement True or False.

opacity is an inheriting property — that’s why setting it on a parent fades the children.

opacity doesn’t inherit. The parent and its subtree flatten into a single composited group that’s made translucent as a whole; the children fade as a side effect of the group being see-through. The tell: a child can’t set opacity: 1 to become solid again, because it’s painted inside an already-transparent group.

A child can override an inherited color, and a child can set visibility: visible to reappear inside a visibility: hidden parent.

Both color and visibility inherit, and an inherited value is the weakest source in the cascade — the mere absence of a declaration. So a child’s own declaration always outranks it, which is exactly why a child can re-show itself with visibility: visible.

A child element with no background-color shows its parent’s background because its own background defaults to transparent, not because background inherits.

background-color doesn’t inherit; its default is transparent, so you’re seeing the parent’s background through the child. The tell: inspect the child and its own background-color is still transparent — it never took the parent’s color, it just isn’t covering it.

Nesting elements that each set font-size: 1.25rem makes the text compound and grow at every level.

That’s em, not rem. em is relative to the parent’s computed font-size, so it multiplies at every level and runs away. rem is relative to the root font-size and never compounds — which is why Tailwind’s text-* scale emits rem.

The placement rule: typography up, box-model down

Section titled “The placement rule: typography up, box-model down”

One rule follows from all of this, and you’ll carry it to every project:

Put typography and color utilities on <body> (or a high ancestor). They inherit, so they reach the whole app for free. Put box-model and layout utilities on each element. They don’t inherit, so they have to be local.

Tokens follow the same logic one level up: custom-property tokens go on :root and .dark, because custom properties inherit. More on that later in the chapter.

In a real codebase, your root layout sets the typographic baseline once, and each leaf component carries only the box and layout utilities it needs. No typography is repeated, because it already flowed down.

app/layout.tsx
<body className="font-sans text-foreground antialiased">
{children}
</body>
// components/plan-badge.tsx
<span className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-muted">
<CheckIcon className="size-4" />
Pro plan
</span>

Typography and color live once, high up, on <body>. Both inherit, so every element below picks up this font and color for free.

app/layout.tsx
<body className="font-sans text-foreground antialiased">
{children}
</body>
// components/plan-badge.tsx
<span className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-muted">
<CheckIcon className="size-4" />
Pro plan
</span>

Box-model and layout utilities live on the element. None inherit, so the badge declares the spacing, shape, and background it needs, right where it’s used.

app/layout.tsx
<body className="font-sans text-foreground antialiased">
{children}
</body>
// components/plan-badge.tsx
<span className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-muted">
<CheckIcon className="size-4" />
Pro plan
</span>

Read the leaf for what isn’t there: no font-*, no text-*. The badge’s typography flowed down from <body> and never needed restating. That absence is the rule working.

1 / 1

When a style misbehaves and you suspect inheritance, the browser shows you where each value came from.

The Styles panel lists an element’s own rules first, then, dimmed below, sections headed Inherited from <selector>, one per ancestor that contributed an inherited value, so you can see that the color here came from a rule on body three ancestors up. The Computed panel goes further: expand any property and it traces where the final value resolved, flagging inherited ones by their ancestor.

Styles
Computed
Layout
Event Listeners
body main div.card span.badge
Filter Show all
font-family : ui-sans-serif, system-ui, …
Inherited from body
var(--font-sans) body globals.css:5
color : oklch(0.21 0.03 264)
Inherited from body
var(--color-foreground) body globals.css:6
padding : 4px 12px this element
4px 12px .badge globals.css:41
line-height : 24px
1 Inherited from — the section that appears under a value the element didn't declare itself.
2 The ancestor — names exactly which element the value fell from. Here color came from body.
3 No attributionpadding doesn't inherit, so it resolved on the element, with no ancestor line.
The Computed panel flags each inherited property with the ancestor it resolved from — here, font-family and color fell from body, while padding shows no attribution because it resolved on the element itself.

It’s the same move as the cascade trace from the last lesson: open Computed, find the misbehaving property, and read where it resolved. For an inheriting property that isn’t taking effect, the panel tells you whether it inherited the value you expected or got overridden on the way down.

Durable references if you want the spec-level detail behind the families and the keywords.