csswg-drafts icon indicating copy to clipboard operation
csswg-drafts copied to clipboard

[css-sizing-4] How does height: stretch interact with margin collapsing with parent

Open dbaron opened this issue 1 year ago • 8 comments

There were some possibly-related previous discussions in #923 and #1614.

The definition of stretch-fit sizing says:

Additionally, in formatting contexts and axes in which the relevant self-alignment property does not apply (such as the block axis in Block Layout, or the main axis in Flex Layout), in cases where a percentage size in that axis would resolve to a definite value, a stretch-fit size causes the box to attempt to fill its containing block—​behaving as 100% but applying the resulting size to its margin box instead of the box indicated by box-sizing. For this purpose, auto margins are treated as zero, and furthermore, for block-level boxes in particular, if its block-start/block-end margin would be adjoining to its parent’s block-start/block-end margin if its parent’s sizing properties all had their initial values, then its block-start/block-end margin is treated as zero.

It's not clear to me if this last sentence that says the margin is treated as zero is also intended to imply that there are revisions to the margin collapsing rules that make the margin show up on the parent. (I don't know of any version of the margin collapsing rules other than the one in CSS 2.1.) At first glance it seems like this might make sense. However, it doesn't make much sense in the case where this element has siblings. In particular:

  • The sizing rule given here sizes the element as though it has no siblings. But it might, in fact, have siblings. (But making stretch behave "correctly" when there are siblings might be something we don't really want to do -- it's certainly difficult, particularly when more than one element has stretch.)
  • The current spec definition of margin-collapsing is intentionally asymmetric: blocks with a non-auto height do not collapse their end margin with their last child. If this is intended to change that in some cases, it should probably be more explicit about it. This spec text introduces a hypothetical "if its parent's sizing properties all had their initial values" to explicitly avoid that rule, but as a result it makes a margin disappear that doesn't end up anywhere else.
  • If the goal were to have margins collapse if the stretch sizes the children to exactly fit the parent, it seems like the special collapsing rules should only apply when there aren't siblings to mess up the sizing, or something like that.

It's not clear to me why it's desirable to make these margins disappear given the current rules for handling siblings and margin collapsing. It seems like a straightforward fix would be to remove the special rule about zeroing margins for block-level boxes. Why don't we want to just allow the normal rules to run and size the element using its margins?

Or was there an intention that siblings and/or margin collapsing be handled differently?

dbaron avatar Oct 17 '24 13:10 dbaron

Quick test: https://software.hixie.ch/utilities/js/live-dom-viewer/saved/13208

<!DOCTYPE html>
<style>.stretch { height: -moz-available; height: -webkit-fill-available; height: stretch }</style>
<div style="outline: solid; height: 100px; width: 100px">
  <div class="stretch" style="margin: 10px; border: solid magenta"></div>
</div>
<br>
<div style="outline: solid; height: 100px; width: 100px">
  <div style="overflow: hidden"></div>
  <div class="stretch" style="margin: 10px; border: solid cyan"></div>
  <div style="overflow: hidden"></div>
</div>
Gecko Blink WebKit

I think the spec says that magenta should be like WebKit, and cyan like Blink, which is what looks the best to me.

Loirooriol avatar Oct 17 '24 15:10 Loirooriol

It's not completely clear to me why we want to ignore the margin on the magenta test, though. Is it really correct that the margin should just disappear? Or should we at least expect the top margin to get collapsed to the outside of the parent? (And does it then make sense to ignore the bottom margin to get some symmetry -- but not full symmetry -- since we don't want to change the margin collapsing rules too much?)

dbaron avatar Oct 17 '24 18:10 dbaron

Yes, the top margin collapses as normal, the bottom one doesn't (because of height: 100px) but it looks like collapsing.

Loirooriol avatar Oct 17 '24 18:10 Loirooriol

Maybe it's clearer like

<!DOCTYPE html>
<style>.stretch { height: -moz-available; height: -webkit-fill-available; height: stretch }</style>
⬇️ margin here
<div style="outline: solid; height: 100px; width: 100px">
  <div class="stretch" style="margin: 30px; border: solid magenta"></div>
</div>
⬆️ no margin here

The bottom margin should just be considered as collapsing with the parent for the purpose of resolving stretch. But then it doesn't actually collapse, it just overflows.

Loirooriol avatar Oct 17 '24 18:10 Loirooriol

I guess I thought the margin gets treated as zero, and doesn't get treated as collapsing. But now I think actually the spec is unclear, because it's not clear if the "and furthermore" part of the sentence is still within the scope of the "For this purpose" earlier in the sentence, or if the "and" escapes that scope. That is, it's not clear if the sentence is structured as "(For this purpose, X), and furthermore, Y." or "For this purpose, (X, and furthermore, Y).".

(I should also mention that this issue came out of a code review discussion.)

dbaron avatar Oct 17 '24 20:10 dbaron

I think it's the latter, the former seems weird.

Loirooriol avatar Oct 17 '24 20:10 Loirooriol

Some practical constraints from an engine perspective. Blink doesn't know anything about any proceeding margins, e.g.

<div style="height: stretch; margin-bottom: 10px"></div>
<div style="margin-bottom: 20px;"></div>
content

(Blink at the time of giving the first div a height, doesn't know about the 20px margin). This is a simple case, but the end margins can come from an arbitrary sibling, and we really don't want to break this invariant.

From a simplicity of implementation POV ignoring margins entirely might be an option.

A tradeoff which might be acceptable is to consider the elements (own) margins if the element is a direct child of a BFC. But from an implementation simplicity POV ignore collapsing e.g.

<div style="display: root; height: 100px;">
  <div style="margin: 10px; height: stretch;"></div>
</div>

Then for the case where the child has a non-BFC parent ignore the margins.

Also note that we are somewhat compat constrained if we want to keep stretch as a pure alias of -webkit-fill-available.

Ian

bfgeek avatar Oct 17 '24 21:10 bfgeek

we are somewhat compat constrained if we want to keep stretch as a pure alias of -webkit-fill-available

Blink and WebKit have different behaviors, so it's probably fine if we choose one or the other depending on the case.

Blink doesn't know anything about any proceeding margins

In Servo we parallelize block layout (when there are no floats), so we also have this problem for preceding margins.

ignoring margins entirely might be an option

Do you mean to just stretch the border box like WebKit? This can result in overflow if the margins don't end up collapsing, which seems bad.

consider the elements (own) margins if the element is a direct child of a BFC

I think that ignoring margins for non-BFC parent is too restrictive.

So I have another proposal. Given that:

  • stretch doesn't make that much sense if there are siblings
  • Margins collapsing through boxes was a mistake and "the root of all margin-collapsing evil".
  • In case of doubt, it seems better to not overflow

Then we could, for the purpose of resolving a vertical stretch on a block-level box:

  • Treat the top margin as 0 if the box has no preceding sibling, and the top margin is adjoining with the top margin of its parent
  • Treat the bottom margin as 0 if the box has no following sibling, and the bottom margin would be adjoining with the bottom margin of its parent if the parent’s sizing properties all had their initial values.

For the purpose of checking preceding/following siblings, ignore collapsed whitespace and out-of-flow elements.

So then I think we only need to check:

  • Whether the parent establishes an independent BFC
  • Whether the parent has vertical borders or padding
  • Whether our element has clearance (only for the top margin)
  • Whether our element is the first/last child

Seems fine from an implementation POV.

Loirooriol avatar Oct 18 '24 10:10 Loirooriol

Starting from the OP; I'll respond to further comments after.

First, we definitely don't care about siblings. We wrote these rules on the assumption that the stretched chidl is the sole content; if that's not the case, it might give suboptimal results, but you should be using Flexbox in that case anyway.

We also, I'm pretty sure intentionally, are not changing margin collapse behavior in any way. Margin collapse is complicated and we didn't want to touch it. All we say is that, if the margin is adjoining with its parents (and, for bottom margins, pretend the parent isn't using a sizing value that would prevent adjoinment), then for the purpose of sizing the child its margins are truncated to zero.

If, independent of that, the child's margins are adjoining either of the parent's margins, according to the standard rules, then they collapse as normal. This means that a sole child using 'stretch', inside of a fixed-size parent, will automatically be flush with both the top and bottom of the parent, and its top margin will be collapsing with the parent as normal; its bottom margin is simply ignored.

@fantasai, does this sound right? It's been a while since we last worked on this section.


I think the spec says that magenta should be like WebKit, and cyan like Blink, which is what looks the best to me.

Correct, that's the spec's intention. Additionally, in the magenta case, the child's top margin will have collapsed with the parent's, so the parent will effectively have a 10px top margin. The example in https://github.com/w3c/csswg-drafts/issues/11044#issuecomment-2420287787 is also correct.


That is, it's not clear if the sentence is structured as "(For this purpose, X), and furthermore, Y." or "For this purpose, (X, and furthermore, Y).".

As Oriol suggests, it's the latter. On a reread, it did take me a second to parse, tho, so I do think we should rewrite that a little.

tabatkins avatar Oct 29 '24 22:10 tabatkins

We wrote these rules on the assumption that the stretched chidl is the sole content; if that's not the case, it might give suboptimal results

The problem isn't that it's suboptimal, it's that Blink and Servo don't know whether the margins are adjoining at the time that they resolve stretch.

So IMO we can either assume that there is no sibling, or assume that if there are siblings they don't collapse thru. The latter seems better to avoid overflow if the assumption is wrong.

Loirooriol avatar Oct 29 '24 22:10 Loirooriol

I interpreted Tab's response to mean "assume that there is no sibling", FWIW.

But Oriol, can you give an example and expected behavior if we go with "assume siblings don't collapse through"?

davidsgrogan avatar Oct 29 '24 23:10 davidsgrogan

Testcase 1

<!DOCTYPE html>
<div style="outline: solid; height: 100px; width: 100px">
  <div style="overflow: hidden"></div>
  <div style="height: stretch; margin: 10px; border: solid cyan"></div>
  <div style="overflow: hidden"></div>
</div>
Assuming no sibling Assuming siblings don't collapse through

Testcase 2

<!DOCTYPE html>
<div style="outline: solid; height: 100px; width: 100px">
  <div></div>
  <div style="height: stretch; margin: 10px; border: solid cyan"></div>
  <div></div>
</div>
Assuming no sibling Assuming siblings don't collapse through

Loirooriol avatar Oct 29 '24 23:10 Loirooriol

Thanks!

So it appears that Blink currently behaves as if siblings don't collapse through, and Webkit behaves as if there is no sibling.

davidsgrogan avatar Nov 05 '24 02:11 davidsgrogan

Well, Blink behaves as if the element had siblings that don't collapse through, even if it doesn't actually have siblings.

So I want Blink's behavior when there are preceding and following siblings, WebKit's behavior when there aren't siblings, and a mix when there are only preceding or following (but not both) siblings.

Loirooriol avatar Nov 05 '24 02:11 Loirooriol

Based on some private discussion with @davidsgrogan, a few clarifications that I think need to make it into the spec:

  • "adjoining as per normal" includes when it has following siblings but they're capable of being "collapsed thru". This is the cases @Loirooriol is talking about in their earlier comment.

    I think we can safely define that away - what we want "adjoining" to mean here is "has no preceding/following in-flow sibling nodes, except possibly collapsed whitespace text nodes". This probably means, then, that we have to manually inline our definition of "adjoining" rather than reusing the existing one.

  • When a BFC child has a preceding float sibling, whether it contacts the top edge of its parent or not depends on its width (if it's narrow enough to fit next to the float). This causes similar issues as the prior. Again, I think we can just take the simple case as canon and say that preceding floats prevent it from being adjoining to the top edge, for this purpose, in all cases. (Following floats would also do so.)

The point of both of these is that our intention is just to allow an element to stretch to fill its parent in the simple, common case. We allow for reasonable complications (like the presence of abspos siblings not interfering with it), but it's fine for more complicated cases to just not work.

tabatkins avatar Nov 19 '24 18:11 tabatkins

So I think we need these edits:

diff --git a/css-sizing-4/Overview.bs b/css-sizing-4/Overview.bs
index be943838f..b994bbcbf 100644
--- a/css-sizing-4/Overview.bs
+++ b/css-sizing-4/Overview.bs
@@ -836,32 +836,52 @@ Stretch-fit Sizing: filling the containing block</h3>
 	to make its outer size as close to filling the [=containing block=] as possible
 	while still respecting the constraints imposed by min-height/min-width/max-height/max-width.
 
-	Formally, its behavior is the same as specifying an [=automatic size=]
+	In [=formatting contexts=] and axises 
+	where the relevant [=self-alignment property=] applies,
+	it gives the element the same size
+	as specifying an [=automatic size=]
 	together with a [=self-alignment property=] value of ''width/stretch''
-	(in the relevant axis),
-	except that the resulting box,
-	which can end up not exactly fitting its [=alignment container=],
-	can be subsequently aligned by its actual [=self-alignment property=] value.
+	(in the relevant axis).
+	<span class=note>(The element can then be aligned as normal
+	by its actual [=self-alignment property=] value
+	if it ends up still not exactly fitting its [=alignment container=].)</span>
 
-	Additionally,
-	in [=formatting contexts=] and axes in which the relevant [=self-alignment property=] does not apply
+	In [=formatting contexts=] and axises 
+	where the relevant [=self-alignment property=] <em>does not</em> apply
 	(such as the block axis in Block Layout, or the main axis in Flex Layout),
-	in cases where a percentage size in that axis would resolve to a definite value,
+	but a percentage size in that axis would resolve to a definite value,
 	a [=stretch-fit size=]
 	causes the box to attempt to fill its containing block--
 	behaving as ''100%''
 	but applying the resulting size to its margin box
 	instead of the box indicated by 'box-sizing'.
 	For this purpose, ''margin/auto'' margins are treated as zero,
-	and furthermore, for [=block-level boxes=] in particular,
-	if its block-start/block-end [=margin=]
-	would be adjoining to its parent's block-start/block-end [=margin=]
-	if its parent’s [=sizing properties=] all had their [=initial values=],
-	then its block-start/block-end [=margin=] is treated as zero.
+	and if either of its margins in that axis are 
+	[=adjoining its parent for stretch-fit purposes=],
+	then that margin is treated as zero.
+	<span class=note>(This causes it to size
+	as if its margin had collapsed with is parent's margin,
+	tho it does not actually change margin-collapsing behavior.)</span>
 
-	Note: Consequently, if neither ''align-self/stretch'' alignment applies
-	nor percentage sizing can resolve,
-	then the box will resolve to its [=automatic size=].
+	<div algorithm>
+		A box's margin is 
+		<dfn export>adjoining its parent for stretch-fit purposes</dfn>
+		if all of the following are true:
+
+		* it is a [=block-level box=]
+		* the margin would be [=adjoining=] its parent's matching margin,
+			assuming the parent's [=sizing properties=] all had their [=initial values=]
+		* the box does not have any preceding/following (as appropriate)
+			[=in-flow=] sibling elements or [=text runs=],
+			except [=collapsed white space=]
+		* if the box is a [=block formatting context=],
+			the box does not have any preceding/following (as appropriate) [=floating=] sibling elements
+	</div>
+
+	In all other cases
+	(when neither ''align-self/stretch'' alignment applies
+	nor percentage sizing can resolve),
+	the box resolves to its [=automatic size=].
 
 	<div class="example">
 		For example, given the following HTML representing two [=block boxes=]:

@fantasai, does this look reasonable?

tabatkins avatar Nov 19 '24 19:11 tabatkins

We could also go the other way with preceding floats, and say they don't affect adjoining-ness. Depends on which case we think will be more common and thus annoying to get wrong.

tabatkins avatar Nov 19 '24 19:11 tabatkins

So I think we need these edits:

diff --git a/css-sizing-4/Overview.bs b/css-sizing-4/Overview.bs
index be943838f..b994bbcbf 100644
--- a/css-sizing-4/Overview.bs
+++ b/css-sizing-4/Overview.bs
 [clip]
+	<div algorithm>
+		A box's margin is 
+		<dfn export>adjoining its parent for stretch-fit purposes</dfn>
+		if all of the following are true:
+
+		* it is a [=block-level box=]
+		* the margin would be [=adjoining=] its parent's matching margin,
+			assuming the parent's [=sizing properties=] all had their [=initial values=]
+		* the box does not have any preceding/following (as appropriate)
+			[=in-flow=] sibling elements or [=text runs=],
+			except [=collapsed white space=]
+		* if the box is a [=block formatting context=],
+			the box does not have any preceding/following (as appropriate) [=floating=] sibling elements
+	</div>

We obviously don't want to restrict that last bullet point to block formatting contexts (right?). Should it link to [=independent-formatting-context=] instead of [=block-formatting-context=]?

davidsgrogan avatar Nov 19 '24 19:11 davidsgrogan

I don't see the problem with following floats, they can't affect whether margins are adjoining, right?

Only preceding floats can do it, depending on whether they trigger clearance on our element. And I initially thought this would be easy to handle (Servo doesn't parallelize BFCs with floats), but if the element has an aspect ratio, then a definite block stretch size can affect its inline size, which can affect whether it needs clearance, which can affect the block stretch size.

But note that it's not just preceding floating siblings, they could be uncles or such.

Loirooriol avatar Nov 19 '24 22:11 Loirooriol

I don't see the problem with following floats, they can't affect whether margins are adjoining, right?

Wouldn't they affect if bottom margins are adjoining?

<div style="outline: solid; width: 100px; height: 100px;">
  <div style="height: stretch; margin: 10px; display: flow-root;"></div>
  <div style="float: left; width: 30px; height: 30px;"></div>
</div>

Only preceding floats can do it, depending on whether they trigger clearance on our element. And I initially thought this would be easy to handle (Servo doesn't parallelize BFCs with floats), but if the element has an aspect ratio, then a definite block stretch size can affect its inline size, which can affect whether it needs clearance, which can affect the block stretch size.

I think width: max-content column-wrap flexboxes have the same block-size-affects-inline-size problem. But, yes, this difficulty is why I brought it up to Tab :)

But note that it's not just preceding floating siblings, they could be uncles or such.

Oof, good point. This makes Tab's "other way" proposal look attractive for ease of implementation; we say margin adjoining-ness isn't affected by any preceding floats at all. Which means more overflowing will happen in the wild, compared to if we say any preceding float in this BFC, no matter where it is in the DOM, will disable adjoining-ness.

davidsgrogan avatar Nov 19 '24 22:11 davidsgrogan

Wouldn't they affect if bottom margins are adjoining?

No, see here, the bottom margins are adjoining despite the float:

<!DOCTYPE html>
<div style="outline: solid">
  <div style="display: flow-root; width: 100px; height: 100px; margin-bottom: 50px; background: cyan"></div>
  <div style="float: left; width: 50px; height: 50px; background: magenta"></div>
</div>
aaaaa

This makes Tab's https://github.com/w3c/csswg-drafts/issues/11044#issuecomment-2486584397 look attractive for ease of implementation

Maybe so, I think with floats we get into broken cases no matter what.

E.g. here we could assume that the top margin won't be adjoining because of the float, and the bottom will be adjoining, so we resolve to height: 80px. But oops, while indeed the float prevents the top margin from being adjoining, it triggers a negative clearance, so the bottom margin doesn't look adjoining as we intented:

<!DOCTYPE html>
<div style="float: left; width: 50px; height: 10px; background: magenta"></div>
<div style="outline: solid; height: 100px; width: 100px">
  <div style="display: flow-root; width: 60px; height: /*stretch*/ 80px; margin: 20px; background: cyan"></div>
</div>

And here the clearance is positive so it overflows:

<!DOCTYPE html>
<div style="float: left; width: 50px; height: 30px; background: magenta"></div>
<div style="outline: solid; height: 100px; width: 100px">
  <div style="display: flow-root; width: 60px; height: /*stretch*/ 80px; margin: 20px; background: cyan"></div>
</div>

So just like we accept that stretch won't look good if there are siblings, it's probably fine to also accept that if there are floats.

Loirooriol avatar Nov 19 '24 23:11 Loirooriol

Hi - I'm back from leave and was discussing these rules today (that David has currently implemented).

One concerning side-effect is this usecase (e.g. very common on landing pages):

<!DOCTYPE html>
<style>
html,body { height: 100%; } /* or stretch, e.g. people place -webkit-fill-available here quite often). */
div { height: stretch; margin: 50px; border: solid 5px; }
</style>
<div></div>
<div></div>
<div></div>

Here a web-developer would (IMO correctly) assume that all the <div>s would be the same size, but due to the preceding/following sibling rules they end up as different sizes which is unexpected (and very confusing to explain).

We should likely remove the proceeding/following sibling rules for this (even if it degrades some of the single element cases). IMO the multiple children cases with margin+stretch (esp. for block layout), will be far more common than the single child with margin.

(I don't have a good counter proposal yet, just that we should keep all children with the same height: stretch as having the same height).

bfgeek avatar Dec 04 '24 21:12 bfgeek

The only case I can think of where having multiple siblings with height: stretch makes sense is when the parent is a scroll container. But that avoids margin collapse, so the siblings will end up with the same height indeed.

Loirooriol avatar Dec 04 '24 22:12 Loirooriol

The only case I can think of where having multiple siblings with height: stretch makes sense is when the parent is a scroll container. But that avoids margin collapse, so the siblings will end up with the same height indeed.

Its very common for people to have wrapping elements.

bfgeek avatar Dec 04 '24 22:12 bfgeek

I was thinking about a solution which triggers a behaviour difference for a singe child vs. multiple children. However this is also kinda bad, e.g. if you are dynamically adding/removing items from a list, it'd be unexpected for a height: stretch child to suddenly change size.

bfgeek avatar Dec 05 '24 21:12 bfgeek

The only case I can think of where having multiple siblings with height: stretch makes sense is when the parent is a scroll container. But that avoids margin collapse, so the siblings will end up with the same height indeed.

Its very common for people to have wrapping elements.

In case anyone, like me, was confused about what "wrapping elements" refers to, it's wrapper divs, NOT inline or flex wrapping.

davidsgrogan avatar Dec 06 '24 18:12 davidsgrogan

Chatting with David a little, there are 3 bad scenarios:

  1. Overflow of a single element of its parent is bad (the cases talked about in comment 2 - https://github.com/w3c/csswg-drafts/issues/11044#issuecomment-2419902622).
  2. Height differences of children with the exact same styling (in block flow) is bad. (case talked about in my comment - https://github.com/w3c/csswg-drafts/issues/11044#issuecomment-2518616089 ). Similarly height differences depending on sibling presence is bad for the dynamic insertion/removal usecase.
  3. Not stretching to the parent is bad (again case talked about in comment 2, Blink's behaviour for "magenta" case).

I don't think we can solve all three of these bad scenarios, which leaves how you choose to rank them. My personal rank is "1", "2", "3". (and the current algorithm is ok with sometimes breaking "3" at the moment).

Based on my personal rank of prioritizing "1" & "2" over "3", my preferred solution is Blink's current implementation. E.g. taking the constraint of "2" you can't use any sibling information, which just leaves information provided by the parent style directly.

Ian

bfgeek avatar Dec 10 '24 20:12 bfgeek

Not opposed, that's also simpler to implement. Losing "3" is unfortunate, but I guess that authors can force the parent to establish a BFC and then the result will look more reasonable.

Loirooriol avatar Dec 10 '24 21:12 Loirooriol

Based on my personal rank of prioritizing "1" & "2" over "3", my preferred solution is Blink's current implementation. E.g. taking the constraint of "2" you can't use any sibling information, which just leaves information provided by the parent style directly.

I agree with this (and this happens to be the approach I'm going with in Gecko's stretch implementation currently).

dholbert avatar Dec 10 '24 23:12 dholbert

So this would mean that, if you were height:stretch and had margins, and you happened to be adjoining to your parent's top margin, you'd size as if you weren't adjoining but you actually would margin-collapse, leaving double space at the bottom of your parent? That seems not great.

The use-case for stretch was explicitly "do the right thing when you're the only (in-flow) child", essentially giving you inverse behavior of an auto-sized parent. (Rather than the parent sizing to you, and possibly collapsing your margins thru, you'd size to your parent, and then possibly collapse your margins thru). The behavior for when you weren't the only child was less important and explicitly not considered beyond making sure it was well-defined; what we were worried about was making it easy for a single element to fill an explicitly-sized wrapper.

Here, you're inverting that prioritization, favoring making sure the behavior when there are potentially multiple stretched children as good as possible, even if that results in worse behavior for the single-child case. (In particular, it means that the children will all size as if their margins don't collapse, even if they do end up collapsing.)

If we were going to do that, the stretch keyword should also suppress margin collapse with an element's parent, so the behavior makes sense.

But I'm still not sure we should do that in the first place. The three-children case you outlined above just wasn't the thing we were trying to solve.

tabatkins avatar Dec 10 '24 23:12 tabatkins