Nakodo's stylesheet is 865 lines with no hex codes in it. It also has no z-index in it, at all. Every layering decision in the app is a Tailwind utility on an element, which means I can count them:

$ grep -rhno 'z-\[[^]]*\]\|\bz-[0-9]\+' src --include="*.tsx" | sed 's/.*://' | sort | uniq -c 15 z-50 9 z-10 4 z-40 4 z-20 1 z-[5] 

33 uses, five values, 21 files, in 72,683 lines of TypeScript. The ladder reads like this:

z-50 every overlay: dialog, alert dialog, sheet, popover, tooltip, dropdown, select content, and the support panel z-40 the app's sticky header, the support widget's button, and the backdrop that covers a phone screen when the support panel opens z-20 sticky action bars that float over a long list, and the hero's copy z-10 sticky filter toolbars, the select menu's scroll arrows, and two birds z-[5] the leaves 

Five values, 10 apart, with a [5] wedged in because the leaf animation had to go over the hero plate and under the text. That is the whole system, and I did not design it, it accumulated. The question I actually wanted answered was whether it holds up in the browser, where layering is decided by stacking contexts rather than by numbers.

5,693 stacking contexts, and nine of them were asked for

I walked every element on all 76 public pages and recorded what, in its computed style, creates a stacking context:

76 pages, 5,693 stacking contexts, median 73 a page transform 5,449 opacity 279 position + z 9 isolation 1 backdrop-filter 0 (it is on the app's header, which is behind the login) filter 0 mix-blend-mode 0 will-change 0 contain 0 

Nine. Out of 5,693. And all nine of them are on the home page:

/ z-[5] svg the leaf gust overlay z-20 div the hero's heading and buttons z-10 svg x2 the two pecking birds z-20 span x5 the five tour notes on the sample card /glossary none /pricing none /privacy none 

Every page on the site except the home page gets its layering entirely from position and source order. 75 of 76 public pages do not contain a single z-index.

What the other 5,684 are

Almost all of them are inside drawings. On a typical content page:

/glossary 73 stacking contexts 70 transform inside <svg> 2 opacity inside <svg> 1 opacity the only HTML one on the page 0 z-index 

Our pages carry hand-drawn line art rather than images: 25 inline SVGs and 410 nodes inside them on /glossary, 78 SVGs and 801 nodes on the home page. Every <g transform="translate(…)"> in those drawings has a computed transform, and by the letter of the CSS Transforms spec that creates a stacking context.

The one HTML entry on each content page is the footer:

<footer className="relative overflow-hidden border-t border-foreground/10"> <div className="pointer-events-none absolute inset-0 opacity-10"> {/* two trees, four bushes, a bird and a row of grass tufts */} </div> <div className="relative mx-auto w-full max-w-6xl px-5 py-12 sm:px-8"> {/* 36 links */} </div> </footer> 

A drawing behind 36 links, with no z-index anywhere. The first child is absolute, so it leaves the flow; the second is relative, so it paints in the positioned-elements layer, and positioned siblings paint in source order. The opacity-10 fades the whole scene as one layer, which is why a shrub in front of a trunk hides the trunk rather than showing through it, and it happens to create the stacking context that keeps the drawing's own internals out of everyone else's way.

This is the part worth taking from the audit: relative with no z-index is a layering instruction. It is the quietest one available, it does not create a stacking context, and it is what most "I need this on top" situations actually want.

z-index does nothing where most of our contexts are

The 5,449 transforms inside SVGs create stacking contexts that cannot be used, because z-index does not apply to SVG child elements. Paste this into a console on any page:

const host = document.createElement("div"); host.innerHTML = `<svg width="60" height="60" style="position:absolute;top:0;left:0"> <rect id="a" x="0" y="0" width="40" height="40" fill="red" style="z-index:99"></rect> <rect id="b" x="10" y="10" width="40" height="40" fill="blue"></rect></svg>`; document.body.appendChild(host); console.log(document.elementFromPoint(25, 25).id, getComputedStyle(document.getElementById("a")).zIndex); host.remove(); 
b 99 

The computed z-index of the red rectangle is 99. The blue one, which has no z-index and comes second, is still on top. SVG 1.1 paints in document order and nothing else, and SVG 2's z-index has not shipped.

That is the same lesson I ran into building a page-turning book in Remotion, where the first version kept explicit z-index numbers for every page of the book and the working version deleted them, because the array order was already the answer and keeping a second copy of the ordering by hand is how the two copies disagree.

The two fifties

The one place in this app where two things could genuinely fight is the support panel against a dialog. Both are z-50. Both are fixed. Neither is nested inside the other.

They do not fight, and the reason is not a number. Radix renders dialogs, sheets and popovers into a portal appended to document.body, so an open dialog is always the last positioned element in the document. The support panel is rendered where it sits in the app layout. Equal z-index in the same stacking context means document order wins, and the portal is later, so the dialog is on top. Which is what you want: whatever you opened most recently is in front.

It is still luck. If the support panel were ever portalled too, the winner would become whichever opened last, which is also what you want, and if either one ever moved inside an ancestor with a transform the number would stop meaning anything at all. The app header uses backdrop-blur, which creates a stacking context, and the only reason that is harmless is that the header is a sibling of the content rather than an ancestor.

What I am changing

  • Names instead of numbers. Five values that mean "overlay", "shell", "floating bar", "sticky toolbar" and "leaves" should be five CSS variables with those names, so the next person adding a sticky thing picks a layer rather than guessing a decade.
  • z-[5] goes. It exists because the leaf overlay had to sit between a z-10 bird and a z-20 column, which is a sign the gaps were wrong rather than that the scale needed a new rung.
  • Nothing about the SVGs. 5,449 stacking contexts that cost nothing and cannot be used are not a problem, they are what a site drawn in vectors looks like from the inside.

The audit's real result is the ratio. 33 deliberate layering decisions in the source, nine of them visible on the public site, 5,684 that the browser created on its own because of how drawings are built. If you have ever gone hunting for the ancestor that trapped your z-index, that ratio is why: the contexts you did not write outnumber the ones you did by about six hundred to one.