onlinespending
onlinespending

Reputation: 1149

Shrink DIV to text that's wrapped to its max-width?

Shrink wrapping a div to some text is pretty straightforward. But if the text wraps to a second line (or more) due to a max-width (as an example) then the size of the DIV does not shrink to the newly wrapped text. It is still expanded to the break point (the max-width value in this case), causing a fair amount of margin on the right side of the DIV. This is problematic when wanting to center this DIV so that the wrapped text appears centered. It will not because the DIV does not shrink to multiple lines of text that wrap. One solution is to use justified text, but that isn't always practical and the results can be hideous with large gaps between words.

I understand there's no solution to shrink the DIV to wrapped text in pure CSS. So my question is, how would one achieve this with Javascript?

This jsfiddle illustrates it: jsfiddle. The two words just barely wrap due to the max-width, yet the DIV does not then shrink to the newly wrapped text, leaving a nasty right-hand margin. I'd like to eliminate this and have the DIV resize to the wrapped text presumably using Javascript (since I don't believe a solution exists in pure CSS).

.shrunken {text-align: left; display: inline-block; font-size: 24px; background-color: #ddd; max-width: 130px;}

<div class="shrunken">Shrink Shrink</div>

Upvotes: 40

Views: 16093

Answers (8)

V. Rubinetti
V. Rubinetti

Reputation: 1716

@Jake Meyer's answer is the only correct answer. Anyone who claims that this can be done with (widely-supported) CSS only does not understand the issue.

Further, any other JS solutions are lengthy, hacky, and/or rely on reimplementing the wrapping that is being done internally by the browser, which is bound to be brittle and inaccurate.


The right answer

@Jake Meyer's answer, cleaned up a bit, with TypeScript thrown in for demonstration:

function shrinkWrap(element: HTMLElement) {
  const { firstChild, lastChild } = element;
  if (!element || !firstChild || !lastChild) return;
  const range = document.createRange();
  range.setStartBefore(firstChild);
  range.setEndAfter(lastChild);
  const { width } = range.getBoundingClientRect();
  element.style.width = width + "px";
  element.style.boxSizing = "content-box";
};

If you're using a frontend framework like React, you'll want to make sure this is called after the element has rendered -- e.g. in a useEffect or after a setTimeout -- so the element you're trying to fit has an actual size and the browser has already determined the natural text wrapping.

Difference's from @Jake Meyer's solution:

  • Uses first/last child to fully select all the text content in the element (not just childNodes[0]).
  • Applies box-sizing: content-box. Many devs these days have CSS resets that set box-sizing: border-box on everything, but in this case we want to set the width of the content box, because the bounding rect/box we get from the range is that of the selected text, i.e. the content.

Demo

function shrinkWrap(element) {
  const {
    firstChild,
    lastChild
  } = element;
  if (!element || !firstChild || !lastChild) return;
  const range = document.createRange();
  range.setStartBefore(firstChild);
  range.setEndAfter(lastChild);
  const {
    width
  } = range.getBoundingClientRect();
  element.style.width = width + "px";
  element.style.boxSizing = "content-box";
};

shrinkWrap(document.querySelector("p:nth-of-type(2)"));
* {
  box-sizing: border-box;
}

code {
  background: #eeeeee;
}

p {
  max-width: 240px;
  background: #eeeeee;
}

p:nth-of-type(3) {
  width: min-content;
}

p:nth-of-type(4) {
  width: min-content;
  min-width: 200px;
}

p:nth-of-type(5) {
  width: min-content;
  min-width: 100%;
}
❌ Regular
<p>Lorem ipsum dolor sit amet consectetur adipiscing elit</p>

✅ @Jake Meyers
<p>Lorem ipsum dolor sit amet consectetur adipiscing elit</p>

❌ @BernieSF
<p>Lorem ipsum dolor sit amet consectetur adipiscing elit</p>

❌ @Akaisteph7
<p>Lorem ipsum dolor sit amet consectetur adipiscing elit</p>

❌ @Chuck Waggon
<p>Lorem ipsum dolor sit amet consectetur adipiscing elit</p>

As you can see, the shrinkWrap @Jake Meyers method is only one that does what we want. And let's clarify what we want, using a very common use case of a tooltip:

  • The tooltip has a hard max-width limit, so that it 1) stays readable, 2) doesn't cover too many things underneath it, and 3) doesn't overflow the screen/parent/whatever with 100vw/100%/etc.
  • The tooltip tries to wrap as little as possible to stay under the max-width. Another way to say this is that the tooltip should try to expand and fill as much of the available width as possible, up to the hard limit.
  • Any leftover empty space from the wrapping should be trimmed, without affecting how it was wrapped from the requirements above. The trim shouldn't cause any additional line wraps than were needed to stay under max-width.
  • A specific sub-case of the requirements above: If the tooltip content is well under the max-width and doesn't need any line-breaks, the tooltip should shrink to just the width of that content, e.g. max-width: min(240px, max-content).

I know that this is "what we want" because it is described clearly in the original question. Anyone landing on this specific question is likely already aware of max/min-width, max/min-content, max()/min(), text-wrap, etc. And even if they're not, those things are simpler behaviors and can be looked up tons of other places.

In all the wrong solutions in the demo above, and in every answer I've seen anywhere on the internet, you get a violation of one of the requirements (shrinks/line-wraps too much or as much as possible, relies on a specific/lucky absolute width, goes over max-width, etc).

Note: Remember that min-width will always override max-width if min > max. I think this might've been achievable with only CSS if max could override min.

Upvotes: 8

Tomer Aberbach
Tomer Aberbach

Reputation: 646

If you're using React, then react-wrap-balancer, with the non-native implementation, solves this problem because it binary searches for most "tight" size as part of balancing.

<Balancer
  // Necessary because native `text-wrap: balance` just wraps
  // the text without resizing the container.
  preferNative={false}
>
  Your text...
</Balancer>

By the way, this problem is https://github.com/w3c/csswg-drafts/issues/191.

NOTE: This is only useful if you also want text balancing.

Upvotes: 0

Akaisteph7
Akaisteph7

Reputation: 6496

You can accomplish this with a combination of width styling:

.shrunken {
    width: min-content;
    min-width: 3rem;
    max-width: 5rem;
}

The result will never be perfect for all situations but this is at least a lot better than a static width.

You can also center or even justify the text so it better fits the available space:

.shrunken {
    text-align: center;
    // text-align: justify;
}

Ultimately, the best solution for me ended up being to just define separate styles for various max widths and manually use the apropriate one depending on the content.

Upvotes: -2

Jake Meyers
Jake Meyers

Reputation: 91

const range = document.createRange();
const p = document.getElementById('good');
const text = p.childNodes[0];
range.setStartBefore(text);
range.setEndAfter(text);
const clientRect = range.getBoundingClientRect();
p.style.width = `${clientRect.width}px`;

p {
  max-width: 250px;
  padding: 10px;
  background-color: #eee;
  border: 1px solid #aaa;
}

#bad {
  background-color: #fbb;
}

<p id="bad">This box has a max width but also_too_much_padding.</p>
<p id="good">This box has a max width and the_right_amount_of_padding.</p>

Upvotes: 9

Rares Ghinga
Rares Ghinga

Reputation: 1

Try this: https://jsfiddle.net/9snc5gfx/1/

.shrunken {
   width: min-content;
   word-break: normal;
}

Upvotes: -4

BernieSF
BernieSF

Reputation: 1828

I little late, but I think this CSS code can be useful for other users with the same problem:

div {
  width: -moz-min-content;
  width: -webkit-min-content;
  width: min-content;
}

Upvotes: 6

SynXsiS
SynXsiS

Reputation: 1898

It's not the prettiest solution but it should do the trick. The logic is to count the length of each word and use that to work out what the longest line is that will fit before being forced to wrap; then apply that width to the div. Fiddle here: http://jsfiddle.net/uS6cf/50/

Sample html...

<div class="wrapper">
    <div class="shrunken">testing testing</div>
</div>

<div class="wrapper">
    <div class="shrunken fixed">testing testing</div>
</div>

<div class="wrapper">
    <div class="shrunken">testing</div>
</div>

<div class="wrapper">
    <div class="shrunken fixed">testing</div>
</div>

<div class="wrapper">
    <div class="shrunken" >testing 123 testing </div>
</div>

<div class="wrapper">
    <div class="shrunken fixed" >testing 123 testing </div>
</div>

And the javacript (relying on jQuery)

$.fn.fixWidth = function () {
    $(this).each(function () {
        var el = $(this);
        // This function gets the length of some text
        // by adding a span to the container then getting it's length.
        var getLength = function (txt) {
            var span = new $("<span />");
            if (txt == ' ')
                span.html('&nbsp;');
            else
                span.text(txt);
            el.append(span);
            var len = span.width();
            span.remove();
            return len;
        };
        var words = el.text().split(' ');
        var lengthOfSpace = getLength(' ');
        var lengthOfLine = 0;
        var maxElementWidth = el.width();
        var maxLineLengthSoFar = 0;
        for (var i = 0; i < words.length; i++) {
            // Duplicate spaces will create empty entries.
            if (words[i] == '')
                continue;
            // Get the length of the current word
            var curWord = getLength(words[i]);
            // Determine if adding this word to the current line will make it break
            if ((lengthOfLine + (i == 0 ? 0 : lengthOfSpace) + curWord) > maxElementWidth) {
                // If it will, see if the line we've built is the longest so far
                if (lengthOfLine > maxLineLengthSoFar) {
                    maxLineLengthSoFar = lengthOfLine;
                    lengthOfLine = 0;
                }
            }
            else // No break yet, keep building the line
                lengthOfLine += (i == 0 ? 0 : lengthOfSpace) + curWord;
        }
        // If there are no line breaks maxLineLengthSoFar will be 0 still. 
        // In this case we don't actually need to set the width as the container 
        // will already be as small as possible.
        if (maxLineLengthSoFar != 0)
            el.css({ width: maxLineLengthSoFar + "px" });
    });
};

$(function () {
    $(".fixed").fixWidth();
});

Upvotes: 7

Jon P
Jon P

Reputation: 821

I guess this is what you are thinking about, it can be done in css:

div {
    border: black solid thin;
    max-width: 100px;
    overflow: auto;
}

You can see it here: http://jsfiddle.net/5epS4/

Upvotes: -3

Related Questions