Make floats and lists collaborate 🙈
April 11, 2022
April 11, 2022
Am I the only one struggling to get lists and other styled elements to render properly next to a float?
At least, it definitely doesn’t work well out of the box. Let’s start from a generic browser style sheet context (a HTML file without any CSS added), or Normalize.css.
<div class="square"></div>
<ul>
<li>Hello</li>
</ul>
.square {
float: left;
width: 6em;
height: 6em;
background: 0366d6;
}
This is not great. The left padding of the <ul>
overlaps with the
square, and so does the bullet point.
The only way I’m aware of to fix this in a normal document flow (e.g.
not doing a very custom thing with Flexbox or grids) is to set
overflow: hidden
to the <ul>
(or on a parent block that’s also being
pushed by the float):
ul {
overflow: hidden;
}
Better. Let’s add a paragraph before the square and after the <ul>
and see what happens.
<p>Paragraph</p>
<div class="square"></div>
<ul><li>Hello</li></ul>
<p>Paragraph</p>
So far so good.
Now let’s assume we have paragraphs inside our list items (yes, this
happens). You also get a similar problem with <blockquote>
s or any
other element where you might want to have a left border and padding,
and that contains paragraphs or anything with a vertical margin.
<p>Paragraph</p>
<div class="square"></div>
<ul><li><p>Hello from a paragraph</p></li></ul>
<p>Paragraph</p>
Subtle yet important thing to notice: the vertical margin from the
list paragraph now don’t merge with the adjacent margins because of our
overflow: hidden
hack! So we have double the margin before and after
the list. Not good.
But what is this behavior in the first place? Meet margin collapsing. This concept is is very well defined by CSS-Tricks:
Collapsing margins happen when two vertical margins come in contact with one another. If one margin is greater than the other, then that margin overrides the other, leaving one margin.
So by using overflow: hidden
, we break margin collapsing.
It’s quite common to use something like margin-top: 0
and
margin-bottom: 1em
on all content elements (or the other way
around) to avoid relying on (and dealing with) margin collapsing.
I ran a poll on Twitter that got 74 answers. A majority of y’all use this technique to avoid margin collapsing!
p { margin: 1em 0; }
| 37.8% |
p { margin-bottom: 1em; }
| 62.2% |
We can try this and see if it helps with our problem.
p, ul {
margin-top: 0;
}
It’s better. We don’t have double the spacing anymore above the <ul>
,
but we still have the problem with the margin-bottom
of the <ul>
not
collapsing with its inner paragraph.
What we can do though is to set overflow: hidden
on a wrapper element
instead of the <ul>
directly, then this should let the inner paragraph
collapse with it’s parent <ul>
's margin-bottom
:
<p>Paragraph</p>
<div class="square"></div>
<div class="float-hack">
<ul><li><p>Hello from a paragraph</p></li></ul>
</div>
<p>Paragraph</p>
.float-hack {
overflow: hidden;
}
Sweet, that works like a charm!
That being said, it works especially well because we decided to kill the
margin-top
of content elements, but if you want to remove the
margin-bottom
instead and keep the margin-top
, it’s a different
story:
p, ul {
margin-bottom: 0;
}
So keep that in mind of you want to drop margin collapsing, the margin you kill has an importance!
The simplicity of the previous solution pretty much convinced me to use this pattern on my blog. But for some reason you might want to keep relying on margin collapsing and still need to fix this spacing issues with floats. Let’s find a way.
Since we need to combat the fact that overflow: hidden
prevents our
vertical margins from collapsing, an option is to set margin-top: 0
recursively on all the first children of the overflow: hidden
element,
and margin-bottom: 0
on all the last children.
Why recursively? Because any element in the tree of direct first
children could set a margin-top
that we want to cancel (and similarly
for margin-bottom
and the last children tree).
But we can’t recursively target all the first or last children of an element!
So a solution would be to write something like this (relying on the
float-hack
class we introduced earlier:
.float-hack {
overflow: hidden;
}
.float-hack > :first-child,
.float-hack > :first-child > :first-child,
.float-hack > :first-child > :first-child > :first-child,
.float-hack > :first-child > :first-child > :first-child > :first-child,
.float-hack > :first-child > :first-child > :first-child > :first-child > :first-child {
margin-top: 0;
}
.float-hack > :last-child,
.float-hack > :last-child > :last-child,
.float-hack > :last-child > :last-child > :last-child,
.float-hack > :last-child > :last-child > :last-child > :last-child,
.float-hack > :last-child > :last-child > :last-child > :last-child > :last-child {
margin-bottom: 0;
}
This should be good enough for most simple cases. You can even have a
paragraph inside a <blockquote>
, itself inside a list item!
<div class="float-hack">
<ul>
<li>
<blockquote>
<p>This is a quote</p>
</blockquote>
</li>
</ul>
</div>
Or a nested list, which reach the same level of nesting:
<div class="float-hack">
<ol>
<li>
<ol>
<li>Nested list item</li>
</ol>
</li>
</ol>
</div>
Maybe don’t add a <blockquote>
with paragraphs inside this nested list
item or you might want to add a few recursion levels to our earlier CSS
rule. 😂
Note: this isn’t a complete fix as it won’t behave properly if the collapsing margins are not equal.
The engine normally keeps the greater margin, but here we’ll always nuke
the margin of the first and last items of our float-hack
element.
And there you go! Two solutions for the price of one:
margin-bottom
and setting margin-top: 0
on all content elements.overflow: hidden
element to cancel their margin.Which one is your favorite?