Skip to content

[cssom-view] getClientRects() for inline box that contains block-levels #13711

Open

Description

@Loirooriol

https://drafts.csswg.org/cssom-view/#dom-element-getclientrects isn't clear about non-replaced inline boxes that contain block-level boxes.

<!DOCTYPE html>
<span id="inline">
  <div style="width: 5px; height: 5px"></div>
  <div style="width: 10px; height: 10px"></div>
  <div style="width: 15px; height: 15px"></div>
</span>
<script>
let pre = document.createElement("pre");
pre.textContent = JSON.stringify(inline.getClientRects(), null, 2);
document.body.prepend(pre);
</script>
Gecko WebKit, Servo Blink
{
  "0": {
    "x": 8,
    "y": 8,
    "width": 0,
    "height": 0,
    "top": 8,
    "right": 8,
    "bottom": 8,
    "left": 8
  },
  "1": {
    "x": 8,
    "y": 8,
    "width": 5,
    "height": 5,
    "top": 8,
    "right": 13,
    "bottom": 13,
    "left": 8
  },
  "2": {
    "x": 8,
    "y": 13,
    "width": 0,
    "height": 0,
    "top": 13,
    "right": 8,
    "bottom": 13,
    "left": 8
  },
  "3": {
    "x": 8,
    "y": 13,
    "width": 10,
    "height": 10,
    "top": 13,
    "right": 18,
    "bottom": 23,
    "left": 8
  },
  "4": {
    "x": 8,
    "y": 23,
    "width": 0,
    "height": 0,
    "top": 23,
    "right": 8,
    "bottom": 23,
    "left": 8
  },
  "5": {
    "x": 8,
    "y": 23,
    "width": 15,
    "height": 15,
    "top": 23,
    "right": 23,
    "bottom": 38,
    "left": 8
  },
  "6": {
    "x": 8,
    "y": 38,
    "width": 0,
    "height": 0,
    "top": 38,
    "right": 8,
    "bottom": 38,
    "left": 8
  }
}
{
  "0": {
    "x": 8,
    "y": 8,
    "width": 0,
    "height": 0,
    "top": 8,
    "right": 8,
    "bottom": 8,
    "left": 8
  },
  "1": {
    "x": 8,
    "y": 8,
    "width": 0,
    "height": 0,
    "top": 8,
    "right": 8,
    "bottom": 8,
    "left": 8
  },
  "2": {
    "x": 8,
    "y": 13,
    "width": 0,
    "height": 0,
    "top": 13,
    "right": 8,
    "bottom": 13,
    "left": 8
  },
  "3": {
    "x": 8,
    "y": 23,
    "width": 0,
    "height": 0,
    "top": 23,
    "right": 8,
    "bottom": 23,
    "left": 8
  },
  "4": {
    "x": 8,
    "y": 38,
    "width": 0,
    "height": 0,
    "top": 38,
    "right": 8,
    "bottom": 38,
    "left": 8
  }
}
{
  "0": {
    "x": 8,
    "y": 8,
    "width": 0,
    "height": 0,
    "top": 8,
    "right": 8,
    "bottom": 8,
    "left": 8
  },
  "1": {
    "x": 8,
    "y": 8,
    "width": 1904,
    "height": 30,
    "top": 8,
    "right": 1912,
    "bottom": 38,
    "left": 8
  },
  "2": {
    "x": 8,
    "y": 38,
    "width": 0,
    "height": 0,
    "top": 38,
    "right": 8,
    "bottom": 38,
    "left": 8
  }
}

Basically:

  • Blink wraps sequences of block-levels inside an anonymous block. So it provides 3 entries: the fragment of the inline before the anonymous block, the anonymous block, and the fragment of the inline after the anonymous block
  • Gecko doesn't wrap sequences of block-levels inside an anonymous block. So it provides 7 entries: 3 for the block-levels, and 4 for the interleaved and surrounding fragments of the inline
  • WebKit and Servo don't wrap sequences of block-levels inside an anonymous block either. They provide 5 entries: 3 for the block-levels, and 2 for the surrounding fragments of the inline. The entries for the block-levels have a size of zero instead of the size of the block-level.

Activity

  1. bfgeek commented on Mar 24, 2026

    @bfgeek

    Blink wraps sequences of block-levels inside an anonymous block. So it provides 3 entries: the fragment of the inline before the anonymous block, the anonymous block, and the fragment of the inline after the anonymous block

    This isn't strictly true. We create a inline fragment which surrounds the anonymous block which doesn't paint any edges, this is what the geometry is from.

  2. Loirooriol commented on Mar 24, 2026

    @Loirooriol
    ContributorAuthor

    Sure, I should have been more precise. What I don't get is why Blink creates these anonymous blocks.

    From https://docs.google.com/document/d/15kgdIHhb9EVNup6Ir5NWwJxpzY5GH0ED7Ld3iMW3HlA/edit?tab=t.0

    The anonymous box is not strictly required, but it eases the logic when there are multiple blocks, such as:

    • Handle margin collapsing between blocks.
    • Handle OOF/floating objects between them.

    This seemed convincing until I realized that, because of phantom line boxes, margins still need to be able to collapse across these anonymous wrappers, so what's the point? Both WebKit and Servo implemented them at first, but ended up removing them for simplicity (WebKit/WebKit#55250, servo/servo#43586).

    So I would prefer to not expose this implementation detail of Blink in getClientRects().

  3. bfgeek commented on Mar 24, 2026

    @bfgeek

    This is off-topic - but we have a lot of logic to handle relayout due to floats etc, that we didn't want to pollute inline layout with needing to handle. For us it was simpler to create a simple set of constraints for the anonymous block, then let the inner block layout algorithm deal with the complexity.

    So I would prefer to not expose this implementation detail of Blink

    I don't think this is an implementation detail. The question really is "what is the geometry of these wrapping inline fragments". This also matters for abspos layout.
    https://www.software.hixie.ch/utilities/js/live-dom-viewer/?saved=14603

    Blink arguably has the most sensible result above given how these block-in-inlines get used accidentally with web-components.

  4. smfr commented on Jun 3, 2026

    @smfr
    Contributor

    Recent webkit builds are producing

    {
      "0": {
        "x": 8,
        "y": 8,
        "width": 0,
        "height": 0,
        "top": 8,
        "right": 8,
        "bottom": 8,
        "left": 8
      },
      "1": {
        "x": 8,
        "y": 8,
        "width": 1624,
        "height": 5,
        "top": 8,
        "right": 1632,
        "bottom": 13,
        "left": 8
      },
      "2": {
        "x": 8,
        "y": 13,
        "width": 1624,
        "height": 10,
        "top": 13,
        "right": 1632,
        "bottom": 23,
        "left": 8
      },
      "3": {
        "x": 8,
        "y": 23,
        "width": 1624,
        "height": 15,
        "top": 23,
        "right": 1632,
        "bottom": 38,
        "left": 8
      },
      "4": {
        "x": 8,
        "y": 38,
        "width": 0,
        "height": 0,
        "top": 38,
        "right": 8,
        "bottom": 38,
        "left": 8
      }
    }
    
  5. css-meeting-bot commented on Jun 3, 2026

    @css-meeting-bot
    Member

    The CSS Working Group just discussed [cssom-view] getClientRects() for inline box that contains block-levels.

    The full IRC log of that discussion <fantasai> oriol: When you have an inline element that contains block-levels, it gets split in the fragment tree
    <fantasai> oriol: There's an API getClientRects() that exposes the fragments
    <fantasai> oriol: We haven't resolved on how this gets split
    <fantasai> oriol: So far only WK and Servo agree
    <fantasai> oriol: Scenario is inline with 3 block-levels inside
    <fantasai> oriol: The fragments before/after the block-levels do not have the size of the block-levels
    <fantasai> oriol: So they get a size of zero
    <fantasai> oriol: What Gecko does is similar, but the collapsible spaces between the block-levels also get fragments
    <fantasai> oriol: And size of inlines around block-levels get size of the block-level
    <fantasai> oriol: Finally Blink produces a fragment before, a fragment after
    <fantasai> oriol: Then it treats the entire sequence of block-levels as a single fragment
    <iank_> q+
    <astearns> ack fantasai
    <fantasai> oriol: It takes the size of the anonymous block, and height is the total
    <fantasai> oriol: I'm not a big fan of what Blink does, wrapping sequences inside an anonymous block seems like an implementation detail that seems weird to expose
    <fantasai> iank_: that's not correct
    <fantasai> oriol: It seems weird to me.
    <fantasai> oriol: Claim is that it offers some simplification in Blink, but worth noting that both WebKit and Servo recently refactored block-in-inline modelling Blink, but did not copy the gCR() behavior.
    <fantasai> iank_: Anonymous block is orthogonal to this issue
    <fantasai> iank_: We are not generating a fragment around the anonymous block
    <fantasai> iank_: All of the block-level content conceptually sits on a line in the inline formatting context
    <fantasai> iank_: There's a fragment for that span on that line which surrounds the content
    <fantasai> iank_: thus takes up the full available width
    <fantasai> iank_: This behavior is actually important for things like out-of-flow positioning
    <fantasai> iank_: If you span is relpos, it establishes a containing block
    <fantasai> iank_: So I argue that Blink has the most reasonable behavior here
    <fantasai> iank_: So basically Blink has fragment before/after the block-level boxes, and one wrapping the sequence of blocks. Same as having content on a line.
    <fantasai> oriol: I don't agree it's orthogonal to anonymous block
    <fantasai> oriol: You place them all on one line, while on Servo each block is on its own line
    <fantasai> [this is also true in Gecko]
    <fantasai> iank_: You can't decide where the lines are are due to margin collapsing
    <fantasai> iank_: We don't add additional spaces between lines
    <astearns> ack iank_
    <fantasai> iank_: So if you place on separate lines, you have to decide where they start/end within the collapsed space
    <emilio> fantasai: pretty sure gecko places each block in an individual line
    <emilio> ... I don't think we should spec line boxes existing around block-level boxes
    <emilio> ... if we wanted to spec this bounding box concept we could do it
    <emilio> ... but I don't think it's necessary
    <emilio> [fantasai is correct re. gecko]
    <fantasai> iank_: This is important for if this inline is a CB. We're exposing that geometry to the engine for abspos reasons
    <astearns> ack fantasai
    <astearns> ack dbaron
    <fantasai> dbaron: My memory of Gecko is that there is a special anonymous block that wraps the sequence of blocks when you split inlines
    <fantasai> dbaron: Sometimes you get more than one
    <fantasai> dbaron: If you have 3 inlines around a block, you get one for each inline
    <fantasai> emilio: I have a plan to simplify, but ...
    <fantasai> dbaron: I remember a bunch of discussions in CSSWG around 10-15 years ago
    <fantasai> dbaron: Because these anonymous boxes are detectable in multiple ways
    <fantasai> dbaron: we never agreed what to do about it
    <fantasai> dbaron: it's annoying
    <fantasai> astearns: Emilio, if you have a plan, what would you be your preference?
    <fantasai> emilio: I have a hacky prototype that does what WK does, seemed like more obvious approach
    <fantasai> iank_: Are you saying that zero width/height fragment is correct?
    <fantasai> emilio: Not necessary, but each block on its own line
    <fantasai> iank_: Orthogonal to question we're asking here
    <fantasai> iank_: I don't think WK or Servo's behavior is good. Have a zero width/height box
    <fantasai> emilio: 2 parts of this behavior
    <fantasai> emilio: 1) where do you place the blocks in BFC that contains the inline, are they their own line
    <fantasai> emilio: 2) what are the fragments of the inline
    <fantasai> emilio: I think offhand, preferred behavior would be for fragment of inline to take the blocks, like Blink
    <fantasai> emilio: To be fair, I'm still failing lots of tests
    <fantasai> emilio: But obvious approach I tried to take was do the inline fragment per block
    <fantasai> emilio: and the geometry would contain the block
    <fantasai> emilio: agree that zero-width fragments are weird
    <dbaron> CSS2.1 section 9.2.1.1 says: "When an inline box contains an in-flow block-level box, the inline box (and its inline ancestors within the same line box) are broken around the block-level box (and any block-level siblings that are consecutive or separated only by collapsible whitespace and/or out-of-flow elements), splitting the inline box into two boxes (even if either side is empty), one on each side of the block-level box(es). The line boxes
    <dbaron> before the break and after the break are enclosed in anonymous block boxes, and the block-level box becomes a sibling of those anonymous boxes. When such an inline box is affected by relative positioning, any resulting translation also affects the block-level box contained in the inline box."
    <astearns> ack dbaron
    <fantasai> astearns: Should the block-levels have height?
    <fantasai> oriol: Having size of zero was not intentional
    <smfr> webkit's behavior has changed; I pasted our new output
    <fantasai> oriol: Agree it woudl be useful to get the size of the blocks inside
    <astearns> ack dbaron
    <Zakim> dbaron, you wanted to react to dbaron
    <fantasai> dbaron: Oriol posted output from a particular DOM API
    <fantasai> dbaron: DOM API might know things are weird and skip them. So might not match the internal representation, might be some processing on top of that.
    <fantasai> dbaron: Also, I pasted quote from CSS2.1 that I have always hated.
    <fantasai> dbaron: It says that relative positioning also applies, but doesn't say that anything causes that positioning
    <fantasai> dbaron: So my reading is that, some of us wanted to say "hey there's an anonymous block there, and that causes relpos", and others didn't want the anonymous block, and now we have weird situation where the anonymous block is half there.
    <astearns> ack fantasai
    <Zakim> fantasai, you wanted to ask about zero-width fragment thing
    <emilio> fantasai: wanted to say that the zero-width fragments don't make a lot of sense to me
    <emilio> ... if the fragment is empty it should be 0-width but if it's not empty it shouldn't
    <emilio> astearns: I think we have consensus that these widths should be reflected
    <fantasai> astearns: Seems we have consensus that there should be width, just no agreement on how many things
    <fantasai> astearns: When multiple block-level elements, do you return whole set, or one per each
    <fantasai> emilio: Is the wrapping everything into a single fragment a result of the whitespace optimizations that aren't always super well handled?
    <fantasai> emilio: Whitespace between blocks creates fragment or not, depends on whether engine proves the whitespace doesn't matter
    <fantasai> emilio: I'm guessing you can convince Blink to produce 5 fragments
    <fantasai> iank_: So that's the wrong way to think about it for us
    <fantasai> iank_: The inline-level fragments only exist within line boxes. They don't exist anywhere else.
    <fantasai> iank_: When we're creating them, generate where they surround stuff
    <fantasai> iank_: When we hit block-level content, we're consuming all of that content as a unit
    <fantasai> iank_: and you need to do that for margin-collapsing purposes
    <fantasai> iank_: generating a fragment around that whole thing
    <fantasai> iank_: theoretically we could have all of this sitting on one line box and then insert extra fragments
    <fantasai> iank_: but no good reason, geometry from Blink makes more sense for abspos etc.
    <fantasai> emilio: For abspos you end up with the bounding box, right? So same containing block fragment?
    <fantasai> iank_: I think Gecko has bugs with inline containing blocks
    <fantasai> emilio: we're fixing them now!
    <fantasai> iank_: Where this gets important is inlines wrapping DIVs are actually very common to web components
    <fantasai> iank_: People forget to specify 'display: block', so this is not a corner case. It's very common.
    <astearns> ack fantasai
    <Zakim> fantasai, you wanted to comment on whitespace
    <emilio> fantasai: I thought fragmented inline-CBs were undefined?
    <emilio> iank_: they're not undefined but there are open issues about changing how they should work
    <emilio> fantasai: if you have an inline split across two lines we don't have a great definition for that
    <emilio> iank_: we have a resolution for how it works I think?
    <emilio> ... top left of first and bottom-right of last
    <emilio> ... but there was an issue to change it
    <emilio> fantasai: I don't think that's what you want here, we want to wrap the blocks
    <emilio> ... I don't think that requires that we accumulate the boxes
    <emilio> +1
    <emilio> ... could take the bounding box
    <emilio> iank_: that gets into what the cb for these are
    <emilio> ... because currently today if you take that first / last thing, blink will wrap everything
    <emilio> ... because if your end fragment is left-shifted ??
    <emilio> ... if you got three fragments
    <emilio> ... in the resolution behavior you take the top left of the first non-empty and bottom right of the last non-empty fragment
    <emilio> ... but that might not wrap all of them
    <emilio> astearns: seems like a separate issue tho
    <emilio> ... not hearing consensus on whether we should wrap the divs or not
    <emilio> ... can we resolve on them having widths / heights?
    <emilio> fantasai: we don't seem consistent on that either
    <emilio> ... Gecko returns widths and heights of each individual block
    <emilio> ... Blink is returning the width of the line box
    <emilio> ... we have the 10px box and 1000px line, gecko returns 10 and blink 1000
    <emilio> ... from the perspective of someone that wants to do something with getClientRects() the whole the CB question is separate
    <schenney> q+
    <emilio> ... because the CB is a single rect
    <emilio> ... but if somebody is looking for the answer of where's the content of this element Gecko's behavior makes more sense
    <oriol> Latest WebKit also uses total available width, not the width of the block-levels
    <emilio> ... we should probably delete the 0-width/0-height boxes
    <emilio> ... other than that, that behavior makes the most sense
    <emilio> ... it answers "where is the content"
    <emilio> astearns: if you want that CB Blink's beaviour seems appropriate
    <emilio> fantasai: if there were no block level elements you'd have just the fragments
    <emilio> ... we wouldn't be including the rest of the CB
    <emilio> ... why would we do it when there's a block level box
    <emilio> ... most consistent behavior is Gecko's
    <emilio> iank_: what I don't like about that is that this would be the only place where getClientRects returns the geometry of the children and not self
    <emilio> ... I guess the fundamental question is what the geometry of the span is
    <emilio> ... all the other side effects fall out of that
    <emilio> +1
    <astearns> ack schenney
    <emilio> ... would be skeptical of defining this just for getClientRects()
    <emilio> schenney: people use it for any time they want to paint something behind / around
    <emilio> ... know when something is or not intersecting
    <emilio> ... overflow detection tools
    <emilio> ... so it does matter that it encloses the geometry of this element
    <emilio> ... if we need to decide we might want to do more research on what the use cases are
    <emilio> ... to figure out what people do
    <emilio> q+
    <fantasai> emilio: I think we coudl agree on the inline fragments not being zero width/height
    <astearns> ack emilio
    <bradk> I’ve used it for overflow detection
    <fantasai> emilio: Inline fragments that wrap the blocks
    <fantasai> astearns: Maybe that can be input into the decision
    <fantasai> emilio: I agree with Ian that fundamental issue is what the geometry is, and gCR should reflect that
    <fantasai> astearns: ok, so let's take back to the issue
  6. Loirooriol commented on Jul 6, 2026

    @Loirooriol
    ContributorAuthor

    Other than getClientRects(), this also raises some question about when to clone decorations with box-decoration-break: clone: #14104 (comment)

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

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions