Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flexbox inside list item doesn't align to the top of the list item

Tags:

html

css

flexbox

Putting a flexbox inside of a list item is causing the content to get pushed down by what appears to be a full line height.

I've been playing around with different CSS properties, such as giving the flexbox margin-top: 0, its children margin-top:0, adding flex-wrap to the flexbox, etc. No dice!

.wrapper {
  display: flex;
}
li {
  background: #ccc;
}
<ul>
  <li>
    <div class="wrapper">
      <div>hello</div>
      <div>world</div>
    </div>
  </li>
</ul>

codepen

like image 957
maxedison Avatar asked Nov 03 '16 23:11

maxedison


3 Answers

This seems due to the ::marker pseudo-element.

I don't know exactly what should happen because this is an overlap of various specs

  • CSS Display 3 - the list-item keyword
  • CSS Pseudo-Elements 4 - the ::marker pseudo-element
  • CSS Lists and Counters 3 - The ::marker pseudo-element

And it's problematic because they are proposals not ready for implementation. See for example the warning in CSS Lists 3 - Positioning Markers, which is also relevant in this issue:

This section is not ready for implementation. It is an unreviewed rough draft that hasn’t even been properly checked for Web-compatibility. Feel free to send feedback, but DON’T USE THIS PART OF THE SPEC.

The following is my guess of how Chrome handles markers under the hood.

First, let's use the simpler list-style-position: inside on the list item, and let it contain a block:

ul {
  list-style-position: inside;
}
li {
  border: 1px solid blue;
}
div {
  border: 1px solid red;
}
<ul>
  <li>
    <div>Hello<br />world</div>
  </li>
</ul>

Chrome displays it like this:

enter image description here

Note the marker overlaps the div, but shrinks its first line box. This behavior is typical of floats!

A float should not overlap a formatting context root. For example, display: inline-block establishes a block formatting context.

ul {
  list-style-position: inside;
}
li {
  border: 1px solid blue;
}
div {
  display: inline-block;
  border: 1px solid red;
}
<ul>
  <li>
    <div>Hello<br />world</div>
  </li>
</ul>

enter image description here

So like a float, the marker pushes it to the right, to prevent overlapping.

But note the marker is vertically aligned. And establishing a block formatting context with overflow: hidden does not prevent overlapping.

ul {
  list-style-position: inside;
}
li {
  border: 1px solid blue;
}
div {
  overflow: hidden;
  border: 1px solid red;
}
<ul>
  <li>
    <div>Hello<br />world</div>
  </li>
</ul>

So it has some float-like behaviors, but it's not a float.

My guess is that Chrome keeps iterating the first box in the list item, then the first box inside it, and so on, until the first box is no longer a block. Then it inserts the marker just before it, displayed inline-level (maybe inline-block). Something like this:

var deepestFirstBlock,
    child = listItem;
do {
  deepestFirstBlock = child;
  child = deepestFirstBlock.firstBoxChild;
} while (child.style.display == 'block');
deepestFirstBlock.insertBefore(marker, deepestFirstBlock.firstBoxChild);

It's not exactly that, because if the direction of the deepest first block is right-to-left, then the marker is inserted after the last box of the first line.

Therefore, as noticed by Michael_B, when you style the inner container with display: inline-flex there is no empty space. That's because the marker is inserted before it, and since both are inline-level they appear side by side.

ul {
  list-style-position: inside;
}
li {
  border: 1px solid blue;
}
div {
  display: inline-flex;
  border: 1px solid red;
}
<ul>
  <li>
    <div>Hello<br />world</div>
  </li>
</ul>

enter image description here

But a display: flex container is block-level. So it is forced to go to the next line. The empty space is then the marker, which occupies the first line box.

ul {
  list-style-position: inside;
}
li {
  border: 1px solid blue;
}
div {
  display: flex;
  border: 1px solid red;
}
<ul>
  <li>
    <div>Hello<br />world</div>
  </li>
</ul>

enter image description here

This does not happen with display: block, which is also block-level, because the marker is inserted inside it. I guess the marker is not inserted inside a block-level flex container because then it would become a flex item and it could produce a bizarre layout.

When you use the initial list-style-position: outside, it seems Chrome just applies some kind of negative horizontal margin to the marker, so that the following box (which was the first one before inserting the marker) becomes aligned at the left of the container like before inserting the marker. Maybe the marker is also floated, since floated contents inside the list-item are no longer able to appear at its left.

However, this negative margin won't allow the block-level flex container to go up. That's why the space remains, even though the marker is no longer in there.

Note this contradicts the current unreviewed rough draft not ready for implementation, which says markers with list-style-position: outside should have position: marker, and

An element with position: marker counts as absolutely positioned.

Absolutely positioned elements are taken out-of-flow and overlap following elements, so the flex container should me moved to the top.

Firefox seems to do something simpler. With list-style-position: inside, it just inserts the marker as the first (inline-level) box of the list-item. It doesn't attempt to iterate blocks to insert the marker at the deepest possible level.

However, with list-style-position: outside, it probably takes the marker out-of-flow, just like the draft says. That's why you don't see the space in Firefox.

like image 99
Oriol Avatar answered Oct 19 '22 05:10

Oriol


Solutions

If you switch .wrapper from display: flex to inline-flex, the issue is resolved (demo).

If you set list-style-type to none, the issue is resolved (credit @Ricky_Ruiz in the comments) (demo).

Browser Behavior

The problem exists in Chrome, Edge and IE11. The original code works fine in Firefox and Safari.

Explanation

I'm currently not sure what's causing the behavior.

For some reason (apparently the marker), the top section of the li is off-limits. The parent is not allowing the children to access the area.

(UPDATE: A detailed explanation has been provided in another answer.)

In testing...

  • I removed padding and margin from ul and descendants. It didn't help.
  • I played with line-height and vertical-align. Also didn't help.

By setting an equal height to all elements, it's clear that something is forcing the flex container to render lower than expected, and overflow the parent.

ul {
  padding: 0;
  margin: 0;
  height: 100px;
  border: 1px solid black;
}
li {
  padding: 0;
  margin: 0;
  background: #ccc;
  height: 100%;
}
.wrapper {
  display: flex;
  padding: 0;
  margin: 0;
  height: 100%;
  border: 1px dashed red;
}
<ul>
  <li>
    <div class="wrapper">
      <div>hello</div>
      <div>world</div>
    </div>
  </li>
</ul>

revised codepen

like image 8
Michael Benjamin Avatar answered Oct 19 '22 06:10

Michael Benjamin


Adding display: block to the li style properties resolves this. By default, the user agent styles will set li elements to display as list-item. The flexbox specifications do not account for this, instead specifying behavior for only block, inline, and inline-block elements (as well as table-cell elements, which trigger block-level flex items).

like image 1
coreyward Avatar answered Oct 19 '22 05:10

coreyward