Inheritance and Computed Style
From declared value to cascaded, specified, computed, used and actual value — which properties inherit, what inherit, initial, unset and revert each mean, and why reading style back can force layout.
The intent, the obvious build, and why it breaks
Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.
After the cascade picks a winner, what does the browser actually store on the element — and why is that not the number I see on screen?
Someone sets a font on a container and expects the text inside it to follow. Then they set a width in percent and expect to be able to read it back, and get a percentage string, or a pixel number, depending on which API they asked.
The cascade picks a value and the element has it. getComputedStyle tells me what it is, and inheritance means children get whatever the parent has.
Most properties do not inherit. color, font-*, line-height, visibility, cursor and the text properties do; background, border, padding, margin, display, width and almost everything geometric do not. Expecting border to inherit is a genuinely common first bug.
- Most properties do not inherit.
color,font-*,line-height,visibility,cursorand the text properties do;background,border,padding,margin,display,widthand almost everything geometric do not. Expectingborderto inherit is a genuinely common first bug. - The value stored on the element is not the value you wrote.
font-size: 1.5emis stored as an absolute length;color: currentColoris stored as the resolved colour; a relative URL is stored as absolute. - It is also not the value on screen.
width: 50%computes to50%and only becomes a pixel count at layout time, when there is a containing block to be half of. getComputedStyleis not one thing. For some properties it returns the computed value and for others the *used* value, which means the same call can be free for one property and force a synchronous layout for another (Layout Thrashing).- It has no account of
inherit,initial,unsetandrevert, which are four different answers to "make this go away" and are routinely used interchangeably.
What is actually happening
In the browser, not in the framework.
- Declared → cascaded. Every declaration that applies is declared; the cascade picks at most one winner per property. That winner is the cascaded value (The Cascade).
- Cascaded → specified. If there is no cascaded value, defaulting fills one in: the inherited value from the parent for inherited properties, the initial value for the rest. This is where inheritance actually happens — it is a *defaulting* step, not a copying step.
- Specified → computed. Relative things are resolved as far as they can be without layout:
emandrembecome absolute lengths, keywords likebolderbecome numbers, relative URLs become absolute,currentColorresolves. This is also wherevar()substitution happens (Custom Properties). - Computed → used. Anything that needed layout is resolved now: percentages against a containing block,
automargins, flex and grid track sizes (Intrinsic Sizing and the Automatic Minimum). - Used → actual. The final adjustment for the device: rounding to physical pixels, font fallback when the requested face is unavailable (The Viewport and Device Pixels).
- The four global keywords act at the specified-value step.
inherittakes the parent's computed value even for a non-inherited property;initialtakes the property's spec-defined initial value, which is often not what the browser's own stylesheet uses;unsetmeansinheritfor inherited properties andinitialfor the rest;revertrolls back to what the previous cascade origin would have produced — usually the user-agent stylesheet — andrevert-layerdoes the same for the previous layer. - Inheritance flows down the element tree, not the visual layout. A positioned element inherits from its DOM parent no matter where it renders (Positioning and Stacking Contexts).
What this makes the browser do
And which of it is avoidable.
- Computing and storing one computed value per property per element, for every element in the document. This is the memory footprint of style, and it is proportional to element count.
- Sharing that work aggressively: engines cache computed styles for elements with identical matched rules and identical inherited context, so a list of a thousand identical rows does not run the pipeline a thousand times (Style Calculation).
- Propagating inherited changes down the subtree. Changing an inherited property near the root —
color,font-size— invalidates every descendant, which is the single largest style invalidation an application performs (Style Invalidation). - Flushing pending style and layout when script asks a question that needs an answer geometry can provide. That flush is synchronous and it is on the main thread.
- The avoidable half: reads interleaved with writes. Every alternation forces another flush, turning a linear operation into a quadratic one.
One value, six stages
The value you write and the value on screen are separated by more steps than most people carry around, and nearly every confusing CSS result is a step boundary. Percentages that will not resolve, em sizes that compound, a reset that gives inline instead of block — each is a specific stage doing exactly what it is defined to do.
Walk the stages in order when something is wrong. The question "at which stage does this value stop being what I wrote" is answerable, and it usually takes one look at the Computed pane.
- 1Declared
Every declaration in every stylesheet that applies to this element and property.
fails by A declaration dropped at parse time for an unsupported value, so it was never declared at all (The CSSOM).
- 2Cascaded
The cascade picks at most one winner, by origin, context, element-attached, layer, specificity, order.
fails by Losing at a step you were not looking at (The Cascade).
- 3Specified
If nothing was cascaded, default: inherit from the parent for inherited properties, initial value otherwise. Global keywords act here.
fails by Expecting
borderorpaddingto inherit. They do not; only their initial values apply. - 4Computed
Resolve everything possible without layout:
em/remto lengths, keywords to numbers,currentColor, relative URLs,var()substitution.fails by
font-sizeinemcompounding through nested elements of the same class, because each level computes against its parent's computed size. - 5Used
Resolve what needed layout: percentages against a containing block,
auto, flex and grid track sizes.fails by A percentage height with no definite containing-block height, so it resolves to
autoand appears to be ignored (Intrinsic Sizing and the Automatic Minimum). - 6Actual
Device adjustments: rounding to physical pixels, font fallback for a face that is not available.
fails by Sub-pixel rounding producing a one-pixel seam between adjacent elements (The Viewport and Device Pixels).
The computed/used boundary is the important one. Everything before it happens during style calculation; everything after it needs layout, which is why reading a used value can force one.
Inheriting, and the four ways to undo
Inheritance is a defaulting rule, not a copy: when an element has no cascaded value for an inherited property, it takes the parent's *computed* value. That "computed" matters — the child inherits an already-resolved length, not the em expression that produced it.
The four global keywords are the escape hatches, and they are not interchangeable. The table below is the one to memorise, because the difference between initial and revert is the difference between display: inline and display: block on a div, and that has ended more debugging sessions than it should.
| Keyword | What the element gets | Use it when | The trap |
|---|---|---|---|
inherit | The parent's computed value, even for a non-inherited property | You want a border or a background to follow an ancestor deliberately | On the root element there is no parent, so it behaves as initial |
initial | The property's spec-defined initial value | You want the CSS-defined default, independent of any browser | display: initial is inline, and color: initial is usually black, not the browser default text colour |
unset | inherit if the property inherits, initial if it does not | You want "as if no rule had been written" | Still lands on spec initials, so it inherits display: inline on a div too |
revert | What the previous cascade origin would have produced — usually the user-agent stylesheet | You want "the browser's normal behaviour back" | The user-agent stylesheet is not standardised, so this is a per-browser value |
revert-layer | What the previous cascade layer would have produced | Undoing one layer's contribution without falling all the way back | Meaningless outside a layer; falls back to revert behaviour |
all: revert | Every property reverted at once | Establishing a clean boundary around embedded or third-party markup | Removes your intentional typography too, so you re-establish it inside |
1:root {2 /* Set inherited properties once. Everything below follows. */3 color-scheme: light dark;4 color: canvastext;5 font-family: system-ui, sans-serif;6 line-height: 1.5; /* unitless: recomputed per descendant font size */7 /* NOT set: font-size. Leave the root at the browser default so a user8 who raised their default text size actually gets larger text. */9}10 11.icon {12 /* Inherits by proxy: color already inherits, so one declaration keeps13 the icon in step with its text, including in forced-colors mode. */14 fill: currentColor;15 block-size: 1em; /* computed against this element's font-size */16}17 18.embed-boundary {19 /* Third-party markup should not inherit our typography. */20 all: revert;21 /* ...then re-establish anything it genuinely needs. */22}23 24/* The compounding trap: each level computes against its PARENT's25 already-computed size, so .tag .tag .tag shrinks three times. */26.tag { font-size: 0.875em; }27 28/* Fixed against the root instead, so nesting does not compound. */29.tag { font-size: 0.875rem; }The two .tag rules differ by one character and by whether nesting changes the result. em is relative to the parent; rem is relative to the root.
Hiding, and what it does to the accessibility tree
The most consequential computed-style decision in most applications is which of several ways to hide something you pick, because they differ in whether the content stays in the accessibility tree and whether it stays focusable. Getting this wrong produces a control that a sighted mouse user cannot see and a keyboard user lands on.
The spec is unambiguous here, so this is a matter of knowing the table rather than of judgement: display: none and visibility: hidden remove from layout and from the tree; opacity: 0 and off-screen positioning remove from *sight* only.
| Change | style | layout | paint | composite | Why |
|---|---|---|---|---|---|
| `getComputedStyle(el).color` | yes | no | no | no | Flushes pending style only. color is fully resolved at computed-value time, so no geometry is needed. |
| `getComputedStyle(el).width` | yes | yes | no | no | A used value. The browser must finish layout before it can answer with a number. |
| `el.getBoundingClientRect()` | yes | yes | no | no | Always a used-value read. Cheap once per frame, ruinous once per list item (Layout Thrashing). |
| Change `color` on `:root` | yes | no | yes | yes | Inherited, so every descendant is invalidated — but no geometry changed, so layout is skipped. |
| Change `font-size` on `:root` | yes | yes | yes | yes | Inherited *and* geometric. Every descendant restyles and the whole document relayouts. The most expensive one-line change in CSS. |
| Toggle `display: none` on a subtree | yes | yes | yes | yes | Boxes are destroyed and everything after it reflows. Also removes the subtree from the accessibility tree. |
| Toggle `visibility: hidden` on a subtree | yes | no | yes | yes | Inherited, and the box is preserved, so surrounding geometry does not move. Still removed from the accessibility tree. |
caveat The layout column collapses to no for a subtree under contain: layout or content-visibility, because the browser can prove nothing outside it moved (CSS Containment). Everything here also depends on what the engine had already marked dirty when your code ran.
semantics The element keeps whatever role its markup gives it. What changes is whether it is exposed at all: display: none and visibility: hidden remove the node from the accessibility tree; clipping to a tiny box does not; aria-hidden="true" removes it from the tree while leaving it visible and focusable.
| Tab | Reaches anything not removed by display: none, visibility: hidden, the hidden attribute, inert, or a negative tabindex — including elements at opacity: 0 or positioned off-screen. |
| Tab (skip link) | The canonical case for visually-hidden: the link is clipped until focused, then :focus-visible restores its box so a sighted keyboard user can see where they are. |
| Escape | Closes a hidden-by-collapse region. Whatever the close path is, the hidden state must be the same one CSS reads and assistive technology reads. |
- — Never leave focus on an element you just hid. The browser moves focus to the body, and the keyboard user loses their place entirely (Focus Management).
- — Use
inerton a subtree that should be neither focusable nor announced while remaining visible — a background behind a modal. It is the correct primitive and it does not touch computed style. - — A visually-hidden element that becomes visible on focus must have a real, visible focus indicator when it does.
- — A region hidden with
display: noneis simply absent; screen readers do not announce it and do not count it in landmark or heading lists. - — A visually-hidden label or hint is announced normally, which is exactly why the pattern exists.
- — Toggling
displayon a live region defeats it: the region must be present in the tree before the content changes for the change to be announced (Live Regions and Announcement).
usually broken by The pattern invites hiding with opacity: 0, visibility on an ancestor, or off-screen positioning and assuming the content is gone. Two of those leave the content focusable and announced, so a keyboard user tabs into an invisible dropdown and a screen-reader user hears a menu that is not on screen. Decide explicitly which of the three states you want — hidden from everyone, hidden from sight only, or hidden from assistive technology only — and use the mechanism that matches.
How to build it
Most important first.
- Set inherited properties once, high up. Typography, colour and text rendering belong on
:rootor a layout container; repeating them per component is bytes plus a wider invalidation surface for no benefit (Responsive Typography). - Reach for
revertrather thaninitialwhen you want "the browser's normal behaviour back".display: initialisinline, which is almost never what someone removing a rule from adivwanted;display: revertgivesblock. - Use
all: reverton a container to escape an inherited context wholesale — useful when embedding third-party markup you do not want your typography to reach into. - Batch all your style reads before all your style writes. One flush at the start of a frame is fine; twenty alternating ones is the classic layout thrash (Layout Thrashing).
- Prefer
currentColorfor borders, outlines and icon fills. It inherits by proxy — the element already inheritscolor— so one declaration keeps a whole component in step, including in forced-colors mode (Contrast, Colour and Motion). - Do not read layout to make a decision that CSS can express.
getBoundingClientRectin a resize handler where a container query would do is a forced layout you chose to pay for (Container Queries).
Keyboard, focus, semantics, announcement
A required field on every lesson in this domain, not a section added when there is room.
display: noneandvisibility: hiddenremove content from the accessibility tree entirely, along with the layout box.opacity: 0,clip-path, and moving something off-screen do not — the content stays focusable and announced, which is how keyboard users end up tabbing into an invisible menu (The Accessibility Tree).- That asymmetry is the basis of the visually-hidden pattern: content clipped to a one-pixel box is still in the tree, so a skip link or a form hint can be available to a screen reader without being drawn (Semantics Before ARIA).
aria-hidden="true"removes from the accessibility tree while leaving the element visible and focusable. Combining it with a focusable element produces a control a screen-reader user can land on and receives no name for (The Rules of ARIA).- Setting a root
font-sizeinpxoverrides the user's browser font-size preference for everything that inherits from it. Keep the root at the browser default and size inrem, so text scaling works (Responsive Typography). - Unitless
line-heightinherits correctly through nested elements with different font sizes; a fixed length does not, and the result is overlapping text for anyone who scales up. This is a real accessibility consequence of a computed-value rule.
What can go wrong
- Setting
font-sizeinemon nested elements of the same class, so the size compounds with depth. Each level computes against its parent's already-computed size. - Using a unitless
line-heightversus a length: the unitless value inherits as a *number* and is recomputed per descendant against that descendant's font size; a length inherits as a fixed length and silently crops text in nested elements with larger fonts. - Assuming
display: nonestill runs your styles. No boxes are generated at all, so anything depending on measured geometry inside a hidden subtree reads zero (Intrinsic Sizing and the Automatic Minimum). - A
getComputedStylecall inside a loop over list items — one forced layout per iteration, which is the most common way a smooth interaction becomes a visible stall (Long Tasks). - The mitigation failing: caching a computed value and reusing it after the DOM changed, so you now render against a stale measurement, which is worse than the slow version because it is wrong rather than slow.
- Computed style is readable by any script in the document, which makes it a side channel: historically
:visitedlink styling leaked browsing history throughgetComputedStyle, and browsers now deliberately lie about visited-link styles for exactly that reason (The Browser Security Model). - That mitigation is itself a lesson in what the browser will enforce: the value you get back is not always the value in use, because privacy won over accuracy.
- Inherited properties cross into content you did not author. Embedded third-party markup inherits your typography and colour, which is a rendering surprise rather than a vulnerability — but a container with
all: revertis the honest boundary (Third-Party Scripts and the Supply Chain). - Nothing here isolates anything. If you need a subtree whose styles cannot be affected by the page, that is shadow DOM, and even then inherited properties still cross the boundary by design (Shadow DOM and the Composed Tree).
- "Everything inherits." Most properties do not; the inherited set is roughly the text-related ones plus a handful of others, and the spec lists it per property.
- "
unsetandrevertare the same."unsetgoes to inherit-or-initial;revertgoes back to what the previous origin — usually the user-agent stylesheet — would have said. Fordisplayon adivthose areinlineandblock. - "
getComputedStyleis a cheap read." It is a *synchronising* read. For layout-dependent properties it forces the browser to finish work it had deferred. - "Computed style is what I see." The used value is what you see. Computed is the last stage before layout gets involved.
- "
display: nonejust hides it." It removes the element from layout *and* from the accessibility tree, which is why it is the correct way to hide something and the wrong way to visually hide something you still want announced.
Measuring it, and what changes in the field
- The Computed pane shows the final computed value for every property, with an expandable trace of the declarations it beat and the ancestor an inherited value came from (A Mental Model of the Devtools).
- Forced synchronous layouts appear in the Performance panel as their own marked entries, usually with a warning triangle and a stack pointing at the exact read (Debugging Rendering and Jank).
- Recalculate Style entries name the number of elements affected, which is how you find an inherited-property change near the root that is invalidating far more than you expected.
- For text scaling, the cheapest test is the browser's own font-size setting plus page zoom, not a devtools emulation — they exercise different code paths.
- On a large document, changing an inherited property on
:rootinvalidates every element. The same change on a leaf invalidates one. The declaration is identical; the cost differs by four orders of magnitude (CSS Containment). - On a slow device, the cost of a forced layout is proportional to how much geometry is dirty, so the same interleaved read-write loop that is imperceptible on a laptop can drop frames on a mid-range phone.
- With a large list, computed style memory becomes real: every rendered row stores a full set of values, which is one of the arguments for virtualisation (List Virtualization).
- Under user font scaling or page zoom, everything sized in
em,remorchmoves and everything inpxdoes not — so a layout mixing both breaks in a way it never does at default settings.
- Relying on inheritance keeps stylesheets small and makes global changes trivial. It also means a component's appearance depends on where it is mounted, which is exactly the property that makes a design system hard to test in isolation (What a Component Owes Its Caller).
all: reverton a boundary gives you predictable isolation and throws away every intentional inherited value, so you must re-establish typography inside.- Reading computed style is the only way to know what the browser actually did. It is also the operation most likely to cost you a frame, so the accurate answer and the fast path are in tension.
- Sizing everything in relative units makes text scaling work and makes precise alignment harder, because everything moves at once.
Where this applies
Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.
- GENERALThe six value stages, the inherited-property set and the meaning of the four global keywords are all specified in CSS Cascading and Inheritance, and engines agree. Where they historically differed — which properties
getComputedStyleresolves to used values — has largely converged. - ENGINE-SPECIFICThe user-agent stylesheet is not standardised beyond a non-normative sample, so
revertlands on slightly different values across engines: default form-control appearance,marginon headings and the exact rendering of<hr>all differ between Blink, Gecko and WebKit.revertgives you "the browser's default", which is a per-browser answer. - BROWSER-SPECIFICOnly some browsers flag forced synchronous layout in the profiler: Chrome marks it explicitly with a warning and a causing stack, Firefox surfaces it less directly in its profiler markers, and Safari does not label it as such — so the same code looks like a diagnosable problem in one browser and like unexplained time in another.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — Programming Languages & Runtime Internals — lazy evaluation and forced materialisation:
getComputedStyleis a read that collapses deferred work, and the interleaving cost is the same shape as any pull-based pipeline forced element by element.