MSEdgeExplainers

CSS Gap Decorations

Authors

Participate

Status of this Document

This document is intended as a starting point for engaging the community and standards bodies in developing collaborative solutions fit for standardization. As the solutions to problems described in this document progress along the standards-track, we will retain this document as an archive and use this section to keep the community up-to-date with the most current standards venue and content location of future work and discussions.

Table of Contents

Introduction

CSS multi-column containers allow for rules to be drawn between columns. Applying similar styling to other container layouts such as grid and flex has been widely sought after, as seen in the discussion for CSS Working Group issue 2748 and in several StackOverflow questions ( [1] [2] [3] [4] ). Currently, developers seeking to draw such decorations must resort to non-ergonomic workarounds such as these examples:

Goals

Non-goals

User research

Use cases in this explainer were collected from the discussion in CSSWG issue 2748. Additional inspiration was drawn from discussions in CSSWG issues 5080, 6748, and 9482.

Comments received on the feature in MSEdgeExplainers issues 996, 1099, 1100, and 1111 have also been incorporated into the design.

Properties

Unless otherwise noted, corresponding row- and column- properties should be assumed to have identical syntax. All such pairs of properties also have shorthands that apply the same values in both directions.

For property grammar details, please see the Editor's Draft.

Width, style, and color

In addition to replicating the existing column-rule properties in the row direction, we expand the syntax of both sets of properties to allow for multiple definitions. If a given property has fewer list entries than the number of gaps, the list is cycled through from the beginning as needed.

Authors may also use familiar syntax from CSS Grid such as repeat() and auto to create patterns of line definitions. Note that while repeat() and auto are inspired by CSS Grid, they may also be used to create patterns of decorations in flex, multi-column, and grid-lanes containers.

Shorthands are also available to combine the width, style, and color properties.

.alternate-red-blue {
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-auto-rows: 30px;
  gap: 10px;
  row-rule: 1px solid;
  row-rule-color: red, blue;
}
.alternate-heavy-light {
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-auto-rows: 30px;
  gap: 10px;
  row-rule: 2px solid black, 1px solid lightgray;
}

Like column rules in multi-column layout, gap decorations in other layout containers do not take up space and do not affect the layout of items in the container. Conceptually, gap decorations are considered after layout has completed, and in particular after we already know the full extent of the implicit grid in grid layout, or the number of lines in flex layout, or the number of columns in multi-column layout, or the number of tracks in grid-lanes layout. Thus, the repeat() grammar, while modeled after the grid-template properties, is simpler for gap decorations as there are fewer unknowns to consider.

.varying-widths {
  dispay: grid;
  grid-template-columns: repeat(3, 100px);
  grid-auto-rows: 30px;
  row-gap: 9px;
  row-rule: 5px solid black, repeat(auto, 1px solid black), 3px solid black;
}
.item {
  height: 30px;
  padding: 5px;
  border: 1px dotted lightgray;
}

Interaction with intersection types

Authors may change the set of intersections where gap decorations break, from the default behavior to either "all intersections" or "no intersections." Where gap decorations overlap items in the container, the decoration is painted behind the item.

.normal {
  rule-break: normal;
}
.all-intersections {
  rule-break: intersection;
}
.no-intersections {
  rule-break: none;
}

Extending or shortening gap decoration segments

By default, gap decorations are painted as continuous segments that extend as far as possible along the centerline of a given visible gap. The decoration is painted from visible intersection to another, with each endpoint at the innermost edge of the intersection.

.grid-with-spans {
  display: grid;
  grid-template: repeat(4, 100px) / repeat(4, 100px);
  gap: 20px;
  row-rule: 6px solid red;
  column-rule: 6px solid blue;
}
.flex {
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
  width: 500px;
  row-rule: 6px solid red;
  column-rule: 6px solid blue;
}

Authors may adjust the positions of endpoints relative to gap intersections, either as a fixed distance or as a percentage of the width of the intersection. The "zero point" is the edge of the intersection, with negative values extending into the intersection and positive values receding from it.

.inset-0px {
  column-rule-break: intersection;
  column-rule-inset: 0px;
}
.inset-5px {
  column-rule-break: intersection;
  column-rule-inset: 5px;
}
.inset-negative-5px {
  column-rule-break: intersection;
  column-rule-inset: -5px;
}

Authors may also adjust endpoints more granularly, making a distinction between "edge" endpoints (which fall on the edge of the container), and "interior" endpoints (any endpoint that is not an "edge").

.edge-interior-insets {
  column-rule-break: intersection;
  column-rule-edge-inset: 0px;
  column-rule-interior-inset: -5px;
}

Similarly, authors can have even more granular control to adjust the positions of endpoints, making a distinction between "start" and "end" endpoints, in addition to the "edge" and "interior" distinction.

.start-end-edge-interior-insets {
  column-rule-break: intersection;

  column-rule-edge-inset-start: 8px;
  column-rule-interior-inset-start: 8px;
  /* or shorthand: */
  column-rule-inset-start: 8px;
}

Paint order

When row and column gap decorations overlap, authors can control their painting order. By default, row-direction decorations are painted on top of column-direction decorations.

rule-overlap: [ row-over-column | column-over-row ]
.row-over-column {
  row-rule: 6px solid red;
  column-rule: 6px solid blue;
  rule-overlap: row-over-column;
}
.column-over-row {
  row-rule: 5px solid red;
  column-rule: 5px solid blue;
  rule-overlap: column-over-row;
}

Decorations next to empty areas

By default, gap decoration segments appear throughout a container. In some cases, authors may not want to paint segments next to empty areas. The *-rule-visibility-items properties allow control over this.

.container {
  display: grid;
  grid-template: repeat(3, 100px) / repeat(3, 100px);
  gap: 10px;
  rule: 1px solid black;
  rule-break: intersection;
  rule-visibility-items: all; /* initial value */
}
.item {
  background: lightgray;
}
<div class="container">
  <div class="item" style="grid-area: 1 / 1">Item 1</div>
  <div class="item" style="grid-area: 2 / 1">Item 2</div>
  <div class="item" style="grid-area: 2 / 2">Item 3</div>
  <div class="item" style="grid-area: 3 / 1">Item 4</div>
</div>
.container {
  display: grid;
  grid-template: repeat(3, 100px) / repeat(3, 100px);
  gap: 10px;
  rule: 1px solid black;
  rule-break: intersection;
  rule-visibility-items: around;
}
.item {
  background: lightgray;
}
<div class="container">
  <div class="item" style="grid-area: 1 / 1">Item 1</div>
  <div class="item" style="grid-area: 2 / 1">Item 2</div>
  <div class="item" style="grid-area: 2 / 2">Item 3</div>
  <div class="item" style="grid-area: 3 / 1">Item 4</div>
</div>
.container {
  display: grid;
  grid-template: repeat(3, 100px) / repeat(3, 100px);
  gap: 10px;
  rule: 1px solid black;
  rule-break: intersection;
  rule-visibility-items: between;
}
.item {
  background: lightgray;
}
<div class="container">
  <div class="item" style="grid-area: 1 / 1">Item 1</div>
  <div class="item" style="grid-area: 2 / 1">Item 2</div>
  <div class="item" style="grid-area: 2 / 2">Item 3</div>
  <div class="item" style="grid-area: 3 / 1">Item 4</div>
</div>

Note that rule-visibility-items in the examples above is a shorthand for column-rule-visibility-items and row-rule-visibility-items, which can also be set independently:

.container {
  display: grid;
  grid-template: repeat(3, 100px) / repeat(3, 100px);
  gap: 10px;
  rule: 1px solid black;
  rule-break: intersection;
  column-rule-visibility-items: around;
  row-rule-visibility-items: between;
}
.item {
  background: lightgray;
}
<div class="container">
  <div class="item" style="grid-area: 1 / 1">Item 1</div>
  <div class="item" style="grid-area: 2 / 1">Item 2</div>
  <div class="item" style="grid-area: 2 / 2">Item 3</div>
  <div class="item" style="grid-area: 3 / 1">Item 4</div>
</div>

Key scenarios

Scenario 1: Horizontal lines between CSS grid rows

https://github.com/w3c/csswg-drafts/issues/2748#issuecomment-446379068, which links to: https://codepen.io/urlyman/pen/yGNOya

The desired effect is a line appearing only between the grid rows, and extending unbroken across the column gaps.

Note that I don't want a line to appear above or beneath all rows, only in the gaps between rows.

.container {
  row-rule: 1px solid #ccc;
}

Scenario 2: Lines dividing items in both directions of a grid

https://github.com/w3c/csswg-drafts/issues/2748#issuecomment-595663212

.container {
  rule: thick solid green;
}

Scenario 3: Segmented gap decorations

https://github.com/w3c/csswg-drafts/issues/2748#issuecomment-446781218 - last example

.container {
  rule: 1px solid black;
  column-rule-inset: 0px;
}

Scenario 4: Grid layout with white space in leading columns

https://github.com/MicrosoftEdge/MSEdgeExplainers/issues/1099

.layout {
  display: grid;
  grid-template-areas:
    ". . content author"
    ". . content social";
  gap: 5px;
  rule: 1px solid gray;
  rule-visibility-items: around;
  border-top: 1px solid gray;
}

Scenario 5: Column decorations only between items

https://github.com/MicrosoftEdge/MSEdgeExplainers/issues/1100

.layout {
  display: grid;
  grid-template-columns: 400px 1000px;
  column-gap: 90px;
  row-gap: 50px;
  column-rule: 1px solid white;
  column-rule-visibility-items: between;
}

Future ideas

Images

Much like border-image, support for images in gap decorations would allow for more decorative separators to be used. These could be purely design choices, or they could be used to achieve practical effects such as coupon borders. Examples:

However, unlike border-image, gap decoration images need to cover significantly more cases, such as T intersections and cross intersections. More detail and examination of this issue:

Corner joins

In Issue 985, it was suggested that we apply a border-radius like property to gap decorations to allow for more flexible styling near intersections. We could also potentially reuse concepts from corner-shape for even more flexibility. This idea is tracked in CSSWG Issue 12150.

Propagation of gap decorations into subgrids

CSS Grid Level 2 defines the subgrid feature. A subgrid matches up its grid lines to lines in the parent grid. Accordingly, gaps will also align between a subgrid and its parent grid, though the sizes of these gaps may differ. There may be use cases for propagating gap decorations from the parent grid into corresponding gaps in the subgrid; we could perhaps do this with a special keyword on the *-rule-width, *-rule-style, and *-rule-color properties. See CSSWG Issue 12326 for further discussion.

Extensions to decoration visibility controls

Design discussions for *-rule-visibility-items also considered companion *-rule-visibility-self properties which would allow the container-wide value to be overridden on specific items. For example, an author who wants to draw decorations only around a specific item in the container might set rule-visibility-items: none on the container, and rule-visibility-self: around on the specific item that they want to draw around.

start-side and end-side have also been suggested as additional values for both *-rule-visibility-items and *-rule-visibility-self, to draw decorations only on one side or the other of items.

Placement of gap decorations

An author may want to apply different sets of gap decorations to different regions of a given container layout. We refer to such regions as a gap decoration areas. The examples below illustrate how these might work on a grid container; gap decoration areas on other container types have not yet been explored.

The author defines these areas using the rule-areas property. Each area is defined by giving it first a name, then a tuple of numbers or line names which work exactly as they would in the grid-area property:

  rule-areas: --first-row 1 / 1 / 2 / -1, --first-column 1 / 1 / -1 / 2;

On a grid container, the above value of rule-areas would define an area named --first-row that includes the grid lines within and around the first row (row line 1, column line 1 to row line 2, column line -1) and an area named --first-column that includes the grid lines within and around the first column (row line 1, column line 1 to row line -1, column line 2). These areas are inclusive of the grid lines on their edges.

Then, on other gap decoration properties such as *-rule-width, *-rule-style, and *-rule-color, the author can then specify first a "default" set of values for the container, then a named area, then a set of values that applies to that area, and so on:

  rule: 1px solid black, 1px solid gray [--first-row] 3px solid black, 5px solid black [--first-column] 1px solid blue;

Cycling behavior applies in named areas the same as it does elsewhere, and where multiple values would cover the same segment of a gap, the last one that applies will "win". Thus, the value above would apply alternating 1px solid black and 1px solid gray rules to the grid in general, then override gaps in the first row with alternating 3px solid black and 5px solid black rules, then on top of that override gaps in the first column with 1px solid blue rules.

Scenario: Calendar grid with header column

.grid-multiple-decoration-areas {
  display: grid;
  grid-template-rows: [top] 30px [main-top] repeat(6, 30px) [bottom];
  grid-template-columns: [left] 100px [main-left] repeat(3, 100px) [right];
  gap: 10px;
  rule-areas: --month-column left / top / main-left / bottom;
  row-rule: 1px solid black [--month-column] 1px solid lightblue;
  column-rule: [--month-column] 1px solid lightblue;
}

Scenario: Different lines for different gaps, applied to a sub-area of a grid

https://github.com/w3c/csswg-drafts/issues/2748#issuecomment-595889781

.container {
  rule-style: solid:
  rule-color: lightgray;
  rule-areas: --main 2 / 2 / -1 / -1;
  column-rule-width: [--main] 1px repeat(auto, 2px) 1px;
  row-rule-width: [--main] 0px repeat(auto, 2px 1px);
}

Scenario: Periodic Table omitting decorations from certain areas

https://github.com/w3c/csswg-drafts/issues/12024#issuecomment-3086244002

.container {
  display: grid;
  grid-template: repeat(4, 280px) / repeat(8, auto);
  gap: 20px;
  rule-areas: --top-center 1 / 4 / 2 / 6, --bottom-left -2 / 1 / -1 / 2, --bottom-right -2 / -1 / -1 / -1;
  column-rule: 18px solid red [--top-center] none [--bottom-left] none [--bottom-right] none;
}

Dropped ideas

Logical properties

This idea was dropped based on feedback raised in the initial proposal discussion.

These are designed to enable scenarios where authors wish to switch, for example, flex-direction based on space constraints or other factors.

Property row or row-reverse direction column or column-reverse direction
main-rule-width row-rule-width column-rule-width
main-rule-style row-rule-style column-rule-style
main-rule-color row-rule-color column-rule-color
main-rule row-rule column-rule
cross-rule-width column-rule-width row-rule-width
cross-rule-style column-rule-style row-rule-style
cross-rule-color column-rule-color row-rule-color
cross-rule column-rule row-rule

And so on for other properties.

For flex containers, the logical properties map based on flex-direction following the convention above.

For grid containers, main maps to row, and cross maps to column.

For multi-column containers, main maps to column, and cross maps to row.

Considered alternatives

Alternative 1: 2021 draft specification

In 2021, Mats Palmgren from Mozilla posted a draft specification for gap decorations. We believe the proposal in this explainer improves on developer ergonomics by (a) reusing concepts from grid layout such as repeat and grid lines, and (b) simplifying the model for fine-tuning segment placement. We also believe the proposal in this explainer offers developers more flexibility even absent support for gap decoration images; see Scenario 3 for one example.

Alternative 2: Using pseudo-elements

An alternative approach to column-rule-* and row-rule-* properties would be to introduce pseudo-elements representing gaps, for example:

.container {
  display: grid;
  grid-template: auto / auto;
  gap: 5px;
}
.container::column-gaps {
  background: red;
  width: 1px;
}
.container::row-gaps {
  background: blue;
  width: 1px;
}

In some ways, this would be more powerful, as it would allow for more flexibility for what can be placed in the gaps.

However, this approach also comes with drawbacks. Varying gap decorations over a container becomes much harder. One might imagine a ::row-gaps::nth(even) pseudo selector to style every other row gap. However, certain container types such as grid can automatically generate rows and columns depending on their contents. That means we don't know until layout time how many such pseudo styles we need to produce, which creates a wrong-way dependency between layout and style. It would also mean that, for large containers, we would incur the costs of calculating and storing styles for every single gap. That would be a large overhead to absorb, especially considering that the more common case is to have at most a single decoration style for a given container.

References & acknowledgements

Many thanks for valuable feedback and advice from: