0

Please consider the following example:

.flexer {
  display: inline-flex;
  border: 1px solid red;
  min-width: 0;
}
.extra {
  flex: 0 1 0%; /* flex-grow: 0; flex-shrink: 1; flex-basis: 0%; */
  min-width: 0;
  transition: flex-grow .3s cubic-bezier(.4,0,.2,1);
  overflow: hidden;
}
.flexer:hover .extra {
  flex-grow: 1
}
<div class="flexer">
  <div>
     test
  </div>
  <div class="extra">
    extra
  </div>
</div>

<hr>
<div class="flexer">
  <div>
     test
  </div>
</div> - How it should look, when not hovered
<br>
<div class="flexer">
  <div>
     test
  </div>
  <div>
    extra
  </div>
</div> - How it should look, when hovered
<br><br>
The red box should animate smoothly and precisely between the two widths (without delay).

I have trouble understanding why .flexer (the parent) doesn't shrink when not hovered (e.g: the red box still remains full, instead of shrinking around test).

From this q/a I understand adding min-width: 0 to the child should allow the parent to shrink. I've added it to both child and parent, to no avail.

Note 1: I'm more interested in understanding the mechanics and why this happens than finding an alternative solution (javascript, absolute positioning, etc...).
I'd like to use flexbox and I'd like to animate flex-grow - or any other animatable flex prop - for this case, if at all possible.

Note 2: the markup is irrelevant (I'm open to changing it - e.g: adding a wrapper to any of the children, if that will make my example work).

Note 3: for a clearer understanding of the desired output, see the JS based answer I added after I realised this is not possible using only CSS.

Thanks for looking into this.

a.h.g.
  • 1,429
  • 3
  • 12

5 Answers5

1

Here is my try

.flexer {
  display: inline-flex;
  border: 1px solid red;
  min-width: 0;
}
.extra {
  flex: 0 1 0%; /* flex-grow: 0; flex-shrink: 1; flex-basis: 0%; */
  width: 0;
  transition: flex-grow .3s cubic-bezier(.4,0,.2,1);
  overflow: hidden;
}

.flexer:hover .extra {
  flex-grow: 1;
  width: 100%; /* you can also use 'auto' value */
}
<div class="flexer">
  <div>
     test
  </div>
  <div class="extra">
    extra
  </div>
</div>
dita
  • 136
  • 4
  • As I've pointed out under Kameron's answer, I'm looking for a solution where the parent animates from initial width to full width. Using `width:0` on the child, it jumps from one value to the other, without animation. – a.h.g. Jun 07 '22 at 13:55
1

You were right on track. Just add width: 0; to .extra and remove the min-width. Then set the width for .extra on :hover to fit-content or auto.

.flexer {
  display: inline-flex;
  border: 1px solid red;
}

.extra {
  flex: 0 1 0%;
  /* flex-grow: 0; flex-shrink: 1; flex-basis: 0%; */
  transition: flex-grow .3s cubic-bezier(.4, 0, .2, 1);
  overflow: hidden;
  width: 0;
}

.flexer:hover .extra {
  flex-grow: 1;
  width: fit-content;
}
<div class="flexer">
  <div>
    test
  </div>
  <div class="extra">
    extra
  </div>
</div>
Kameron
  • 10,240
  • 4
  • 13
  • 26
  • That's clearly a step forward. I'm looking for a solution where the parent would animate based on current children width. In your solution, it jumps from min to max, without an animation. At a glance, this looks like a trivial task, but the more I play with it, the more it looks like it's not that simple. :) – a.h.g. Jun 07 '22 at 13:49
  • @a.h.g. So you want the border of `.flexer` to animate with the `.extra` text? – Kameron Jun 07 '22 at 13:58
  • Yes, precisely that. Thanks. That's because this item is part of a navbar menu and the menu needs to grow in width smoothly. It shouldn't jump. Another issue is that it's used multiple times, for various buttons, with various lengths of text, so I can't really hardcode a `width` value for the hidden text. It varies. – a.h.g. Jun 07 '22 at 13:59
1

You will not have any chance using flex-grow or flex-basis because by design those properties need to know the container dimension to work. they cannot update the container dimension.

What you can do is to play with width/max-width as you already discovered because those can affect the container dimension.

.flexer {
  display: inline-flex;
  border: 1px solid red;
}
.extra {
  min-width: 0;
  max-width: 0;
  transition: .3s cubic-bezier(.4,0,.2,1);
  overflow: hidden;
}
.flexer:hover .extra {
  max-width: 100px;
}
<div class="flexer">
  <div>
     test
  </div>
  <div class="extra">
    extra
  </div>
</div>

Or consider CSS grid but this will not work in all the browsers. Only Firefox for now I think:

.flexer {
  display: inline-grid;
  grid-template-columns: auto 0fr;
  justify-content: start;
  border: 1px solid red;
  transition: .3s cubic-bezier(.4, 0, .2, 1);
}

.extra {
  min-width: 0;
  overflow: hidden;
  width: 100%;
}

.flexer:hover {
  grid-template-columns: auto 1fr;
}
<div class="flexer">
  <div>
    test
  </div>
  <div class="extra">
    extra
  </div>
</div>
Temani Afif
  • 245,468
  • 26
  • 309
  • 415
  • Thanks for your answer. I've played with `grid` myself hoping it can achieve what `flexbox` can't, and now you've given me confirmation i can't use `grid`, either. Since it's not cross-browser (and particularly because it only works on a tiny fraction of the browser market), I can't mark your answer as accepted, although I must admit I was hoping for a JavaScript free solution. – a.h.g. Jun 07 '22 at 22:12
  • @a.h.g. grid is cross browser since a while now: https://caniuse.com/css-grid, it has almost the same support than flexbox: https://caniuse.com/flexbox Only the animation part won't work since it's something new (but will become supported soon) – Temani Afif Jun 07 '22 at 22:41
  • Well, it's the animation that I need. And it doesn't seem to work in Chrome, for now. – a.h.g. Jun 07 '22 at 23:09
0
<div class="flexer">
  <div>
     test
  </div>
  <div class="extra">
    extra
  </div>
</div>
.flexer {
  display: inline-flex;
  border: 1px solid red;
}
.extra {
  flex: 0 1 0%;
  transition: flex-grow .3s cubic-bezier(.4,0,.2,1);
  overflow: hidden;
  width: 0;
}
.flexer:hover .extra {
  flex-grow: 1;
  width: auto;
}

https://jsfiddle.net/1kvx2obq/1/

kushal jain
  • 85
  • 10
  • Could you create a *runnable* [mcve] as I did in my question? It looks to me like adding `width: 0` to `.extra` disables the animation. – a.h.g. Jun 07 '22 at 13:42
0

From all the answers so far, I understand there's no clean CSS way to reduce the size of the parent based on computed widths of all children, while the children are being animated, using flexbox.

Technically, this means flexbox props (-shrink, -basis, -grow) can't be used to animate here and (max-)width should be used instead. I must admit, I still find it hard to believe. I would have thought flexbox got this covered.

The problem with animating max-width or width is you can't animate from 0 to auto without JavaScript.

Which leads to the following solution:

[...document.querySelectorAll('.flexer')].forEach(el => {
  const item = el.querySelector('.extra');
  if (item) {
    el.onmouseenter = () => {
      item.style.maxWidth = item.scrollWidth + 'px'
    }
    el.onmouseleave = () => {
      item.style.maxWidth = '0'
    }
  }
})
.flexer {
  display: inline-flex;
  border: 1px solid red;
}
.extra {
  transition: max-width .42s cubic-bezier(.4, 0, .2, 1);
  overflow: hidden;
  max-width: 0;
  white-space: nowrap;
}
<div class="flexer">
  <div>
    test
  </div>
  <div class="extra">
    &nbsp;extra
  </div>
</div>

<div class="flexer">
  <div>
    test
  </div>
  <div class="extra">
    &nbsp;I have a lot more content...
  </div>
</div>

Needless to point out, if anyone has a pure CSS solution, which animates perfectly from 0 to the appropriate width for each item, I'd be happy to mark their answer as accepted.

So far, none of the provided answers has the desired output, except this one, which uses JavaScript.

a.h.g.
  • 1,429
  • 3
  • 12