TT Lab
Get started
Learn Learning paths Courses

CSS — The Later Rule Wins

Finding the Reason in the Ancestors

Continue in TT Lab

Goal

You compute directly, going up the ancestors, the containing block, the scroll box, and the stacking context. There is no browser, but for these three, the answer is determined by markup and CSS alone.

Why it matters

For bugs where fixed doesn't stay and sticky doesn't follow, the symptom gives no hint of the cause at all. That element's CSS is perfectly right, and if you look at just that element in the developer tools, nothing is wrong. The place to look is the ancestor.

If an ancestor has a single transform, the reference for fixed changes from the viewport to that ancestor. If an ancestor has a single overflow: hidden, that box becomes a scroll box and sticky never gets a chance to stick. A card hover effect and a modal look unrelated, so if you don't know the rules, finding the cause takes hours.

This lab's document

The grader and the calculator assume the structure below. You don't write HTML.

div.page
├ header.topbar
└ div.shell
  ├ aside.side
  │ └ nav.toc
  └ main.content
    └ div.card
      ├ span.badge
      └ div.overlay

What to build

It is a single file, /root/work/pos/layout.css.

The calculator

cd /root/work/pos
python3 /opt/lab/checks/css-pos-lab/boxes.py layout.css
python3 /opt/lab/checks/css-pos-lab/boxes.py layout.css 500

For each element it prints position, the containing block, the sticky verdict, and the z-index scope. If you give the screen width as the second argument, it recomputes at that width.

Steps

  1. Make .badge relative to .card (the containing block of absolute positioning)
  2. Make .overlay relative to the viewport (the default containing block of fixed)
  3. Where you put transform — the trap where fixed gets trapped
  4. Make .topbar sticky (the threshold)
  5. Make .toc actually stick (choosing the scroll box)
  6. Make z-index compete at the root (stacking contexts)
  7. Save the calculator's result as 07-boxes.txt
  8. On narrow screens, put the table of contents back in the flow (the order trap again)
  9. Wrap-up → 09-notes.md

Notes

Create the reference for absolute positioning

Create /root/work/pos/layout.css, make .badge position: absolute, and give it two or more directions (for example top and right). Its containing block must be .card.

Run mkdir -p /root/work/pos. The containing block of absolute is the nearest positioned ancestor. relative leaves the element in its own place while creating a reference, so it is the most common choice for this purpose.

Pin the overlay to the viewport

Make .overlay position: fixed and set all four directions (the single line inset: 0 also works). Give it a z-index too. Its containing block must be the viewport.

The default containing block of fixed is the viewport — so it doesn't follow you when you scroll and stays on the screen. All four directions must be set for it to cover the screen completely.

Where to put transform

Give .badge a transform so that it juts slightly over the corner. .card must not have any of transform, filter, perspective, will-change, or contain, and the containing block of .overlay must still be the viewport.

First put transform: translateZ(0) on .card and run the calculator — the containing block of .overlay changes to .card. That is the trap this lab wants to show. Once you have confirmed it, move that property to the element that actually needs to move.

Pin the header bar

Make .topbar position: sticky and give it a top. The calculator must give a verdict of "sticks."

If there is no threshold (at least one of top, right, bottom, and left), there is no line to stick to, so it just flows along. The fact that there is no error and no warning is what lets this bug survive so long.

Choose the scroll box

Make .toc position: sticky; top: 64px, and give .page overflow-y: auto so that .page is the scroll box. .shell and .side must not have any overflow other than visible.

Sticky sticks only within the nearest scroll box. If an intermediate container has a single overflow: hidden, that becomes the scroll box, and since the user doesn't scroll that box, it never gets a chance to stick. overflow-x: hidden has the same result.

The range in which z-index works

Give .topbar a z-index, and make the z-index of .overlay larger than that. None of .page, .shell, .content, or .card may create a stacking context.

If an ancestor creates a stacking context, the z-index inside it can't get out, however large it is. A careless line like opacity: 0.99 or transform creates one. The calculator also prints "inside which box z-index takes effect."

Check with the calculator

Run boxes.py and save the result as /root/work/pos/07-boxes.txt. The output must contain no "does not stick" verdict (the calculator prints it in Korean), and all the element lines must be present without being cut off.

python3 /opt/lab/checks/css-pos-lab/boxes.py layout.css | tee 07-boxes.txt. The parentheses next to the containing block tell you why it was decided that way — the ancestor's name and its property.

Back in the flow on narrow screens

In @media (max-width: 700px), return .toc to position: static. On a wide screen (1280px) it must still stick. The media query must be placed below the base rule.

When specificity is equal, the one written later wins. Being inside a media query doesn't make it stronger — if you put the base rule below the media query, the media query can do nothing. Check the narrow screen with boxes.py layout.css 500.

Write down the diagnosis order

In /root/work/pos/09-notes.md, write at least three lines (at least 120 characters). The text must contain 담음 블록, transform, and overflow (the first is the Korean phrase for "containing block"). The sheet must also still pass the criteria of the earlier steps.

The core is one thing — the cause is always in an ancestor. Write down in your own words what to look for, going up the ancestors, the next time you meet the same symptom.