4

I want to use flexbox to set-up a simple elastic* grid...

.container{
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
}

...but I want the outer margins to be equal to the space between the items.

*By "elastic" I mean that the widths not fixed. They are all percentages, and so different total widths at different screen widths.

This does not appear to be the default behavior of justify-content: space-between. It generates equal space "around" each item but the space does not collapse the way margins do. So the result is that the "between" spaces are twice as wide as the far right and left (assuming you're flex-direction is row).

In the sketch below, the first diagram is what justify-content: space-between does naturally. Notice widths A and B. The second diagram is what I want (only one consistent gutter C).

Is this possible with flexbox?

enter image description here

Update

I would like to avoid having to add "row" wrapping elements if possible. The pseudo element solution sounds like just the ticket (see answers below), but let's work it out so that the items can be of an undetermined number (and wrap onto new rows)

emersonthis
  • 32,822
  • 59
  • 210
  • 375
  • 2
    Love the illustrations btw. – Joseph Marikle Feb 17 '17 at 01:31
  • @Michael_B I'd actually like that to differ based on breakpoints. That's why I'm trying to avoid the row wrappers. As a simple proof of concept let's say 2/row up to 1000px and then 3/row. Do you think we can do this using `:nth-child()`? Or maybe it doesn't matter... – emersonthis Feb 17 '17 at 01:39
  • Posted a 3:rd sample, using breakpoints – Asons Feb 18 '17 at 15:10
  • Just came across this searching for another issue. Was `justify-content: space-around;` not an option? – AlexG Jun 13 '17 at 10:23

5 Answers5

2

You can use space-between with pseudo elements from the parent

.flex {
  display: flex;
  width: 500px;
  border: 1px solid black;
  justify-content: space-between;
}
.flex > div {
  background: #eee;
  border: 1px solid #aaa;
  height: 100px; width: 100px;
}
.flex:before,.flex:after {
  content: '';
}
<div class="flex">
  <div></div>
  <div></div>
  <div></div>
</div>
Michael Coker
  • 52,626
  • 5
  • 64
  • 64
  • Wow this is clever!! Can you explain briefly why it works? Are the pseudo elements being considered as flex items? I would have expected them to "appear" before/after the flex context... but I guess not? – emersonthis Feb 17 '17 at 01:37
  • @emersonthis yep exactly. The first time I learned that this works was on accident when I spent like an hour trying to debug a flex layout, only to learn the :before and :after elements were being treated as flex children. – Michael Coker Feb 17 '17 at 01:41
  • 1
    @emersonthis but it's important to remember that the pseudo elements behave like children of the element they're associated with. For example, you can do `div { position: relative; } div:after { position: absolute; }` and the `:after` pseudo element will be positioned absolutely relative to the div. Or `div { border: 1px solid black; } div:before { content: 'foo'; }`, and "foo" will be inside of the border. Those elements behave like a child element. – Michael Coker Feb 17 '17 at 01:44
1

How about using 0 width pseudo elements before and after the items?

.container{
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    border: 1px solid #C55;
}

.column {
  flex: 1 20%;
  max-width: 20%;
  height: 100px;
  background: #C55;
}

.even:before, .even:after{
  content: '';
  display: block;
  width: 0;
}
no side spacing:
<div class="container">
  <div class="column"></div>
  <div class="column"></div>
  <div class="column"></div>
</div>
<br>
side spacing:
<div class="container even">
  <div class="column"></div>
  <div class="column"></div>
  <div class="column"></div>
</div>
Joseph Marikle
  • 76,418
  • 17
  • 112
  • 129
1

A couple of pseudo-elements will accomplish that:

flex-container {
  display: flex;
  justify-content: space-between;
  background-color: orangered;
}

flex-item {
  flex: 0 0 20%;
  height: 100px;
  margin: 5px 0;
  background-color: lightyellow;
}

flex-container::after {
  content: '';
}

flex-container::before {
  content: '';
}
<flex-container>
  <flex-item></flex-item>
  <flex-item></flex-item>
  <flex-item></flex-item>
</flex-container>

Most, if not all, browsers interpret pseudo-elements on a flex container to be flex items. The ::before pseudo is the first item, and the ::after is the last.

Here's a reference from Firefox documentation:

In-flow ::after and ::before pseudo-elements are now flex items (bug 867454).

The code above works because justify-content: space-between now factors in five flex items, but the first and last have 0 width.

I'm not sure this can be made to work in a multi-line container (flex-wrap: wrap) with pure CSS. You can try using auto margins on flex items instead of justify-content on the container. Otherwise, JavaScript.

Michael Benjamin
  • 346,931
  • 104
  • 581
  • 701
  • Do you know any specifics on the browser support for this technique? Basically, which browsers _don't_ consider pseudo-elements flex items? – emersonthis Feb 17 '17 at 17:13
  • I believe all major browsers have the same behavior. – Michael Benjamin Feb 17 '17 at 17:17
  • Awesome. I stumbled across this, which suggests that a few (older) flavors of IE11 were buggy regarding this. But sounds like it's pretty safe for production these days: https://connect.microsoft.com/IE/feedbackdetail/view/1058330/ie11-will-not-apply-flexbox-on-pseudo-elements – emersonthis Feb 17 '17 at 17:20
  • If I try the `flex-wrap: wrap` technique with `margin: auto` what should the `justify-content` setting be? Default? – emersonthis Feb 17 '17 at 17:26
  • I'm not sure what your ultimate goals are, so I don't know if `auto` margins will work for you: https://jsfiddle.net/uu9chyey/1/ – Michael Benjamin Feb 17 '17 at 17:32
  • My ultimate goal is for (in the diagram above) the **C** gaps and the item widths to be percentages so the proportions remain the same at all break points. Does that make sense? – emersonthis Feb 17 '17 at 17:46
  • So for example, I want a 3 item grid layout with each item occupying 20% of the space and 10% gutters. – emersonthis Feb 17 '17 at 17:49
  • That part I know. But what's supposed to happen on the last row, when there are less items to fill the row? `auto` margins will center them. `space-between` will have different behavior. – Michael Benjamin Feb 17 '17 at 17:54
  • Last item(s) centered would be okay. In a perfect world it would sit flushed left. But I don't think that's possible, or worth overcomplicating the solution to achieve. – emersonthis Feb 17 '17 at 17:56
  • What do you think? https://jsfiddle.net/uu9chyey/2/ – Michael Benjamin Feb 17 '17 at 18:02
  • Will it work without fixed px widths etc? Trying now... – emersonthis Feb 17 '17 at 18:03
  • If we can get that to work with % than it's a winner – emersonthis Feb 17 '17 at 18:03
  • Unfortunately, that's not recommended: http://stackoverflow.com/a/36783414/3597276 – Michael Benjamin Feb 17 '17 at 18:04
  • Sorry let me be more clear... I didn't mean necessarily overtly setting % margins for the items. More like setting % widths and then letting the resulting space between achieve an equivalent result... does that make more sense? – emersonthis Feb 17 '17 at 18:05
  • The margins guarantee the gaps between flex items. Without them, percentage-based items can blend into each other: https://jsfiddle.net/uu9chyey/3/ ... You me be better off with a script. – Michael Benjamin Feb 17 '17 at 18:12
  • Right but remember... we're trying to let the solution be agnostic of the TOTAL number of items, but we can definitely know the number of items/row at various breakpoints and adjust the % width accordingly to prevent them smashing together. Does that help? I'm thinking `:nth-child()` might be able to come to the rescue... – emersonthis Feb 17 '17 at 18:14
  • ^ I think that amounts to saying: Can we use `:nth-child()` to force a "row break" for the last item in each row? – emersonthis Feb 17 '17 at 18:16
  • Yes, I've used `:nth-child()` before in similar situations. If you know the total items per row, that's makes a solution much easier. – Michael Benjamin Feb 17 '17 at 18:18
  • Definitely. We don't know the total count. But we can definitely decide how many / row at each breakpoint. For sure. Sorry if that wasn't clear initially – emersonthis Feb 17 '17 at 18:22
  • See my answer here for an idea: http://stackoverflow.com/q/32802202/3597276 – Michael Benjamin Feb 17 '17 at 18:23
  • ah. `inline-block` I'll revisit that idea and maybe comment on the other answer to avoid getting too off-topic here – emersonthis Feb 17 '17 at 18:27
0

You can avoid the use of pseudos using margins to position the elements.

The first element is a special case, and needs a margin-left; all the others have only margin-right.

But this solution won't work with wrapping enabled. (There is no way to target the first element of the second and following rows)

.flex {
  display: flex;
  width: 500px;
  border: 1px solid black;
}

.flex > div {
  background: lightblue;
  height: 100px; 
  width: 100px;
  margin-right: auto;
}

.flex > div:first-child {
  margin-left: auto;
}
<div class="flex">
  <div></div>
  <div></div>
  <div></div>
</div>
vals
  • 61,425
  • 11
  • 89
  • 138
-1

Calculate the difference and add a margin-left to the left element and a margin-right to the right element. Though I know you wanted an HTML/CSS solution, you can do this with JavaScript and make it perfectly responsive or just feel out the margin sizes with HTML/CSS.

HTML

<div class="box" id="element"></div>
<div class="box"></div>
<div class="box"></div>

JS

window.addEventListener('load', setMargins);

var boxClass = document.getElementsByClassName('box');

function setMargins() {
    var element = document.getElementById('element');
    var widthOfElement = element.clientWidth;
    var sizeOfEachMargin = (window.innerWidth - (widthOfElement * 3)) / 4;

    for (var i = 0; i < boxClass.length; i++) {
        boxClass[i].style.marginLeft = sizeOfEachMargin + 'px';
    }
}
oldboy
  • 5,729
  • 6
  • 38
  • 86