Root element now creates a stacking context unconditionally. Refactored display list builder to correctly bucket stacking participants into three groups (negative/zero/positive z-index) per Appendix E steps 2/6/7. Fixed z-index:0 elements painting at step 7 instead of step 6, and descendant stacking contexts being trapped inside positioned z-index:auto ancestors instead of participating in the parent stacking context. Added StackingBuckets struct, unified step-6 rendering with tree-order merge by node_id, and 7 unit tests for creates_stacking_context(). Added 6 golden tests (213-218) and 4 integration tests asserting paint-order invariants programmatically. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
16 KiB
Story 1.2: Stacking Contexts & Z-Index
Status: done
Story
As a web user, I want overlapping elements to render in the correct front-to-back order, so that menus, modals, and layered content display correctly.
Acceptance Criteria
- Given an element with
position: relative/absolute/fixedand az-indexvalue other thanauto, When the page is rendered, Then a new stacking context is created per CSS 2.1 §9.9.1 - Given elements within the same stacking context, When the page is rendered, Then painting order follows CSS 2.1 §E.2: backgrounds/borders → negative z-index → block flow → floats → inline flow → z-index 0/auto → positive z-index
- Given nested stacking contexts, When the page is rendered, Then child stacking contexts are painted atomically within their parent's z-order position
- Golden tests cover overlapping positioned elements with various z-index values, checklist is updated, and
just cipasses
Tasks / Subtasks
-
Task 1: Root element stacking context (AC: #1, #2)
- 1.1 Update
creates_stacking_context()incrates/layout/src/types.rs(~L523-525) to returntruefor the root element (NodeId(0) or document root), regardless of positioning/z-index - 1.2 Unit test confirming root element creates stacking context without positioning
- 1.1 Update
-
Task 2: Verify and fix Appendix E paint order completeness (AC: #2)
- 2.1 Audit
render_stacking_context()incrates/display_list/src/builder.rs(L148-350) against CSS 2.1 §E.2 step-by-step - 2.2 Verify Step 2 (negative z-index descendants) sorts and renders correctly — infrastructure exists but checklist shows incomplete
- 2.3 Verify Step 6 (positioned descendants with
z-index: auto) renders in tree order within their stacking context - 2.4 Verify stable sort preserves tree order for elements with equal z-index values
- 2.5 Fix any deviations found in the audit
- 2.6 Golden tests for each Appendix E step: negative z-index, block flow behind positioned, floats between blocks and inlines, positioned-auto tree order
- 2.1 Audit
-
Task 3: Atomic painting of nested stacking contexts (AC: #3)
- 3.1 Verify that child stacking contexts paint atomically — all descendants within a stacking context paint together before the next stacking context at the same level
- 3.2 Test nested stacking contexts: parent z-index:1 with children at various z-indices should all paint within parent's z-order slot
- 3.3 Golden test with 3+ levels of nesting to confirm atomicity
-
Task 4: Edge cases and correctness (AC: #1, #2, #3)
- 4.1 Verify
position: relative+z-index: 0creates a stacking context (z-index is not auto) - 4.2 Verify
position: relative+z-index: autodoes NOT create a stacking context (per §9.9.1) - 4.3 Verify
position: static+z-indexvalue has no effect (z-index only applies to positioned elements per §9.5.1) - 4.4 Test interleaved positioned and non-positioned siblings — non-positioned paints at Step 3/4/5, positioned paints at Step 6/7
- 4.5 Unit tests for
creates_stacking_context()covering all position/z-index combinations
- 4.1 Verify
-
Task 5: Golden tests and checklist update (AC: #4)
- 5.1 Add golden test fixtures covering: nested stacking contexts with mixed z-indices, negative z-index behind parent background, positioned-auto tree order, interleaved positioned/non-positioned
- 5.2 Update
docs/CSS2.1_Implementation_Checklist.md— check off stacking context creation rules and z-index stacking contexts - 5.3 Run
just ciand ensure all tests pass
Dev Notes
Current Implementation Status
Stacking contexts and z-index are partially implemented. What works:
- Z-index parsing —
z-indexproperty parsed in CSS, stored asOption<i32>inComputedStylesandLayoutBox - Stacking context creation — positioned elements with explicit z-index create stacking contexts
- Positive z-index sorting (Step 7) — children sorted ascending and rendered
- Appendix E steps 1, 3, 4, 5 — background/borders, block flow, floats, inlines all render in order
- Existing golden tests — 9 golden tests already cover basic z-index scenarios (056, 057, 058, 177, 178, 179, 123, 192, 194)
What needs verification and fixing (this story's scope):
- Root element stacking context —
creates_stacking_context()does NOT handle root element - Negative z-index (Step 2) — infrastructure exists but checklist shows incomplete
- Positioned-auto (Step 6) — renders but tree-order correctness unverified
- Stable sort / tree order — equal z-index elements must paint in tree order
- Nested atomicity — needs verification that nested contexts paint atomically
Key Code Locations
| Component | File | Key Functions/Lines |
|---|---|---|
| Stacking context creation | crates/layout/src/types.rs:523-525 |
creates_stacking_context() — currently is_positioned() && z_index.is_some() |
| is_positioned() | crates/layout/src/types.rs:510-512 |
Returns position != Static |
| z_index field | crates/layout/src/types.rs:228 |
z_index: Option<i32> on LayoutBox |
| Paint order algorithm | crates/display_list/src/builder.rs:148-350 |
render_stacking_context() — CSS 2.1 Appendix E implementation |
| StackingParticipant | crates/display_list/src/builder.rs:13-20 |
Struct collecting positioned/stacking descendants |
| RenderPhase enum | crates/display_list/src/builder.rs:53-62 |
Controls phased rendering for Appendix E |
| Participant collection | crates/display_list/src/builder.rs:529-598 |
collect_stacking_participants() — recursive tree walk |
| Z-index sorting | crates/display_list/src/builder.rs:236-249 |
Stable sort of negative/positive children |
| Z-index computed style | crates/style/src/types/computed.rs:89 |
z_index: Option<i32> — None = auto |
| Positioned layout | crates/layout/src/engine/block/positioning.rs |
Absolute/fixed/relative layout calculations |
| Existing golden tests | tests/goldens/expected/056-*.txt, 057-*, 058-*, 177-*, 178-*, 179-* |
Basic z-index, negative, stacking, deep nesting, tree order, overflow clip |
Implementation Approach
Task 1 (Root stacking context):
Modify creates_stacking_context() in crates/layout/src/types.rs. Add a check: if this LayoutBox represents the root element (check node_id == NodeId(0) or parent is document), return true unconditionally. The root element always creates a stacking context per CSS 2.1 §9.9.1.
Task 2 (Appendix E audit):
Walk through render_stacking_context() in builder.rs line by line against the spec:
- Step 1 (L178-186): Background, borders — likely correct
- Step 2 (L251-257): Negative z-index — verify sorting is ascending (most negative first) and rendering is recursive
- Step 3 (L259-280): Block descendants BG/borders only — verify
RenderPhase::BackgroundsOnly - Step 4 (L282-302): Floats — verify they paint as mini stacking contexts
- Step 5 (L304-310): Inline content — verify inline formatting context rendering
- Step 6 (L312-332): Positioned-auto — verify tree-order rendering for
z-index: autopositioned elements - Step 7 (L334-340): Positive z-index — verify ascending sort
Key question: Does collect_stacking_participants() correctly distinguish between "creates stacking context" (goes into negative/positive buckets) vs "positioned-auto" (goes into auto bucket)?
Task 3 (Nested atomicity):
This should work by design — each stacking context calls render_stacking_context() recursively, which renders all its descendants before returning. Verify with a golden test showing a z-index:1 parent with z-index:999 child does NOT paint above a z-index:2 sibling.
Architecture Constraints
- Layer rule: Changes span
layout(Layer 1) anddisplay_list(Layer 1) — both at same layer, horizontal deps allowed - No unsafe: Both crates have
unsafe_code = "forbid" - Pipeline order: Display list building reads layout tree (after layout phase). Do not modify layout boxes during display list building.
- CSS Property Implementation Order: This story is primarily display_list (paint) with a small layout touch. Follow: verify parsing → verify style computation → verify layout propagation → fix display list painting → tests → docs.
Previous Story Intelligence (Story 1.1)
From Story 1.1 (Margin Collapsing):
- Golden test infrastructure is well-established: fixtures in
tests/goldens/fixtures/, expected intests/goldens/expected/ - Unit tests follow the pattern of constructing DOM + style manually, running layout, asserting positions
- CSS 2.1 checklist at
docs/CSS2.1_Implementation_Checklist.mdmust be updated just ciis the single validation gate
Testing Strategy
- Unit tests for
creates_stacking_context()— add tests incrates/layout/src/tests/or alongside existing positioning tests incrates/style/src/tests/positioning.rs - Golden tests — add new fixtures (check latest number in
tests/goldens/fixtures/). Key scenarios:- Nested stacking contexts with mixed positive/negative z-indices
- Root element as stacking context
- Positioned-auto elements in tree order
- Interleaved positioned + non-positioned siblings
- Atomicity test: child z-index:999 inside parent z-index:1 must NOT paint above sibling z-index:2
- Verify existing golden tests still pass — 9 existing z-index goldens should not regress
- Run
just ciat the end
CSS 2.1 Spec References
- §9.9.1 — Stacking context creation rules: positioned elements with z-index != auto, root element
- §E.2 — Painting order within a stacking context (Appendix E): 7-step algorithm
- §9.5.1 — z-index only applies to positioned elements
- §9.3 — Positioning schemes: static, relative, absolute, fixed
WPT Impact
Stacking context and z-index tests are spread across WPT css2-visuren and css2-zindex categories. Fixing root stacking context and verifying Appendix E compliance should improve WPT pass rates in these areas.
Project Structure Notes
- Changes primarily in
crates/display_list/src/builder.rs(paint order) andcrates/layout/src/types.rs(stacking context predicate) - No new crates or cross-layer changes
- New golden test fixtures go in
tests/goldens/fixtures/with next available number - Expected golden outputs go in
tests/goldens/expected/ - Checklist update in
docs/CSS2.1_Implementation_Checklist.md
References
- [Source: crates/display_list/src/builder.rs#render_stacking_context] — Core paint order algorithm (Appendix E)
- [Source: crates/display_list/src/builder.rs#collect_stacking_participants] — Stacking participant collection
- [Source: crates/layout/src/types.rs#creates_stacking_context] — Stacking context creation predicate
- [Source: crates/layout/src/types.rs#is_positioned] — Position check helper
- [Source: crates/style/src/types/computed.rs] — z_index computed style field
- [Source: tests/goldens/expected/056-z-index-basic.*] — Existing basic z-index golden tests
- [Source: tests/goldens/expected/058-stacking-context.*] — Existing stacking context golden tests
- [Source: tests/goldens/expected/177-z-index-deep-stacking.*] — Deep nesting golden tests
- [Source: docs/CSS2.1_Implementation_Checklist.md] — Implementation status checklist
- [Source: _bmad-output/planning-artifacts/architecture.md#CSS-Property-Implementation-Order] — CSS feature implementation checklist
Dev Agent Record
Agent Model Used
Claude Opus 4.6 (1M context)
Debug Log References
None required — implementation was straightforward with no blocking issues.
Completion Notes List
- Task 1: Added
is_root_elementcheck tocreates_stacking_context()incrates/layout/src/types.rs. The existingis_root_elementfield (from story 1.1) was leveraged instead of checking NodeId(0). - Task 2: Audit of
render_stacking_context()found two bugs: (1) z-index:0 stacking contexts were in the step-7 bucket instead of step-6, (2) descendant stacking contexts under positioned z-index:auto ancestors were trapped instead of participating in the parent stacking context. Both fixed by refactoringcollect_stacking_participantsinto three buckets (negative/zero/positive) with aStackingBucketsstruct, recursing into positioned-auto ancestors, and merging all step-6 participants by node_id for tree-order rendering. Golden tests 218 and 219 exercise both fixed scenarios. - Task 3: Verified atomic painting of nested stacking contexts. The recursive
render_stacking_context()design inherently ensures atomicity — all descendants render before returning. Confirmed with golden test 216 (z:999 child inside z:1 parent paints before z:2 sibling). - Task 4: Added 7 unit tests for
creates_stacking_context()covering all position/z-index combinations including root element, positioned+z-index, positioned+auto, static+z-index. - Task 5: Added 7 new golden tests (213-219), updated CSS2.1 checklist, registered 4 pre-existing margin collapse golden tests (209-212).
just cipasses with 0 failures.
Change Log
- 2026-03-13: Implemented stacking contexts and z-index compliance (Story 1.2). Root element now creates stacking context per CSS 2.1 §9.9.1. Fixed z-index:0 step-6 bucketing and descendant SC hoisting through positioned-auto ancestors. Added 7 unit tests and 7 golden tests.
- 2026-03-13: Code review fixes — updated stale comments (steps 2/7 → 2/6/7), added NodeId monotonicity invariant comment, updated
collect_stacking_participantsdoc comment for three-bucket design, clarifiedowns_stacking_contextrelationship to unconditional SC skip, added golden test 220 exercising all three buckets (negative+zero+positive) in one container.
File List
crates/layout/src/types.rs— Modifiedcreates_stacking_context()to include root element check; added 7 unit testscrates/display_list/src/builder.rs— AddedStackingBucketsstruct; refactoredcollect_stacking_participantsto three-bucket collection; fixed z-index:0 to step 6; recurse into positioned-auto ancestors; unified step-6 rendering with tree-order merge;render_stacking_contextskips SC collection for non-SC boxestests/goldens.rs— Added golden test functions for 209-219tests/goldens/fixtures/213-z-index-negative.html— New golden fixturetests/goldens/fixtures/214-z-index-positioned-auto-tree-order.html— New golden fixturetests/goldens/fixtures/215-z-index-interleaved-positioned.html— New golden fixturetests/goldens/fixtures/216-z-index-nested-atomicity.html— New golden fixturetests/goldens/fixtures/217-z-index-three-level-nesting.html— New golden fixturetests/goldens/fixtures/218-z-index-zero-step6.html— New golden fixture (z-index:0 at step 6)tests/goldens/fixtures/219-z-index-auto-ancestor-descendant-sc.html— New golden fixture (SC hoisting through auto ancestor)tests/goldens/expected/213-z-index-negative.{layout,dl}.txt— New golden expectedtests/goldens/expected/214-z-index-positioned-auto-tree-order.{layout,dl}.txt— New golden expectedtests/goldens/expected/215-z-index-interleaved-positioned.{layout,dl}.txt— New golden expectedtests/goldens/expected/216-z-index-nested-atomicity.{layout,dl}.txt— New golden expectedtests/goldens/expected/217-z-index-three-level-nesting.{layout,dl}.txt— New golden expectedtests/goldens/expected/218-z-index-zero-step6.{layout,dl}.txt— New golden expectedtests/goldens/expected/219-z-index-auto-ancestor-descendant-sc.{layout,dl}.txt— New golden expectedtests/goldens/expected/211-margin-collapse-nested-chain.layout.txt— Updated (stale NodeIds)docs/CSS2.1_Implementation_Checklist.md— Checked off stacking context and z-index itemstests/goldens/fixtures/220-z-index-three-buckets.html— New golden fixture (all three buckets)tests/goldens/expected/220-z-index-three-buckets.{layout,dl}.txt— New golden expected_bmad-output/implementation-artifacts/sprint-status.yaml— Updated story status