Skip to content

[css-gaps-1] overlap-join with between rule visibility #13697

Closed

Description

@kbabbitt

This came up while @oSamDavis @jav099 and I were discussing implementation of rule-inset: overlap-join which we resolved to add in #13137.

The intent of that value is to give authors an easy way to join decoration segments neatly with each other. Where two perpendicular segments meet, it's equivalent to rule-inset: calc( -(50% + crossing_decoration_width/2) ) or in English, "halfway into the intersection, plus enough to cover the remaining width of the crossing decoration":

Image

But there's an edge case that arises when this value is used with rule-visibility-items: between. That value causes us to paint decorations only where there are items on both sides of the gap. The calc() treatment above would lead to decorations still protruding into intersections even when there's no crossing decoration for it to join:

Image

My initial reaction was that we should not do that and instead leave such decorations flush with the adjacent items, which would be equivalent to an inset of 0 at those specific endpoints. But I'm somewhat worried that behavior might be too magical. What do folks think?

Activity

  1. self-assigned this
    on Mar 20, 2026
  2. kbabbitt commented on Mar 20, 2026

    @kbabbitt
    CollaboratorAuthor

    Here's a better illustration of the magic: https://codepen.io/kevinbcmu/pen/raMzdLW

    Image
    • Should the red segments protrude above the tops of A and B?
    • As C moves back and forth, should the bottoms of the red segments get longer and shorter depending on whether there's a blue segment to meet it?
  3. SebastianZ commented on Mar 21, 2026

    @SebastianZ
    Contributor
    • Should the red segments protrude above the tops of A and B?

    I'd say no.

    • As C moves back and forth, should the bottoms of the red segments get longer and shorter depending on whether there's a blue segment to meet it?

    I'd say yes.

    This is currently just a gut feeling, but it looks somewhat strange when the decorations protrude like that.

    But I'm somewhat worried that behavior might be too magical.

    I believe the keyword overlap-join explains this behavior nicely, as it only joins the decorations when they overlap.

    Sebastian

  4. moved this from Agenda+ FTF to Tuesday morning in CSSWG spring 2026 meeting agendaon Mar 28, 2026
  5. css-meeting-bot commented on Mar 31, 2026

    @css-meeting-bot
    Member

    The CSS Working Group just discussed [css-gaps-1] `overlap-join` with `between` rule visibility, and agreed to the following:

    • RESOLVED: Trim the unjoined segments. TBD how exactly that should work.
    The full IRC log of that discussion <TabAtkins> kbabbitt: at gap breakout we resolved to add overlap-join keyword
    <TabAtkins> kbabbitt: when you have decos in crossing directions they overlap
    <TabAtkins> kbabbitt: while implementing that, there's a weird edge cases about "between"
    <TabAtkins> kbabbitt: because overlap-join takes you to the intersection, you get a little bit sticking out
    <TabAtkins> kbabbitt: my reaction when I saw it is this is wrong, and we should leave the deco flush with the remaining items
    <TabAtkins> kbabbitt: do people think authors would want this sticking out, or should we trim?
    <TabAtkins> kbabbitt: when there's no intersecting deco?
    <miriam> q+
    <TabAtkins> fantasai: I think your heuristic makes sense to me
    <Rossen> ack miriam
    <TabAtkins> miriam: would trimming be the same as what you do at the edge of the grid?
    <TabAtkins> kbabbitt: yes, effectively
    <florian> q?
    <TabAtkins> miriam: if so, yeah, doesn't feel as magical
    <fantasai> But it gets weird here: https://github.com//issues/13697#issuecomment-4101453468
    <TabAtkins> kbabbitt: specifically, it means that overlap-join extends the decoration when there is a decoaration to join with, and doesn't when there's not
    <hoch> q+
    <florian> q+
    <TabAtkins> kbabbitt: [shows another example of the gaps extending or not]
    <TabAtkins> fantasai: given this illustration i think we don't want to trim it flush
    <TabAtkins> TabAtkins: i disagree, i htink trimming would look good
    <TabAtkins> kbabbitt: [shows an example of what it would actually look like]
    <astearns> +1 to trimming (treating gap-less sections as grid edges makes sense to me)
    <TabAtkins> miriam: going back to the previous image, there was a q about top and bottom... you said it would trim the top and not the bottom?
    <TabAtkins> kbabbitt: it would trim the tops of all lines, and the bottom of this lone line, but not the bottom of these others that area cctually joining
    <Rossen> ack hoch
    <TabAtkins> hoch: i'm wondering if this beahvior... would it be better to address this by changing the definition of what the outside is?
    <TabAtkins> hoch: if we make this magic only apply to the join keyword, if authors want it for a not-quite-overlapping join they can't get it
    <TabAtkins> hoch: so what if we defined this as making these edges count as "outside" edges?
    <TabAtkins> kbabbitt: we can indeed define edges from interior insets
    <fantasai> +1 hoch
    <TabAtkins> hoch: right so what if instead of changing this keyword, we change the definition of which corners are edge vs interior
    <TabAtkins> hoch: in these cases they are "edge" insets rather that interior ones
    <miriam> +1 that's what I was trying to get at as well
    <TabAtkins> kbabbitt: let me restate
    <TabAtkins> kbabbitt: suggestion is to have differentiated inset properties based on whether there's an intersection or not?
    <TabAtkins> hoch: yes
    <Rossen> ack florian
    <TabAtkins> florian: havne't thought about this latest variant... but until that point i think it made sense
    <TabAtkins> florian: the various examples differed in how wrong they felt, some could go either some were def wrong, but they were all at least somewhat better with the "fixed" behavior
    <Rossen> ack fantasai
    <Zakim> fantasai, you wanted to say this also allows more control
    <TabAtkins> fantasai: i think hoch's propopsal makes sense for antoher reason, you might not want those perfectly flush, might want them inset or outset a bit
    <TabAtkins> fantasai: by defining them as an exterior join you get that control
    <TabAtkins> arronei: i think it could lend to future designs we were talking about during break, corner joins being defined differently (rounded, chamfered, etc)
    <fantasai> PROPOSED: Define ends of segments that do not join to an intersection to be "exterior" insets (which can be controlled as such).
    <TabAtkins> arronei: so as we separate this out a bit it might lead to taht future work
    <fantasai> TabAtkins: overlap-join keyword, which property?
    <fantasai> kbabbitt: the inset properties
    <fantasai> TabAtkins: those have edge/interior/start/end?
    <fantasai> kbabbitt: yeah
    <fantasai> TabAtkins: So if we have this behavior, then this wouldn't be controlled by the inset property
    <fantasai> fantasai: It could be
    <fantasai> kbabbitt: If we want a different offset, then it may be a separate property. Or maybe a separate ?
    <TabAtkins> fantasai: if you already ahve edge vs interior, you'd set interior to overlap-join and the exterior to 3px
    <fantasai> TabAtkins: Ah, thought we would add a new property to enable that
    <fantasai> TabAtkins: but if they're always exterior joins ...
    <fantasai> TabAtkins: and then overlap-join would be specifiable for any interior join then that's ok
    <TabAtkins> fantasai: so if you set interior and exterior to overlap-join, you'll get an outset at the edges
    <TabAtkins> alisonmaher: would you really want the exterior edge to extend out?
    <TabAtkins> fantasai: yes. i imagine you'd want that?
    <TabAtkins> fantasai: i think if you want an outset of 3px at these "no item" "interior" corners, you probably want it on the actual exterior corners too
    <TabAtkins> fantasai: i can imagine for the shorthand, if you just said "overlap-join" we'd default that to the interior and give exterior a 0 default
    <fantasai> TabAtkins: and then overlap-join would be specifiable for any interior join then that's ok
    <fantasai> (oops sorry mispasted)
    <TabAtkins> (this is definitely running into my "there are about 20 types of joins" problem with defining gap-image)
    <TabAtkins> kbabbitt: i see where the train of thought is going... i think i need to whiteboard this out and think about it
    <fantasai> Rossen: So proposal: define ends of segments that do not join to an intersection to be "exterior" insets (which can be controlled as such).
    <fantasai> TabAtkins: Maybe just "trim those by default"?
    <oSamDavis> I think the issue is that there's no separate edge-interior property, so controlling edge controls all edges
    <TabAtkins> yeah that's my concern too, oSamDavis
    <TabAtkins> there is https://drafts.csswg.org/css-gaps/#insets-edge-interior for inside vs outside
    <alisonmaher> +1 we'd need separate controls for that likely
    <TabAtkins> fantasai: if all your other edges aren't flush it's unlikelly you want these to be flush
    <TabAtkins> kbabbitt: I think i'm good with TAb's higher-level resolution
    <fantasai> PROPOSED: Trim the unjoined segments. TBD how exactly that should work.
    <fantasai> RESOLVED: Trim the unjoined segments. TBD how exactly that should work.
  6. kbabbitt commented on Apr 3, 2026

    @kbabbitt
    CollaboratorAuthor

    Been thinking about @mhochk's suggestion more. I think my issue with treating "interior intersections with a single neighboring gap segment" as "exterior" is that, while I can make sense of it in something like:

    Image

    I have a much harder time calling them "exterior" with something like:

    Image
  7. kbabbitt commented on Apr 6, 2026

    @kbabbitt
    CollaboratorAuthor

    RESOLVED: Trim the unjoined segments. TBD how exactly that should work.

    @oSamDavis @jav099 @alisonmaher @mhochk and I had a long discussion on the "TBD" this morning. The biggest challenge we ran into is: What should we specify today to put us on the right path to a future level of Gap Decorations that affords more author control over join behaviors?

    Recap of current state

    Per the spec, we have eight inset property longhands, consisting of the various combinations of [column|row]-rule-[edge|interior]-inset-[start|end].

    In practice, I expect approximately no one other than WPT authors will use these longhands. Instead, they will use shorthands to achieve the effects they want:

    The initial value for all inset properties is 0. We resolved on that at the January F2F.

    One issue that came out of that discussion is that it should still be easy for authors to specify that they want decorations to join at T and corner intersections. They can achieve this with calc() to a degree, but it's not very author-friendly and not feasible with decorations of varying widths. So during the Gaps breakout call in mid-March, we resolved to add the overlap-join keyword to achieve joining behavior.

    Last week at the April F2F, we reached the resolution at the top of this comment: unjoined segments should not be affected by overlap-join. Simple enough on its face, but it sparked further discussion on what the right pivot points should be for insets in general and joining vs dangling ends specifically.

    That led us to today's discussion, in which a few different paths forward surfaced. Again, these are sketches of future levels of the spec, to help inform the direction we should take now. For Level 1, I don't think we should bake in any more features beyond the single toggle we resolved on that allows authors to achieve joins without having to rely on calc().

    All names in the examples below are subject to bikeshedding.

    Option 1: Move join behavior to a separate property

    Level 1

    *-rule-*-inset-*: <length-percentage>
    rule-intersection-join: [ none | overlap ]

    Simple toggle. If the decoration end adjoins an intersection with one or more other decorations, stretch into an overlap-join; otherwise apply the inset.

    Future possibility

    *-rule-*-inset-*: <length-percentage>
    rule-intersection-join: [ none | overlap | abutment ] <length-percentage>?

    The optional <length-percentage> on *-rule-intersection-join would further offset from the keyword effect, for example allowing authors to achieve a "near abutment" effect where the decoration protrudes into the intersection without actually touching other decorations.

    Advantages

    Avoids further exploding the *-rule-*-inset-* property/value space.

    Drawbacks

    Draws a boundary around the flexibility we could expose to authors in the future. I think this is, essentially, a boiled-down version of Option 3.

    The way to achieve an abutment join is not clear. If there are separate column- and row- variants of rule-intersection-join then an abutment would require you to specify overlap on one and abutment on the other to indicate which abuts the other, which is a bit of a wart. Or we could have row-abuts-column and column-abuts-row.

    Option 2: Supply join and not-join behaviors together on the inset properties

    There are a couple of variations on this theme:

    2a. Function for join vs non-join behaviors

    Level 1

    *-rule-*-inset-*: <length-percentage> | overlap-join

    If the decoration end adjoins an intersection with one or more other decorations, stretch into an overlap-join; otherwise apply the inset.

    Future possibility

      *-rule-*-inset-*: <inset-value>
      <inset-value> =   <length-percentage>
                        | overlap-join   /* = if-intersecting(overlap 0px, 0px) */
                        | abutment-join  /* = if-intersecting(abutment 0px, 0px */
                        | if-intersecting([abutment | overlap]? <length-percentage>, <length-percentage>);

    The if-intersecting() function applies the first value in cases where a decoration end adjoins an intersection with one or more decorations, the second value in other intersections. The optional [abutment | overlap] keyword changes the reference point for the <length-percentage> from "edge of the intersection" to "near edge of the crossing decoration" or "far edge of the crossing decoration" respectively.

    Advantages

    More flexible than Option 1; gives authors control over decoration lengths both in cases where decorations intersect each other and where they're left dangling.

    Drawbacks

    Not as cascade-friendly as separating intersecting and non-intersecting behaviors into their own properties. Forces authors into a complex functional notation if they want join behavior at applicable intersections and inset behavior at other intersections.

    2b. Instead of eight longhands, a single extensible rule-inset property

    We didn't fully flesh this one out, but roughly it would be:

      rule-inset: <inset-spec>#
      <inset-spec> = [ [ interior | exterior ] || [ start | end ]
                       || [ row | column ] || [ intersecting | dangling ] ]? <length-percentage>

    Missing from the straw grammar above: selection of overlap or abutment join behavior.

    In this model, authors would define their inset behaviors with a single property. Values with various combinations of keywords apply to all endpoints that match those keywords; for a given endpoint the last applicable value wins.

    Example:

      /* Apply an inset of 0px to all endpoints except column starts which are 5px */
      rule-inset: 0px, column start 5px;

    Advantages

    Limits the explosion of additional properties as we discover more ways authors want to apply differing insets.

    Drawbacks

    Even less cascade-friendly. Not clear that selection of overlap or abutment join behavior would integrate cleanly into the grammar: it would be useful for the "intersecting" properties but a no-op (or invalid?) for the "dangling" properties.

    Option 3: Change the edge|interior distinction to intersecting|dangling

    This is similar to @mhochk's previous suggestion, except using the terms "intersecting" and "dangling" to avoid confusion about the endpoints inside the "donut hole" in my reaction comment.

    Level 1

    [column|row]-rule-[intersecting|dangling]-inset-[start|end]: <length-percentage> | overlap-join

    Future possibility

    [column|row]-rule-[intersecting|dangling]-inset-[start|end]: <length-percentage> [ overlap-join | abutment-join ]

    Advantages

    Achieves flexible distinction between intersecting and non-intersecting decorations in a cascade-friendly way, without further increasing the number of properties in Level 1.

    Drawbacks

    We might need to add back the edge|interior distinction later, especially if we pursue the author suggestion in #13087 to allow for gaps & decorations around the edges of a container. If that happens, that would bring us up to sixteen inset longhands which exist only to allow for the various permutations authors might actually want. We might also find ourselves needing to shorthandify some of our L1 properties, which runs into the issues discussed in #8398. Also runs into the same issues as Option 1 with regard to achieving an abutment join.

  8. jav099 commented on Apr 7, 2026

    @jav099
    Collaborator

    In my opinion, option 1 feels too restrictive, somewhat unintuitive (due to moving inset-behavior outside of the inset property), and additionally has the warts discussed above. Options 2a and 2b feel overly complex for what should be more simple and intuitive behavior, and the cascade-hostile aspect of them seem like red flags to me.

    As such, I personally think I like Option 3 the best. It covers the basic cases for which there will be use cases, and it does it in a way that is easy to explain and easy to reason about. It provides decent extensibility moving forward without the fundamental limitations and complexities of the other options.

    However, I don't love the names intersecting and dangling. I think dangling has somewhat of negative connotations and not as easy to understand for non-native english speakers. Some alternatives I would propose are intersecting/nonintersecting, open/closed and blocked/unblocked. I think the last two suggestions read naturally (i.e. "for closed endpoints, overlap-join; for open endpoints, inset 0"), are the simplest vocab-wise and fulfill the key idea that the name should describe the visual condition of the endpoint

  9. oSamDavis commented on Apr 7, 2026

    @oSamDavis
    Collaborator

    Thanks for drafting these up, @kbabbitt. My thoughts align closely with @jav099:

    • Option 1: introduces two separate properties that control almost the same behavior, which makes it a confusing interaction in css. If an author sets both *-rule-*-inset-* and rule-intersection-join, it becomes unclear which wins and difficult to debug why a decoration looks the way it does. Splitting related concerns across properties like this feels somewhat brittle.

    • Options 2a and 2b: you've already captured this well in your writeup, but I want to +1 the concern: both variants feel un-CSS-like and impose a high mental workload on authors to express what should be straightforward behavior. The functional notation in 2a and the keyword-heavy grammar in 2b are powerful but feel mentally heavy for css.

    • Option 3: feels like the strongest path forward. It gives authors meaningful, cascade-friendly control over intersecting vs. non-intersecting endpoints without adding new properties beyond what we have today.

    That said, I agree with Javier that "dangling" is not a great name as it is definitely not intuitive for non-native English speakers, and I'd also argue it's not intuitive for non-tech folks. I'd propose keeping edge from the current edge|interior distinction and pairing it with intersecting, giving us [column|row]-rule-[intersecting|edge]-inset-[start|end]. The term "edge" can describe endpoints that don't meet a crossing decoration (whether they're on the outer boundary of the grid or on the inside of a "donut hole.") Both are visually edges of a decoration segment in my opinion.

    This can also set us up well for the future: if we later need to distinguish between outer-boundary edges and inner-boundary edges (e.g., for #13087), we could introduce exterior/interior as further refinements of edge. There's also @alisonmaher's earlier exclude-edges suggestion from #12024, which could evolve into something like exclude-exterior / exclude-interior to give authors targeted control over which edge endpoints receive certain treatments.

  10. mhochk commented on Apr 7, 2026

    @mhochk

    @kbabbitt @oSamDavis @jav099 @alisonmaher and I chatted some more today and mostly aligned that a variant of Option 3 was our best bet. Namely, we don't think that we will need to later add edge|interior support, as we have no known use cases that wouldn't be satisfied instead by intersecting|dangling. If we did end up needing to expand to support edge|interior distinction, we would need to do so with a function similar to option 2a, to avoid the problems of turning a shorthand into a longhand.

    So to recap the proposal, the set of 8 longhand properties would look like:

    [column|row]-rule-[intersecting|dangling]-inset-[start|end]: <length-percentage> | overlap-join
    

    Any existing references from edge|interior would be updated to intersecting|dangling, so the rule-inset shorthand would be:

    rule-inset: dangling-start dangling-end? [/ intersecting-start intersecting-end?]?
    

    overlap-join would assign 0px as the corresponding dangling value and calc( -(50% + crossing_decoration_width/2) ) for the intersecting value. This means you could specify just the keyword to get the expected common join case:

    rule-inset: overlap-join;
    

    or you could make a modification of just the dangling portion from 0px to 3px by doing something as simple as:

    rule-inset: 3px / overlap-join;
    

    If we ever had to add support for distinguishing between interior and exterior/edge in the future (which we are betting we will not have to do), we could do so with a exterior-interior(<inset-value>, <inset-value>) function.

    Assuming there is broader agreement on all of that, the remaining piece is bikeshedding the names. The group seemed to like intersecting, but there were mixed opinions on what the other name should be. Some options include:

    • intersecting/nonintersecting
    • intersecting/hanging
    • intersecting/trailing
    • intersecting/spoked
    • intersecting/bounds
    • intersecting/discontinuous
    • intersecting/jagged
    • intersecting/splintered
    • intersecting/severed
    • intersecting/edge
    • open/closed
    • blocked/unblocked
  11. 11 remaining items

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

Metadata

Metadata

Assignees

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions