Add CssValue::Unset variant with proper cascade semantics: acts as inherit for inherited properties, initial for non-inherited. Fix unset/inherit/initial handling in font, list-style, and text-decoration shorthand expansion. Add unset handling to both pseudo-element code paths. Promote WPT test wpt-css-css2-normal-flow-inlines-002 (now passes with font shorthand fix). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
21 KiB
Story 1.13: CSS Inheritance Completeness
Status: done
Story
As a web user, I want CSS property values to correctly inherit, reset, and cascade, So that styling behaves predictably across nested elements.
Acceptance Criteria
-
Given an element with a property set to
inheritWhen the page is rendered Then the element uses its parent's computed value for that property per CSS 2.1 §6.2.1 -
Given an element with a property set to
initialWhen the page is rendered Then the element uses the property's initial value per its specification -
Given an element with a property set to
unsetWhen the page is rendered Then the property behaves asinheritif it's an inherited property, orinitialif it's not -
Given all CSS 2.1 properties When checking inheritance behavior Then each property correctly defaults to inherited or non-inherited per its CSS 2.1 specification
-
Golden tests cover inherit/initial/unset for both inherited and non-inherited properties, checklist is updated, and
just cipasses.
Tasks / Subtasks
-
Task 1: Add
CssValue::Unsetvariant and proper handling (AC: #3)- 1.1 Add
Unsetvariant toCssValueenum incrates/css/src/types.rs(~line 199) - 1.2 Add parser recognition: in
crates/css/src/parser/mod.rs(~line 1161,parse_generic_valueand ~line 1296,parse_single_value), add"unset" => Some(CssValue::Unset) - 1.3 Remove incorrect
"unset" | "revert" => Some(CssValue::Inherit)workarounds incrates/css/src/parser/shorthands/background.rs(~line 47) andtypography.rs - 1.4 In
crates/style/src/context.rs(~line 587), addCssValue::Unsethandling: checkPropertyId::is_inherited()— if inherited, behave likeinherit(copy from parent); if not inherited, behave likeinitial(reset to initial value) - 1.5 In
crates/style/src/types/computed.rsapply_declaration()(~line 500), add early return forCssValue::Unsetthat delegates to the cascade engine (or handle inline if parent context is available) - 1.6 Add unit tests:
unseton inherited property (e.g.,color: unset) behaves likeinherit;unseton non-inherited property (e.g.,margin: unset) behaves likeinitial
- 1.1 Add
-
Task 2: Fix
text-indentauto-inheritance (AC: #4)- 2.1 Add
PropertyId::TextIndenttois_inherited()match incrates/css/src/types.rs(~line 778) —text-indentis inherited per CSS 2.1 §16.1 - 2.2 Add
text_indenttoinherit_from()incrates/style/src/types/computed.rs(~line 1864) — currently missing despite being supported incopy_property_from_parent() - 2.3 Add unit test: child element inherits
text-indentfrom parent without explicitinheritkeyword
- 2.1 Add
-
Task 3: Audit and complete CSS 2.1 inherited property list (AC: #4)
- 3.1 Cross-reference CSS 2.1 spec against
is_inherited()incrates/css/src/types.rs(~line 778). The complete list of CSS 2.1 inherited properties is:azimuth,border-collapse,border-spacing,caption-side,color,cursor,direction,elevation,empty-cells,font-family,font-size,font-style,font-variant,font-weight,font,letter-spacing,line-height,list-style-image,list-style-position,list-style-type,list-style,orphans,pitch-range,pitch,quotes,richness,speak-header,speak-numeral,speak-punctuation,speak,speech-rate,stress,text-align,text-indent,text-transform,visibility,voice-family,volume,white-space,widows,word-spacing
- 3.2 For properties already in ComputedStyles but missing from
is_inherited(): add them (e.g.,TextIndent) - 3.3 For properties NOT yet in ComputedStyles (e.g.,
word-spacing,cursor,direction,font-variant,empty-cells,caption-side,list-style-position,list-style-image,quotes,orphans,widows): skip aural/speech properties (out of scope), but note which visual properties are missing for future stories - 3.4 Ensure
inherit_from()andis_inherited()are kept in sync — every property inis_inherited()must be copied ininherit_from()
- 3.1 Cross-reference CSS 2.1 spec against
-
Task 4: Verify
inheritkeyword works for all properties (AC: #1)- 4.1 Verify
copy_property_from_parent()incrates/style/src/types/computed.rs(~line 1636) covers all PropertyId variants currently in ComputedStyles - 4.2 Scan for any PropertyId variants missing from the match statement — these would silently fail when
inheritis used - 4.3 Add any missing property arms to
copy_property_from_parent() - 4.4 Add unit tests for
inheriton non-inherited properties (e.g.,border: inherit,display: inherit)
- 4.1 Verify
-
Task 5: Verify
initialkeyword works for all properties (AC: #2)- 5.1 Verify
reset_to_initial()incrates/style/src/types/computed.rs(~line 1512) covers all PropertyId variants - 5.2 Cross-check initial values against CSS 2.1 spec — verify
css_initial_values()(~line 1758) returns correct values - 5.3 Add any missing property arms to
reset_to_initial() - 5.4 Add unit test for
initialon inherited properties (e.g.,color: initial→ black,font-size: initial→ 16px)
- 5.1 Verify
-
Task 6: Golden tests and documentation (AC: #5)
- 6.1 Create golden test:
inheritkeyword on inherited property (e.g., nested div withcolor: inherit) - 6.2 Create golden test:
inheritkeyword on non-inherited property (e.g.,border: inherit) - 6.3 Create golden test:
initialkeyword resetting inherited property (e.g.,color: initialoverrides parent color) - 6.4 Create golden test:
unseton inherited vs non-inherited properties - 6.5 Create golden test: auto-inheritance of text properties (text-indent, letter-spacing flowing to children without explicit inherit)
- 6.6 Update
docs/CSS2.1_Implementation_Checklist.md— correct status entries for inheritance items and mark as complete - 6.7 Run
just ci 2>&1 | tee /tmp/ci-output.txtand fix any issues
- 6.1 Create golden test:
Dev Notes
CSS 2.1 Spec References
- §6.2 Value processing: specified → computed → used → actual values
- §6.2.1 Specified values: if cascade yields
inherit, use parent's computed value. If cascade yields no value, use inherited value (for inherited properties) or initial value (for non-inherited). - §6.2.4
inheritkeyword: forces inheritance for any property - CSS3 Cascading §3.6
unsetkeyword: acts asinheritfor inherited properties,initialfor non-inherited - CSS3 Cascading §3.5
initialkeyword: resets property to its spec-defined initial value
Current Inheritance Pipeline State
Already working (DO NOT reimplement):
CssValue::InheritandCssValue::Initialenum variants incrates/css/src/types.rs(~line 207-209)- Parser recognition of
"inherit"and"initial"keywords incrates/css/src/parser/mod.rs(~line 1161, 1296) - Cascade-level
inherithandling incrates/style/src/context.rs(~line 587) — copies from parent viacopy_property_from_parent() apply_declaration()early return forCssValue::Initial→ callsreset_to_initial()incrates/style/src/types/computed.rs(~line 500)reset_to_initial()(~line 1512) — resets any property to its CSS 2.1 initial valuecss_initial_values()(~line 1758) — complete struct with all spec-correct initial valuescopy_property_from_parent()(~line 1636) — handles ~50 properties for explicitinheritinherit_from()(~line 1864) — auto-inherits 15 properties for normal cascadePropertyId::is_inherited()(~line 778) — 15 properties marked as inherited!importanthandling with correct cascade weight orderingCascadeOrigin::UserAgentandCascadeOrigin::Authorwith proper priority
Must be added:
CssValue::Unsetvariant + parser recognition + cascade handlingtext-indentinis_inherited()andinherit_from()- Audit of all CSS 2.1 inherited properties
- Remove incorrect
"unset" => CssValue::Inheritworkarounds in shorthands - Golden tests for inherit/initial/unset
Key Code Locations
| Component | File | What to modify |
|---|---|---|
| CssValue enum | crates/css/src/types.rs (~line 199) |
Add Unset variant |
| is_inherited() | crates/css/src/types.rs (~line 778) |
Add TextIndent, audit list |
| Parser keywords | crates/css/src/parser/mod.rs (~line 1161, 1296) |
Add "unset" recognition |
| Background shorthand | crates/css/src/parser/shorthands/background.rs (~line 47) |
Remove "unset" => Inherit workaround |
| Typography shorthand | crates/css/src/parser/shorthands/typography.rs (~line 73) |
Remove "unset" workaround |
| Cascade inherit handler | crates/style/src/context.rs (~line 587) |
Add CssValue::Unset handling |
| apply_declaration | crates/style/src/types/computed.rs (~line 500) |
Add Unset early return |
| reset_to_initial | crates/style/src/types/computed.rs (~line 1512) |
Verify all properties covered |
| copy_property_from_parent | crates/style/src/types/computed.rs (~line 1636) |
Verify all properties covered |
| inherit_from | crates/style/src/types/computed.rs (~line 1864) |
Add text_indent |
| css_initial_values | crates/style/src/types/computed.rs (~line 1758) |
Verify correctness |
| Golden fixtures | tests/goldens/fixtures/ |
New inherit/initial/unset fixtures |
| Checklist | docs/CSS2.1_Implementation_Checklist.md (~line 65) |
Update inheritance items |
Architecture Constraints
- No unsafe code — all changes in Layer 1 crates only (css, style)
- Cascade engine design —
inheritandunsetmust be handled in the cascade engine (context.rs) because that's where parent context is available.apply_declaration()incomputed.rshandlesinitialbecause it doesn't need parent context. - Phase-based pipeline — inheritance runs during style computation phase only
Implementation Pattern (Follow Exactly)
- CssValue in
css/types.rs— addUnsetvariant - Parser in
css/parser/mod.rs— add"unset"keyword recognition - Remove workarounds in
css/parser/shorthands/— delete incorrect"unset" => Inheritmappings - is_inherited() in
css/types.rs— addTextIndent, audit full list - inherit_from() in
style/types/computed.rs— addtext_indent, keep in sync withis_inherited() - Cascade engine in
style/context.rs— addCssValue::Unsethandling (checkis_inherited(), delegate to inherit or initial) - Verify
copy_property_from_parent()andreset_to_initial()cover all properties - Golden tests in
tests/goldens/— inherit/initial/unset fixtures - Checklist update —
docs/CSS2.1_Implementation_Checklist.md - CI validation —
just ci 2>&1 | tee /tmp/ci-output.txt
unset Implementation Strategy
The unset keyword semantics (CSS Cascading §3.6):
- If the property is an inherited property → behave like
inherit(copy from parent) - If the property is a non-inherited property → behave like
initial(reset to initial value)
Implementation in context.rs cascade handling:
if matches!(m.declaration.value, CssValue::Unset) {
if m.declaration.property.is_inherited() {
// Behave like inherit
if let Some(parent_id) = doc.parent(node_id) {
if let Some(parent) = styles.get(&parent_id) {
computed.copy_property_from_parent(&m.declaration.property, parent);
}
}
} else {
// Behave like initial
computed.reset_to_initial(&m.declaration.property);
}
continue;
}
CSS 2.1 Inherited Properties Reference
Complete list of CSS 2.1 visual inherited properties (excluding aural/speech):
| Property | In is_inherited()? | In inherit_from()? | In ComputedStyles? |
|---|---|---|---|
color |
✅ | ✅ | ✅ |
font-family |
✅ | ✅ | ✅ |
font-size |
✅ | ✅ | ✅ |
font-style |
✅ | ✅ | ✅ |
font-weight |
✅ | ✅ | ✅ |
font-variant |
❌ | ❌ | ❌ (Story 1-10 scope) |
letter-spacing |
✅ | ✅ | ✅ |
line-height |
✅ | ✅ | ✅ |
list-style-type |
✅ | ✅ | ✅ |
list-style-position |
❌ | ❌ | ❌ |
list-style-image |
❌ | ❌ | ❌ |
text-align |
✅ | ✅ | ✅ |
text-indent |
❌ MISSING | ❌ MISSING | ✅ |
text-transform |
✅ | ✅ | ✅ |
visibility |
✅ | ✅ | ✅ |
white-space |
✅ | ✅ | ✅ |
word-spacing |
❌ | ❌ | ❌ |
border-collapse |
✅ | ✅ | ✅ |
border-spacing |
✅ | ✅ | ✅ |
caption-side |
❌ | ❌ | ❌ |
empty-cells |
❌ | ❌ | ❌ |
cursor |
❌ | ❌ | ❌ |
direction |
❌ | ❌ | ❌ |
quotes |
❌ | ❌ | ❌ |
orphans |
❌ | ❌ | ❌ |
widows |
❌ | ❌ | ❌ |
Action for this story:
- Fix
text-indent(exists in ComputedStyles but missing from inheritance lists) - For properties not in ComputedStyles at all: document as future work, don't add empty fields
font-variantis being added in Story 1-10 — if already merged, add to inheritance lists
Previous Story Learnings
From stories 1-11 (Overflow) and 1-12 (Media Rules):
- Type changes in
crates/css/src/types.rsmust not break any existing match statements — adding an enum variant requires updating allmatchexpressions - Golden test fixtures:
tests/goldens/fixtures/NNN-descriptive-name.html(sequential numbering) - Regen goldens:
cargo test -p rust_browser --test regen_goldens -- --nocapture just cimust pass clean at the end
Git Intelligence
Recent relevant commits:
7c8d2b8— :link/:visited pseudo-class (style computation pattern)- Previous milestone work established the inheritance infrastructure
Testing Strategy
- Unit tests for
CssValue::Unsetparsing (CSS parser tests) - Unit tests for
unseton inherited property → inherits from parent - Unit tests for
unseton non-inherited property → resets to initial - Unit tests for
text-indentauto-inheritance - Unit tests for
inheriton non-inherited properties (e.g.,display: inherit) - Unit tests for
initialon inherited properties (e.g.,color: initial) - Golden tests — minimum 4 fixtures:
inheritkeyword on inherited + non-inherited propertiesinitialkeyword overriding inherited valueunseton inherited property (acts as inherit)unseton non-inherited property (acts as initial)
- Regression — all existing style computation tests pass unchanged
Scope Boundaries
In scope:
CssValue::Unsetvariant + proper handling- Fix
text-indentauto-inheritance - Audit
is_inherited()/inherit_from()for existing properties - Verify
copy_property_from_parent()/reset_to_initial()completeness - Golden tests, checklist updates
Out of scope (future stories):
revertkeyword (CSS Cascading Level 4 — not CSS 2.1)allshorthand property (CSS3 — not CSS 2.1)- Adding new properties to ComputedStyles (e.g.,
word-spacing,cursor,direction) - User stylesheet origin
Project Structure Notes
- All changes span Layer 1 crates only —
cssandstyle - No new external dependencies needed
- Adding
CssValue::Unsetwill require updating allmatcharms onCssValue— search for existing matches and add the new variant
References
- [Source: CSS 2.1 §6.2 — Value processing (specified/computed/used/actual)]
- [Source: CSS 2.1 §6.2.1 — Specified values and inheritance]
- [Source: CSS 2.1 §6.2.4 — inherit keyword]
- [Source: CSS Cascading Level 3 §3.5-§3.6 — initial and unset keywords]
- [Source: crates/css/src/types.rs — CssValue enum (~line 199), is_inherited() (~line 778)]
- [Source: crates/css/src/parser/mod.rs — keyword parsing (~line 1161, 1296)]
- [Source: crates/style/src/context.rs — cascade inherit handling (~line 587)]
- [Source: crates/style/src/types/computed.rs — apply_declaration (~line 500), reset_to_initial (~line 1512), copy_property_from_parent (~line 1636), inherit_from (~line 1864)]
- [Source: crates/css/src/parser/shorthands/background.rs — incorrect unset workaround (~line 47)]
- [Source: docs/CSS2.1_Implementation_Checklist.md — inheritance status (~line 65)]
- [Source: _bmad-output/project-context.md — Full project rules and patterns]
Dev Agent Record
Agent Model Used
Claude Opus 4.6 (1M context)
Debug Log References
None — implementation was straightforward.
Completion Notes List
- Task 1 (CssValue::Unset): Added
Unsetvariant toCssValueenum, parser recognition invalues.rs,property_dispatch.rs, andcontent.rs. Removed incorrect"unset" => CssValue::Inheritworkaround inbackground.rs. AddedUnsethandling in cascade engine (context.rs) usingis_inherited()to decide between inherit/initial semantics. Added early return inapply_declaration(). - Tasks 2-3 (text-indent & property audit): Already complete in the codebase —
text-indent,word-spacing,direction,font-variant,list-style-position,list-style-image,caption-side,empty-cellsall present inis_inherited()andinherit_from(). No changes needed. - Task 4 (inherit keyword): Verified
copy_property_from_parent()covers all PropertyId variants. No gaps found. - Task 5 (initial keyword): Verified
reset_to_initial()covers all PropertyId variants. Made methodpub(crate)for use byunsethandling incontext.rs. - Task 6 (golden tests & docs): Created 5 golden test fixtures (270-274) covering inherit/initial/unset/auto-inheritance. Updated CSS2.1 Implementation Checklist.
just cipasses.
Change Log
- 2026-03-14: Implemented CSS inheritance completeness — added
CssValue::Unset, verified inherit/initial/auto-inheritance coverage, 12 unit tests + 5 golden tests added. - 2026-03-14: Code review fixes — added
CssValue::Unsethandling to both pseudo-element code paths incontext.rs; fixedfont: unset,list-style: unset, andtext-decoration: unsetshorthand expansion; promoted WPT testwpt-css-css2-normal-flow-inlines-002(now passes due tofont: inheritfix); added 5 shorthand expansion tests.
File List
crates/css/src/types.rs— AddedCssValue::Unsetvariantcrates/css/src/parser/values.rs— Added"unset"keyword recognition in all value parserscrates/css/src/parser/property_dispatch.rs— Added"unset"keyword recognition in 3 parse functionscrates/css/src/parser/content.rs— Added"unset"to CSS-wide keyword checkcrates/css/src/parser/shorthands/background.rs— Fixed"unset"to useCssValue::Unset(was incorrectly mapped toCssValue::Inherit)crates/style/src/context.rs— AddedCssValue::Unsetcascade handling (inherit for inherited props, initial for non-inherited)crates/style/src/types/computed.rs— AddedCssValue::Unsetearly return inapply_declaration(), madereset_to_initialpub(crate)crates/style/src/tests/mod.rs— Addedinheritancetest modulecrates/style/src/tests/inheritance.rs— New: 12 unit tests for unset/inherit/initial/auto-inheritancetests/goldens/fixtures/270-inherit-inherited-property.html— New golden fixturetests/goldens/fixtures/271-inherit-non-inherited-property.html— New golden fixturetests/goldens/fixtures/272-initial-overrides-inherited.html— New golden fixturetests/goldens/fixtures/273-unset-inherited-vs-non-inherited.html— New golden fixturetests/goldens/fixtures/274-auto-inheritance-text-properties.html— New golden fixturetests/goldens/expected/270-*.txt— Generated expected outputs (layout + display list)tests/goldens/expected/271-*.txt— Generated expected outputstests/goldens/expected/272-*.txt— Generated expected outputstests/goldens/expected/273-*.txt— Generated expected outputstests/goldens/expected/274-*.txt— Generated expected outputsdocs/CSS2.1_Implementation_Checklist.md— Updated inheritance items to completecrates/css/src/parser/shorthands/typography.rs— Fixedfont: unset,list-style: unset,text-decoration: unsetshorthand expansiontests/external/wpt/wpt_manifest.toml— Promotedwpt-css-css2-normal-flow-inlines-002from known_fail to pass_bmad-output/implementation-artifacts/sprint-status.yaml— Status: in-progress → done
Senior Developer Review (AI)
Review Date: 2026-03-14 Reviewer: Claude Opus 4.6 (code review workflow) Outcome: Approve (after fixes)
Findings Summary
| # | Severity | Description | Status |
|---|---|---|---|
| H1 | HIGH | CssValue::Unset not handled in pseudo-element styling (2 code paths) |
Fixed |
| M1 | MEDIUM | list-style: unset shorthand not handled |
Fixed |
| M2 | MEDIUM | text-decoration: unset shorthand silently drops |
Fixed |
| M3 | MEDIUM | font: unset silently drops (also fixed font: inherit/initial) |
Fixed |
| L1 | LOW | No test for unset through shorthand expansion |
Fixed |
Action Items
- H1: Add
CssValue::Unsethandling to bothcompute_pseudo_stylescode paths incontext.rs - M1: Add CSS-wide keyword handling (
inherit/initial/unset) tolist-styleshorthand - M2: Add CSS-wide keyword handling to
text-decorationshorthand - M3: Expand
font: inherit/initial/unsetto all 6 sub-properties instead of returning empty - L1: Add 5 shorthand expansion tests for
unset