Flex transition: Stretch (or shrink) to fit content

It is possible to solve it using max-width and calc().

First, replace width: 100% with flex: 1 for the divs in CSS, so they will grow, which is better in this case. In addition, use transition for max-width.

Now, we have to store some relevant values:

  • The amount of divs that will be animated (divsLength variable) - 3 in this case.
  • The total width used for the fixed div and the borders (extraSpace variable) - 39px in this case.

With those 2 variables, we can set a default max-width (defaultMaxWidth variable) to all the divs, as well as using them later. That is why they are being stored globally.

The defaultMaxWidth is calc((100% - extraSpace)/divsLength).

Now, let's enter the click function:

To expand the div, the width of the target text will be stored in a variable called textWidth and it will be applied to the div as max-width. It uses .getBoundingClientRect().width (since it return the floating-point value).

For the remaining divs, it is created a calc() for max-width that will be applied to them.
It is: calc(100% - textWidth - extraScape)/(divsLength - 1).
The calculated result is the width that each remaining div should be.

When clicking on the expanded div, that is, to return to normal, the default max-width is applied again to all .div elements.

var expanded = false,
    divs = $(".div:not(:first-child)"),
    divsLength = divs.length,
    extraSpace = 39, //fixed width + border-right widths 
    defaultMaxWidth = "calc((100% - " + extraSpace + "px)/" + divsLength + ")";

    divs.css("max-width", defaultMaxWidth);

$(document).on("click", ".div:not(:first-child)", function (e) {
  var thisInd = $(this).index();

  if (expanded !== thisInd) {
    
    var textWidth = $(this).find('span')[0].getBoundingClientRect().width;  
    var restWidth = "calc((100% - " + textWidth + "px - " + extraSpace + "px)/" + (divsLength - 1) + ")";
    
    //fit clicked fluid div to its content and reset the other fluid divs 
    $(this).css({ "max-width": textWidth });
    $('.div').not(':first').not(this).css({ "max-width": restWidth });
    expanded = thisInd;
  } else {
    //reset all fluid divs
    $('.div').not(':first').css("max-width", defaultMaxWidth);
    expanded = false;
  }
});
.wrapper {
  overflow: hidden;
  width: 100%;
  margin-top: 20px;
  border: 1px solid black;
  display: flex;
  justify-content: flex-start;
}

.div {
  overflow: hidden;
  white-space: nowrap;
  border-right: 1px solid black;
  text-align:center;
}

.div:first-child {
  min-width: 36px;
  background: #999;
}

.div:not(:first-child) {
  flex: 1;
  transition: max-width 1s;
}

.div:not(:first-child) span {
  background: #ddd;
}

.div:last-child {
  border-right: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>

Click on the div you want to fit/reset (except the first div)

<div class="wrapper">

  <div class="div"><span>Fixed</span></div>
  <div class="div"><span>Fluid (long long long long text)</span></div>
  <div class="div"><span>Fluid</span></div>
  <div class="div"><span>Fluid</span></div>

</div>

This approach behaves dynamically and should work on any resolution.
The only value you need to hard code is the extraSpace variable.


You need to deal with the width or calc functions. Flexbox would have a solution.

To make all divs equal (not first one) we use flex: 1 1 auto.

<div class="wrapper">
  <div class="div"><span>Fixed</span></div>
  <div class="div"><span>Fluid (long long long long text)</span></div>
  <div class="div"><span>Fluid</span></div>
  <div class="div"><span>Fluid</span></div>
</div>

Define flex rules for your normal div and selected div. transition: flex 1s; is your friend. For selected one we don't need flex grow so we use flex: 0 0 auto;

.wrapper {
  width: 100%;
  margin-top: 20px;
  border: 1px solid black;
  display: flex;
}

.div {
  white-space: nowrap;
  border-right: 1px solid black;
  transition: flex 1s;
  flex: 1 1 auto;
}

.div.selected{
  flex: 0 0 auto;
}

.div:first-child {
  min-width: 50px;
  background: #999;
  text-align: center;
}

.div:not(:first-child) {
  text-align: center;
}

.div:last-child {
  border-right: 0px;
}

div:not(:first-child) span {
  background: #ddd;
}

Add selected class each time when the user clicks a div. You can also use toggle for the second click so you can save selected items in a map and you can show multiple selected items (not with this code example of course).

$(document).on("click", ".div:not(:first-child)", function(e) {
  const expanded = $('.selected');
  $(this).addClass("selected");
  if (expanded) {
    expanded.removeClass("selected");
  }
});

https://jsfiddle.net/f3ao8xcj/


After a few trial versions, this seems to be my shortest and most straighforward solution.

All that essentially needs to be done is have Flexbox stretch the <div> elements to their limits by default, but when <span> clicked, constraint the stretch of the <div> to <span> width ...

pseudo code:

when <span> clicked and already toggled then <div> max-width = 100%, reset <span> toggle state

otherwise <div> max-width = <span> width, set <span> toggle state

I have split the CSS into a 'relevant mechanism' and 'eye-candy only' section for easy reading (and code recyling).

The code is heavily commented, so not much text here...

Quirk Somehow there is an extra delay in the transition when switching the div from max-width: 100% to max-width = span width. I've checked this behaviour in Chrome, Edge, IE11 and Firefox (all W10) and all seem to have this quirk. Either some browser internal recalc going on, or maybe the transition time is used twice ('feels like'). Vice Versa, oddly enough, there is no extra delay.

However, with a short transition time (e.g. 150ms, as I am using now) this extra delay is not/hardly noticable. (Nice one for another SO question...)

$(document).on('click', '.wrapper>:not(.caption) span', function (e) {
// Save the current 'toggle' status
var elemToggled = e.target.getAttribute('toggled');

   // Set parent max-width to maximum space or constraint to current child width
    e.target.parentElement.style.maxWidth = 
        (elemToggled=="true") ? '100%' : parseFloat(window.getComputedStyle(e.target).width) + 'px';
    
    // (Re)set child toggle state 
    e.target.setAttribute('toggled', (elemToggled=="true") ? false : true);    
});
/*********************/
/* Wrapper mechanism */
/*********************/
.wrapper { /* main flexible parent container */
    display  : flex;        /* [MANDATORY] Flexbox Layout container, can't FBL without */
    flex-wrap: nowrap;      /* [MANDATORY] default FBL, but important. wrap to next line messes things up */
    flex-grow: 1;           /* [OPTIONAL] Either: if '.wrapper' is a FBL child itself, allow it to grow */
    width    : 100%;        /* [OPTIONAL] or    : full parent width */
                            /* (Maybe a fixed value, otherwise redundant here as 'flex-grow' = 1) */
}
/* generic rule */
.wrapper>* { /* flexed child containers, being flexible parent containers themselves */
    flex-grow : 1;          /* [MANDATORY] important for this mechanism to work */
    overflow: hidden;       /* [MANDATORY] important, otherwise output looks messy */

    display: flex;          /* [MANDATORY] for FBL stretching */
    justify-content: center;/* [MANDATORY] as per SOQ */

    max-width : 100%;       /* [OPTIONAL/MANDATORY], actually needed to trigger 'transition' */
}
/* exception to the rule */
.wrapper>.fixed { /* fixed child container */
    flex-grow: 0;           /* [MANDATORY] as per SOQ, don't allow grow */
}

/******************/
/* Eye-candy only */
/******************/
.wrapper {
    border: 1px solid black;
}
.wrapper>:not(.fixed) {
    transition: max-width 150ms ease-in-out; 
}
.wrapper>:not(:last-child){
    border-right: 1px solid black;
}
/* generic rule */
.wrapper>*>span {
    white-space: nowrap;
    background-color: #ddd;
}
/* exception to the rule */
.wrapper>.fixed>span {
    background-color: #999;
}

/* debug helper: show all elements with outlines (put in <body>) */
[debug="1"] * { outline: 1px dashed purple }
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="wrapper">
    <div class="fixed"><span>Fixed</span></div>
    <div><span>Fluid (long long long long long text)</span></div>
    <div><span>Fluid</span></div>
    <div><span>Fluid</span></div>
</div>

UPDATE

New version that resets all other <div>. I truly hate the jumpiness, but that is due to Flexbox stretching and the transition value. Without transition no jumps visible. You need to try out what works for you.

I only added document.querySelectorAll() to the javascript code.

$(document).on('click', '.wrapper>:not(.caption) span', function (e) {
var elemToggled = e.target.getAttribute('toggled');                    // Toggle status
var elemWidth   = parseFloat(window.getComputedStyle(e.target).width); // Current element width

    // reset ALL toggles but 'this'...
    document.querySelectorAll('.wrapper>:not(.caption) span')
        .forEach( function (elem,idx) {
                      if (elem != this){
                         elem.parentElement.style.maxWidth = '100%';
                         elem.setAttribute('toggled',false);
                      };
                  });

    // Set parent max-width to maximum space or constraint to current child width
    e.target.parentElement.style.maxWidth = 
        (elemToggled=="true") ? '100%' : parseFloat(window.getComputedStyle(e.target).width) + 'px';

    // (Re)set child toggle state 
    e.target.setAttribute('toggled', (elemToggled=="true") ? false : true);    
});
/*********************/
/* Wrapper mechanism */
/*********************/
.wrapper { /* main flexible parent container */
    display  : flex;        /* [MANDATORY] Flexbox Layout container, can't FBL without */
    flex-wrap: nowrap;      /* [MANDATORY] default FBL, but important. wrap to next line messes things up */
    flex-grow: 1;           /* [OPTIONAL] Either: if '.wrapper' is a FBL child itself, allow it to grow */
    width    : 100%;        /* [OPTIONAL] or    : full parent width */
                            /* (Maybe a fixed value, otherwise redundant here as 'flex-grow' = 1) */
}
/* generic rule */
.wrapper>* { /* flexed child containers, being flexible parent containers themselves */
    flex-grow : 1;          /* [MANDATORY] important for this mechanism to work */
    overflow: hidden;       /* [MANDATORY] important, otherwise output looks messy */

    display: flex;          /* [MANDATORY] for FBL stretching */
    justify-content: center;/* [MANDATORY] as per SOQ */

    max-width : 100%;       /* [OPTIONAL/MANDATORY], actually needed to trigger 'transition' */
}
/* exception to the rule */
.wrapper>.fixed { /* fixed child container */
    flex-grow: 0;           /* [MANDATORY] as per SOQ, don't allow grow */
}

/******************/
/* Eye-candy only */
/******************/
.wrapper {
    border: 1px solid black;
}
.wrapper>:not(.fixed) {
    transition: max-width 150ms ease-in-out; 
}
.wrapper>:not(:last-child){
    border-right: 1px solid black;
}
/* generic rule */
.wrapper>*>span {
    white-space: nowrap;
    background-color: #ddd;
}
/* exception to the rule */
.wrapper>.fixed>span {
    background-color: #999;
}
/* show all elements with outlines (put in <body>) */
[debug="1"] * { outline: 1px dashed purple }
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<div class="wrapper">
    <div class="fixed"><span>Fixed</span></div>
    <div><span>Fluid (long long long long long text)</span></div>
    <div><span>Fluid</span></div>
    <div><span>Fluid</span></div>
</div>