Skip to content

[css-lists] CSS counter scope/inheritance is incompatible with HTML ordinals #5477

Closed

Description

@MatsPalmgren

The rules for counter scope/inheritance in CSS Lists is incompatible with HTML <ol><ul><li> ordinals and is thus not web-compatible.
https://drafts.csswg.org/css-lists/#nested-counters
https://drafts.csswg.org/css-lists/#inheriting-counters

Gecko has implemented HTML <ol><ul><li> ordinals using the built-in list-item counter as per the CSS Lists spec. However, we've got many reports that this breaks existing web content to the extent that we need to ship a (non spec-compliant) fix.
Consider this simple HTML example:

<style>
  li::before { content: "(list-item:" counters(list-item,'.') ")"; }
</style>
<ol>
  <li></li>
  <li></li>
  <ol>
    <li></li>
  </ol>
  <li></li>
</ol>

According to CSS Lists, the correct rendering is:
image
This is clearly not how HTML lists should work - the last item should have the ordinal 3.
Granted, the above HTML is invalid - <ol><ul> aren't allowed as a direct child of <ol><ul>, but this kind of bogus markup is rather common on the web unfortunately. It's also the DOM that execCommand('indent') produce (sigh), which affects HTML editors. So telling authors to fix their markup isn't a tenable solution.

The alternative fixes we considered were:

  1. change CSS Lists to make all CSS counters behave differently for this case
    Con: breaks existing (author-specified) nested CSS counters of the same name
  2. change the behavior for the built-in list-item counter only
    Con: makes the built-in list-item and other CSS counters behave differently which is a more complicated model to implement, specify, and teach to authors
  3. extend CSS Lists to allow either behavior
    (e.g. something like ol, ul, menu { counter-reset: strict-scope(list-item); } in the UA sheet)
    Con: a more complicated model to implement, specify, and teach to authors

We concluded that 1 is the least bad solution, so we propose to include a new step in finding the counter scope that searches an element's ancestors first, before falling back to the current steps in the spec that searches previous siblings in reverse DOM order.
This also makes author-defined counters behave more sensible - consider the analogous example:

<style>
  list, item { display: block; }
  list { counter-reset: foo; margin-left: 40px; }
  item { counter-increment: foo; }
  item::before { content: counters(foo,'.'); }
</style>
<list>
  <item></item>
  <item></item>
  <list>
    <item></item>
  </list>
  <item></item>
</list>

which currently renders as:
image
to instead render as:
image
which makes more sense IMO.

CC @fantasai @emilio

Activity

  1. faceless2 commented on Aug 27, 2020

    @faceless2

    Slightly nervous about this one. Can I propose a fourth option which might be lower impact?

    1. Change the counter rules so that counter-reset is applied after counter-increment and counter-set (it's currently applied before). Then you can cater for the invalid markup with this in a UA sheet.
    ol > ol { counter-increment: list-item; counter-reset: list-item }

    The outer-scoped list-item will be incremented, then a new scope is created with the counter value of 1.

    The only impact that would have is on elements that have both a counter-reset and counter-increment - a pointless combination as the rules are currently defined (and also one that can be searched for to see how often its used). It feels like this approach might have less unintended side-effects (and be easier to explain too).

  2. Loirooriol commented on Sep 1, 2020

    @Loirooriol
    Contributor

    The outer-scoped list-item will be incremented, then a new scope is created with the counter value of 1.

    @faceless2 If counter-reset: list-item is applied afterwards, then the inner scope will have value 0. And if counter inheritance prefers the previous sibling over the parent, the next element will still get the value from the inner scope? I don't get how your proposal solves the problem.

  3. faceless2 commented on Sep 2, 2020

    @faceless2

    I've just worked through it again. I'm wrong and you're correct. Scratch that idea, sorry!

  4. added this to the TPAC-2020-08-22 milestone on Oct 16, 2020
  5. css-meeting-bot commented on Oct 22, 2020

    @css-meeting-bot
    Member

    The CSS Working Group just discussed [css-lists] CSS counter scope/inheritance is incompatible with HTML ordinals, and agreed to the following:

    • RESOLVED: change CSS Lists to make all CSS counters behave differently for this case
    The full IRC log of that discussion <emilio> Topic: [css-lists] CSS counter scope/inheritance is incompatible with HTML ordinals
    <emilio> github: https://github.com//issues/5477
    <emilio> TabAtkins: when I was writing the counter rules I was working from css 2.1 and my testing
    <emilio> ... I knew HTML <ol> didn't match CSS counters in invalid markup
    <emilio> ... Gecko tried to switch to counters for CSS lists
    <emilio> ... and found out that they were not web compatible
    <emilio> ... see the example in the issue with a nested <ol> directly under another
    <emilio> ... html renders that as you'd probably expect
    <emilio> ... that's also what editors generate for some reason
    <emilio> ... In order to fix it we can specify that either html is magic
    <emilio> ... that the list-item counter is magic
    <emilio> ... or switch counter behavior
    <emilio> ... mats is going for the later
    <Rossen_> q
    <emilio> ... and is proposing to do an ancestor-only check and only if that fails we look for a previous sibling counter
    <emilio> ... that's probably fine and would still work with the main sibling use case which is naming headers
    <emilio> ... so I'm ok with making that change
    <emilio> Rossen_: you're referring to the third approach in the comment?
    <emilio> TabAtkins: no, number 1
    <dbaron> seems like a reasonable way to fix this without breaking use of counter numbering for headers
    <emilio> fantasai: if you look at the example and replace list with section and item with h1, it will end up giving a better result
    <emilio> ... I'm in favor of this change
    <Rossen_> ack fantasai
    <emilio> ... if we can pull it of
    <emilio> off
    <emilio> q+
    <astearns> and you’re OK with this, faceless2?
    <fantasai> emilio: We actually have made this change, unsure if released yet
    <fantasai> emilio: but haven't heard of any breakage
    <fantasai> Rossen_: cool
    <faceless2> yes. everything I had to say on the topic before was wrong :-)
    <emilio> Rossen_: objections?
    <fantasai> strong +1 given emilio's report :)
    <emilio> RESOLVED: change CSS Lists to make all CSS counters behave differently for this case
    <emilio> [... discussion about republishing CSS2 ...]
    <oriol> The change is in Firefox 82, which seems it was released on 2020-10-20 (but I still don't have it)
  6. 29 remaining items

  7. Loirooriol commented on Aug 1, 2024

    @Loirooriol
    Contributor

    That's an HTML concept that seems only relevant for non-CSS UAs. CSS UAs need to obey https://drafts.csswg.org/css-lists/#content-property, so in this case the text comes from the list-item counter. And as per https://html.spec.whatwg.org/multipage/rendering.html#lists, only <ol>, <ul> and <menu> default to counter-reset: list-item.

  8. zp1996 commented on Aug 4, 2024

    @zp1996
    Member

    @Loirooriol This test case is failed on Firefox. Is Firefox's behavior correct based on the above description?

    <style>
      li {
        list-style-type: decimal;
        list-style-position: inside;
      }
    
      .container {
        padding: 50px;
      }
    </style>
    <div class="container">
      <li>A</li>
      <li>B</li>
      <div>
        <li>C</li>
        <span>
          <li>D</li>
        </span>
      </div>
      <blockquote>
        <li>E</li>
      </blockquote>
      <li>F</li>
    </div>

    Chromium/Webkit:
    image
    Firefox:
    image

  9. Loirooriol commented on Aug 5, 2024

    @Loirooriol
    Contributor

    Yes, I would again say that Firefox seems right since that's how CSS counters behave, and Blink agrees with ::marker { content: counter(list-item) ". " }.

  10. zp1996 commented on Aug 6, 2024

    @zp1996
    Member

    @Loirooriol Is the behavior of this test correct in Firefox?

    <style>
      li {
        list-style-type: decimal;
      }
    </style>
    <div>
      <li></li>
      <li></li>
    </div>
    <div>
      <li></li>
      <li></li>
    </div>

    actual result:
    image
    IMO, expected result:
    image

  11. Loirooriol commented on Aug 6, 2024

    @Loirooriol
    Contributor

    Yes, Firefox is correct. The 1st <li> instantiates the list-item counter, which is propagated to descendants, following siblings, and descendants of following siblings. It's not propagated to following uncles, so the 3rd <li> creates a new instance. If you want all the items to share the same instance of the counter, you can use body { counter-reset: list-item } (or a <ol> wrapper)

  12. yisibl commented on Aug 16, 2024

    @yisibl
    Contributor
  13. MurakamiShinyu commented on Aug 17, 2024

    @MurakamiShinyu

    Is this the same issue? https://issues.chromium.org/issues/359623686

    I think so. I encountered this problem when testing a stylesheet that uses counters() for nested section numbering, as shown in the following example:

    <style>
      section {
        counter-increment: section;
        margin-inline-start: 1em;
      }
      section:first-of-type {
        counter-reset: section;
      }
      section > :is(h1,h2,h3,h4,h5,h6):first-child::before {
        content: counters(section, ".") " ";
      }
    </style>
    <section>
      <h1>Section Level 1</h1>
      <section>
        <h2>Section Level 2</h2>
      </section>
      <section>
        <h2>Section Level 2, 2nd</h2>
        <section>
          <h3>Section Level 3</h3>
        </section>
        <section>
          <h3>Section Level 3, 2nd</h3>
        </section>
        <section>
          <h3>Section Level 3, 3rd</h3>
        </section>
      </section>
      <section>
        <h2>Section Level 2, 3rd</h2>
      </section>
    </section>
    <section>
      <h1>Section Level 1, 2nd</h1>
    </section>

    (CodePen: https://codepen.io/MurakamiShinyu/pen/KKjZVbR )

    I was surprised that the latest Chrome (126 and later) does not work as expected, while Firefox and Safari work as expected, and I thought it is a bug in Chromium. Then I found this CSS issue.

    I believe that reverting the spec change is the right decision, and I hope that all browsers will behave consistently.

  14. zp1996 commented on Sep 19, 2024

    @zp1996
    Member

    According to recent changes around counter inheritance in the spec:

    Changed counter inheritance to take from the parent first, and only take from the sibling if it’s a new counter.

    But Example 22 in the spec conflicts with the spec. In the example, the last h2's counter does not inherit from the parent element, so the last h2 render like this:

    B.1 First H2 in H1
    

    A comparable issue: #10491

    IMO, we need to update the spec to clarify the behavior.

  15. Loirooriol commented on Sep 19, 2024

    @Loirooriol
    Contributor

    According to recent changes around counter inheritance in the spec

    These changes need to be reverted as resolved in #5477 (comment)

  16. zp1996 commented on Sep 20, 2024

    @zp1996
    Member

    As we have reverted the spec, so the test results of Firefox and Safari are correct?

    <style type="text/css">
    #test { counter-reset: c; }
    #test span { counter-increment: c; }
    #test span::before { content: counter(c, decimal-leading-zero); }
    </style>
    <p>The following two lines should look the same:</p>
    <div id="test">
      <span></span>
      <span></span>
      <span></span>
      <span></span>
      <span></span>
      <span></span>
      <span></span>
      <span></span>
      <span></span>
      <span></span>
      <span></span>
      <span></span>
      <span style="counter-reset: c 98"></span>
      <span></span>
      <span></span>
    </div>

    Chrome inherits from the parent element:

    01 02 03 04 05 06 07 08 09 10 11 12 99 13 14
    

    Firefox and Safari inherit from the sibling element:

    01 02 03 04 05 06 07 08 09 10 11 12 99 100 101
    
  17. Loirooriol commented on Sep 21, 2024

    @Loirooriol
    Contributor

    Yes, but Firefox seems to have a special behavior for list-item (which is odd given that Mats argued against "change the behavior for the built-in list-item counter only")

  18. emilio commented on Sep 21, 2024

    @emilio
    Collaborator

    Yeah, I changed that in https://bugzilla.mozilla.org/show_bug.cgi?id=1885842 because we kept getting issues like that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions