Skip to content

[CSS2][css-position-3] Negative containing blocks and auto margins #11478

Open

Description

@fantasai

Alan Baradlay noticed that we have an incompatibility between browsers in the case of a negative containing block and block-axis auto margins. Example:

<div style="border: solid; width: 100px; height: 100px; position: relative">
  <div style="position: absolute; border: solid orange 10px; margin: auto; inset: 90px"></div>
</div>

This creates a 20px orange abspos inside a 100x100 containing block, with auto margins on all sides.

But the insets are 90px, which means the inset-modified containing block would be a negative size.

CSS2 defines the vertical-axis positioning rules in terms of an equation in section 10.6.4, which results in this box being centered.

Position 3 defines a slightly different behavior. First, the negative-size IMCB is adjusted to zero-size by altering the weaker inset in section 3.5.1. Then, auto margins get an equal negative size, centering the element on the IMCB in section 4.2. (In the inline axis the spec instead start-aligns the item into the IMCB, presumably to match the asymmetry of CSS2.)

WebKit currently exhibits the CSS2 behavior, while Firefox and Chrome exhibit the css-position-3 behavior.

The difference between the two specs was not intentional. What should we do?

Activity

  1. Loirooriol commented on Jan 10, 2025

    @Loirooriol
    Contributor

    WebKit currently exhibits the CSS2 behavior, while Firefox and Chrome exhibit the css-position-3 behavior.

    I'm seeing something different:

    Webkit Firefox Chrome Servo
  2. bfgeek commented on Jan 13, 2025

    @bfgeek

    The difference in axis behaviour in Blink is due to the (compat required) difference in auto margins. See:
    https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/layout/absolute_utils.cc;l=276-279?q=is_block_direction&ss=chromium

    (In the block-direction of the child auto-margins are "unsafe", in the inline-direction they are "safe").
    https://www.software.hixie.ch/utilities/js/live-dom-viewer/?saved=13386

    Gecko & Blink match behaviour in the above case, Firefox appears to change its behaviour based on if the width/height & min-width/min-height are auto. E.g.
    https://www.software.hixie.ch/utilities/js/live-dom-viewer/?saved=13387

    Safari starts to match Servo in the above case.

    Blink is consistent in behaviour across the different cases.

  3. moved this from FTF agenda items to Not this meeting? in CSSWG April 2025 meeting agendaon Mar 28, 2025
  4. Loirooriol commented on Aug 5, 2025

    @Loirooriol
    Contributor

    It's not just auto margins, it's also not interoperable what happens with a negative inset-modified cb and auto sizes.

    <!DOCTYPE html>
    <div style="border: solid; width: 100px; height: 100px; position: relative">
      <div style="position: absolute; border: solid orange 10px; margin: -50px; inset: 90px"></div>
    </div>
    Blink Gecko, WebKit, Servo
    Aligning Servo with Blink is quite straightforward, just a matter of flooring the size of the IMCB so that it doesn't get negative.
    --- a/components/layout/positioned.rs
    +++ b/components/layout/positioned.rs
    @@ -723,6 +723,11 @@ impl AbsoluteAxisSolver<'_> {
             }
         }
     
    +    #[inline]
    +    fn available_space(&self) -> Au {
    +        Au::zero().max(self.containing_size - self.inset_sum())
    +    }
    +
         #[inline]
         fn automatic_size(&self) -> Size<Au> {
             match self.alignment.value() {
    @@ -736,8 +741,7 @@ impl AbsoluteAxisSolver<'_> {
         #[inline]
         fn stretch_size(&self) -> Au {
             Au::zero().max(
    -            self.containing_size -
    -                self.inset_sum() -
    +            self.available_space() -
                     self.padding_border_sum -
                     self.computed_margin_start.auto_is(Au::zero) -
                     self.computed_margin_end.auto_is(Au::zero),
    @@ -751,8 +755,7 @@ impl AbsoluteAxisSolver<'_> {
                     self.computed_margin_end.auto_is(Au::zero),
                 )
             } else {
    -            let free_space =
    -                self.containing_size - self.inset_sum() - self.padding_border_sum - size;
    +            let free_space = self.available_space() - self.padding_border_sum - size;
                 match (self.computed_margin_start, self.computed_margin_end) {
                     (AuOrAuto::Auto, AuOrAuto::Auto) => {
                         if self.avoid_negative_margin_start && free_space < Au::zero() {

    Other browsers seem less consistent, so I prefer either the Servo or the Blink behaviors.

  5. moved this from FTF agenda items to Tuesday Breakout in CSSWG August 2025 meeting agendaon Aug 12, 2025
  6. moved this from Tuesday Breakout to FTF agenda items in CSSWG August 2025 meeting agendaon Aug 12, 2025
  7. moved this from FTF agenda items to Thursday Breakout (1pm?) in CSSWG August 2025 meeting agendaon Aug 18, 2025
  8. 1 remaining item

  9. css-meeting-bot commented on Aug 21, 2025

    @css-meeting-bot
    Member

    The Houdini Task Force just discussed [CSS2][css-position-3] Negative containing blocks and `auto` margins, and agreed to the following:

    • RESOLVED: the IMCB size is floored at zero by adjusting weaker inset
    • RESOLVED: we center in block axis and safe align in inline axis
    The full IRC log of that discussion <alisonmaher> TabAtkins: if your CB is negatively sized
    <alisonmaher> ...how does it interact with auto margins and sizes
    <alisonmaher> ...CSS2 definition is bad and inconsistent implementations
    <alisonmaher> ...what do we want?
    <alisonmaher> ...CSS2 has diff behavior dependinding on H/V axis. We think that was an accident. Copy and paste error?
    <alisonmaher> iank_: the diff is with auto margins in one axis is safe, and other axis is not
    <alisonmaher> iank_: required by web compat
    <oriol> q+
    <alisonmaher> TabAtkins: maybe, but not consistent across browsers
    <alisonmaher> oriol: prob web compat. If IMCB is negative ?? if abspos is big enough, then all browser agree
    <alisonmaher> ...in the case where subtract index, the IMCB can be negative, servo and blink disagree. blink clamps
    <alisonmaher> ...I could align with either. Ian doesn't want it to go negative
    <alisonmaher> ...ok with that
    <alisonmaher> TabAtkins: cool with clamping sizes to not be negative
    <alisonmaher> PROPOSED: the IMCB size is floored at zero by adjusting weaker axis
    <fantasai> s/axis/inset
    <alisonmaher> iank_: agree, a mess otherwise
    <alisonmaher> TabAtkins: objections?
    <alisonmaher> RESOLVED: the IMCB size is floored at zero by adjusting weaker inset
    <alisonmaher> TabAtkins: if the CB is positive but too small, there is interop
    <alisonmaher> oriol: I believe so
    <alisonmaher> ....you may have IMCB that is non-negative, but abspos overflows, in one axis you do one thing and another in the other
    <alisonmaher> ...I recall an issue when inconsistency between browsers with orthogonal flows but eventually agree
    <alisonmaher> fantasai: weaker in block axis?
    <alisonmaher> ...shouldn't depend on direction. Should be inline vs block
    <fantasai> s/direction/physical axis/
    <alisonmaher> TabAtkins: we center in block axis and safe align in inline axis
    <alisonmaher> RESOLVED: we center in block axis and safe align in inline axis
    <oriol> https://github.com//issues/5374
  10. Loirooriol commented on Aug 22, 2025

    @Loirooriol
    Contributor

    the IMCB size is floored at zero by adjusting weaker inset

    This made me think that I had only considered sizing, not alignment. And turns out we will need to discuss this again, because Blink is only flooring the IMCB by zero when resolving auto margins or auto sizes. But it's not doing so when aligning:

    <!DOCTYPE html>
    <div style="position: relative; width: 50px; height: 50px; border: 3px solid">
      <div style="position: absolute; inset: 50px; place-self: unsafe center; border: 10px solid magenta"></div>
    </div>
    Blink, Gecko, Servo WebKit Expected from the resolution

    So arguably the only self-consistent browser is Servo.

  11. moved this from Agenda+ to Thursday Morning in CSS WG TPAC 2025 agendaon Nov 8, 2025
  12. moved this from Thursday Morning to Friday Morning in CSS WG TPAC 2025 agendaon Nov 10, 2025
  13. css-meeting-bot commented on Nov 14, 2025

    @css-meeting-bot
    Member

    The CSS Working Group just discussed [CSS2][css-position-3] Negative containing blocks and `auto` margins, and agreed to the following:

    • RESOLVED: close with previous resolution accepted
    The full IRC log of that discussion <noamr> oriol: we resolved this issue in Paris. when the inset is big enough so that the containing block is negative, what should happen? browsers seem inconsistent. Servo allows it to have a negative size. We resolved to go with the blink way. but we implemented it in servo we realized that blink it does this for sizing, auto sizing and margin, but for alignment it uses negative size inset-modified containing block
    <noamr> iank_: it's not what's happening in blink
    <noamr> iank_: it's a bug in blink. when we clamp the IMCP we have 3 options. adjust end inset, start inset, or both
    <noamr> iank_: we shift the inset based on alignment to be negative
    <noamr> iank_: the other option is to take the dominant side and always do that. blink always has a positive IMCP for alignment
    <noamr> iank_: the other option is just to take the dominant side based on writing mode and direction. I think we resolved on that.
    <noamr> oriol: we resolved on aligning with blink and then I found a case where this is different. no strong opinion
    <noamr> iank_: I am fine with changing this
    <astearns> ack fantasai
    <noamr> fantasai: between blink and resolved behavior, no strong opinion. unlikely people would intentionally run into this. intuition from authoring perspective anyone?
    <noamr> iank_: I want to clamp the negative size to non-negative
    <noamr> astearns: keep resolution and consider it a bug in blink; or amend it slightly to specify blink's current behavior
    <noamr> PROPOSED RESOLUTION: close with previous resolution accepted
    <noamr> RESOLVED: close with previous resolution accepted
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