Skip to content

fix: resolve CSS counters in document order with proper scoping - #673

Merged
cossssmin merged 1 commit into
masterfrom
fix/css-counters
Sep 24, 2026
Merged

cossssmin merged 1 commit into
masterfrom
fix/css-counters

Conversation

@cossssmin

@cossssmin cossssmin commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #480

With inlinePseudoElements, counters used in content were computed rule by rule instead of in document order, and a counter was only visible to the element's descendants (via a per-element prototype chain), not its following siblings. So any interleaved numbering came out wrong, like headings where each h1 resets the h2 counter:

#nice { counter-reset: counterh1; }
#nice h1 { counter-reset: counterh2; }
#nice h1:before { counter-increment: counterh1; content: 'Part'counter(counterh1); }
#nice h2:before { counter-increment: counterh2; content: counter(counterh1)"."counter(counterh2); }
Output
Chrome Part1, 1.1, 1.2, Part2, 2.1, 2.2
Before Part1, 0.counter(counterh2), 0.counter(counterh2), Part2, 0.counter(counterh2), 0.counter(counterh2)
After Part1, 1.1, 1.2, Part2, 2.1, 2.2

How it works now

Counters are resolved in a single pass over the document after all rules are applied, following CSS Lists 3:

  • an element inherits counters from its parent and preceding sibling, with values from the element right before it in document order
  • then counter-reset, counter-increment and counter-set are applied, using the declarations that win the cascade (before, every matching rule's counter-* was applied, even overridden ones)
  • ::before and ::after count as the element's first and last child
  • a counter that's incremented or used without a reset is created at 0, like browsers do (before, counter(x) was left as literal text)

The pass only runs with inlinePseudoElements on and when the CSS uses counters.

Tests

Expected values come from what Chrome renders for the same CSS and HTML: the issue's headings, the reporter's exact CSS (where counterh1 is only incremented on h1:before, so it's scoped to that pseudo-element and the h2s see 0, same as in Chrome), a parent reset shadowing sibling resets, nested lists, a counter that's never reset, counter-set, and the cascade winner. The existing counter tests and the pseudo-elements fixture pass unchanged.

Performance

No difference for documents without counters. For a counter-heavy document (50 chapters × 6 numbered headings + paragraphs, ~650 elements), 4.23ms → 4.83ms, since the pass copies the active counters at each element.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@cossssmin
cossssmin merged commit c823e9f into master Sep 24, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

关于counter-reset无法多次重置的问题

1 participant