Skip to content

[selectors-4] Grid-structural selectors like :nth-col(): having pseudo-classes for rows e.g. :nth-row() is needed for CSS to cope with tables and rowSpan #8352

@DavidJCobb

Description

@DavidJCobb

The current Selectors 4 draft introduces the :nth-col() and :nth-last-col() selectors, as a way to select grid elements (including table cells) that exist within a given column. However, no similar pseudo-classes exist for rows. The draft justifies this on the grounds that tables and similar grid structures are typically row-major; however, that doesn't account for things like the rowSpan attribute on table cells.

Consider the following table:

<table>
   <tbody>
      <tr>
         <td rowSpan="2"> A </td>
         <td> B </td>
      </tr>
      <tr>
         <td> C </td>
      </tr>
   </tbody>
</table>
<!--
   layout:
   A B
   A C
-->

In this scenario, there is no way to tell that the "A" cell extends to the bottom of the table. Tables are row-major, but we can only tell what row "A" starts in; there's no way to tell what row it ends in. This causes a few problems:

  • Setting a border-radius on a table will round off the table's corners, but not the corners of its corner cells; the cells remain rectangular and are drawn overtop the table's rounded corners. To give a table proper rounded corners, we must set border-top-left-radius and friends on the top-left, top-right, bottom-left, and bottom-right cells. However, we can't reliably identify the bottom-left cell in the above table using selectors alone.
  • Setting border-collapse on a table will prevent you from using border-radius, and may additionally prevent other effects that rely on separated borders to work. In order to work around this, you have to leave the borders separated, and then emulate border-collapse by selectively setting some border widths to zero in order to manually collapse those borders. However, if a cell is rowspanned from a non-bottom row down into the bottom row, you can't reliably detect this, and so you can't collapse its bottom border into the table's bottom border.

Typically, we would detect when a cell is on any edge of the table using the following selectors:

/* Top edge: */
table > :is(thead,tbody,tfoot):first-child > tr:first-child > :is(td,th) {}

/* Bottom edge: */
table > :is(thead,tbody,tfoot):last-child > tr:last-child > :is(td,th) {}

/* Left edge: */
table > :is(thead,tbody,tfoot) > tr > :is(td,th):first-child {}

/* Right edge: */
table > :is(thead,tbody,tfoot) > tr > :is(td,th):last-child {}

The selectors above fail for rowspanned cells, and for cells adjacent to them (i.e. "B" in our example above would wrongly test as a leftmost-column cell).

With the column pseudo-classes introduced in Selectors 4, I believe the syntax would now be:

/* Top edge: */
table > :is(thead,tbody,tfoot):first-child > tr:first-child > :is(td,th) {}

/* Bottom edge: */
table > :is(thead,tbody,tfoot):last-child > tr:last-child > :is(td,th) {}

/* Left edge: */
table   :is(td, th):nth-col(0) {}

/* Right edge: */
table   :is(td, th):nth-last-col(0) {}

This fixes the "B" case; however, the rowspanned "A" cell in our example table above still won't test as being on the bottom edge. We would need :nth-row() and :nth-last-row() counterparts in order to cope with rowSpan.

I offer a more detailed breakdown of use cases, including links to examples, in the issue I originally opened in the WHATWG HTML repo by mistake. Those examples were written before I was aware of the yet-to-be-implemented :nth-col() and :nth-last-col() selectors, but I believe they still adequately demonstrate the utility of row-based selectors.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions