Finding the Reason in the Ancestors
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
- Make
.badgerelative to.card(the containing block of absolute positioning) - Make
.overlayrelative to the viewport (the default containing block offixed) - Where you put
transform— the trap wherefixedgets trapped - Make
.topbarsticky (the threshold) - Make
.tocactually stick (choosing the scroll box) - Make
z-indexcompete at the root (stacking contexts) - Save the calculator's result as
07-boxes.txt - On narrow screens, put the table of contents back in the flow (the order trap again)
- Wrap-up →
09-notes.md
Notes
- In step 3, first put
transformon.cardand run the calculator to see the trapping yourself, and then move that property to.badge. - The single line
inset: 0means setting all four directions to 0. - The length of the sticking range depends on the height of the parent box, so you can't tell it from the calculator. You have to see that in a browser.
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.