4

I'm trying to create a horizontal navigation menu with flexible spacing between items. Flexbox seems like the appropriate choice, but I'm having trouble getting it working as I intend.

The requirements are:

  • If there is enough space available, have 4rems of space between each item
  • As the viewport width decreases, shrink the space between items evenly, down to a minimum of 2rem
  • If the viewport width decreases further, keep 2rem space between and shrink the items themselves

I would like this to accommodate a variable number of items (there'll be a maximum).

The closest I got was using 2rem padding on the items, and then making the items themselves flex containers, with shrinking pseudo-elements, but the shrinking is quite uneven.

HTML

<div class="container">
  <div class="item">
    Item 1
  </div>
  <div class="item">
    Item 2
  </div>
  <div class="item">
    Item 3 With Long Name
  </div>
  <div class="item">
    Item4WithLongNameNoSpaces
  </div>
  <div class="item">
    Item 5
  </div>
</div>

CSS

.container {
  display: flex;
}

.item + .item {
  display: flex;
  padding-left: 2rem;
}

.item + .item:before {
  content: "";
  display: block;
  width: 2rem;
  flex-shrink: 1000;
}

Codepen: https://codepen.io/qubaji/pen/PoZLEbW

It seems like flexbox could do it if I could find the right combination of flex-grow/flex-shrink values, but I can't quite get there. Any help much appreciated!

piemanji
  • 43
  • 4
  • @MrT That was my original try but this has no upper bound on the space between items, whereas I'd like it to have a maximum EDIT: Oh you deleted your comment about `justify-content: space-between` but I'll leave my reply here in case others have the same question – piemanji Jul 24 '20 at 08:48
  • yeah, sorry, didn't think you will pick it up so quickly :) – Mr T Jul 24 '20 at 08:55
  • 4
    Thats a good question, i would probably try out justify-content: space-evenly or space-between and put empty divs with max-width: 2rem in between your items. Sometimes using empty divs as spacers is a very usable solution – Warden330 Jul 24 '20 at 09:01
  • @Warden330 `justify-content: space-evenly` and `space-between` wouldn't match the requirements, but spacer divs are a good shout – piemanji Jul 24 '20 at 09:14

2 Answers2

2

I would consider display:contents to make your pseudo element at the same level of the flex items but this may limit the CSS you can apply to your items:

.container {
  display: flex;
  outline:1px solid red;
}

.item {
  display: contents;
}

.item + .item::before {
  content: "";
  width: 4rem;
  min-width:2rem;
  flex-shrink:9999;
  outline:1px solid green;
}
<div class="container">
  <div class="item">
    Item 1
  </div>
  <div class="item">
    Item 2
  </div>
  <div class="item">
    Item 3 With Long Name
  </div>
  <div class="item">
    Item4WithLong
  </div>
  <div class="item">
    Item 5
  </div>
</div>

Since you said that your elements will be limited to a maximum number, here is a CSS grid idea:

/* extra container to hide the overflow */
.wrapper {
  overflow:hidden; 
  outline:1px solid red;
}
/**/
.container {
  display: inline-grid;
  vertical-align:top;
  /* the item will take only the auto (even columns)
     the 1fr will define the shrinkable gap
  */
  grid-auto-columns:1fr auto; 
  justify-content:flex-start;
  grid-auto-flow:column; /* column flow */
  margin-left:-2rem; /* to hide the pseudo element */
}
/* this will define the size if the 1fr */
.container:before {
  content:"";
  width:2rem;
}
/* */

.item + .item {
  padding-left:2rem; /* the fixed gap */
}

/* you can easily generate the below using SASS/Less*/
.item:nth-child(1) {grid-column:2}
.item:nth-child(2) {grid-column:4}
.item:nth-child(3) {grid-column:6}
.item:nth-child(4) {grid-column:8}
.item:nth-child(5) {grid-column:10}
.item:nth-child(6) {grid-column:12}
.item:nth-child(7) {grid-column:14}
.item:nth-child(8) {grid-column:16}
.item:nth-child(9) {grid-column:18}
.item:nth-child(10) {grid-column:20}
<div class="wrapper">
  <div class="container">
    <div class="item">
      Item 1
    </div>
    <div class="item">
    Item 2
  </div>
    <div class="item">
      Item 3 With Long Name
    </div>
    <div class="item">
      Item4WithLong
    </div>
    <div class="item">
      Item 5
    </div>
  </div>
</div>
Temani Afif
  • 245,468
  • 26
  • 309
  • 415
  • Thanks, I hadn't heard of `display: contents;` which looks fantastic and works perfectly - but unfortunately this doesn't seem to be very well-supported at the moment (https://caniuse.com/#feat=css-display-contents) – piemanji Jul 24 '20 at 09:13
  • This is a cool idea too! Unfortunately because of the text wrapping you either have to use `white-space: nowrap;` or deal with additional space between elements when one wraps. I've realised this is a limitation (or feature!) of CSS, that a parent can't know when its child element wraps. Essentially I want to "shrink wrap" these elements but it doesn't look like it's possible with pure CSS. – piemanji Jul 27 '20 at 10:40
  • @piemanji yes you have no way to deal with that extra space because this how CSS works. The parent doesn't know when the content will wrap. Related: https://stackoverflow.com/a/37413580/8620333 – Temani Afif Jul 27 '20 at 10:46
0

A possible solution (or approximation) using spacer divs, as suggested by Warden330.

.container {
  display: flex;
  justify-content: center;
}
.item {
  white-space: nowrap;
}
.spacer {
  min-width: 2rem;
  max-width: 4rem;
  width: 100%;
  flex-shrink: 2;
}
<div class="container">
  <div class="item">
    Item 1
  </div>
  <div class="spacer"></div>
  <div class="item">
    Item 2
  </div>
  <div class="spacer"></div>
  <div class="item">
    Item 3 With Long Name
  </div>
  <div class="spacer"></div>
  <div class="item">
    Item4WithLongNameNoSpaces
  </div>
  <div class="spacer"></div>
  <div class="item">
    Item 5
  </div>
</div>
alotropico
  • 1,904
  • 3
  • 16
  • 24
  • Thank you for the suggestion. I did try something very similar but I didn't want to use `white-space: nowrap` as I _did_ want items to wrap - but have discovered that flexbox has no idea when this has happened so you end up with what looks like a huge space between a wrapped item and then next one. With `white-space: nowrap;` this actually works OK for my original idea without spacers. – piemanji Jul 27 '20 at 09:24
  • Sometimes we get fixed on an idea of simplicity, but often the most obvious answer is the best. I mean that using a media query in your case, to give the menu more spacing, could actually give you more precision, leading to a better result. Just a suggestion – alotropico Jul 27 '20 at 10:10