Search code examples
javascripthtmlcssknockout.jscss-transforms

Squeeze text to fit in CSS


I have a "wall" of quare-sized Bootstrap button with one fontawesome icon and two lines of text (<span>) below. Sometimes, the text for each line can be too long, so it wraps naturally. I want to prevent that. However, it is a requirement, that the full text must be displayed. I also don't want to break the tile layout by enlarging single tiles.

Therefore, my idea was to use a CSS transform: scaleX(?) to squeeze the text in case. But I don't have a reference to the actual width of the text. Also, the with of the tiles is based on relative units, so I can't use any fixed pixel values.

Here's my current css declatation:

<style>
    .flex-container .btn-lg {
        width: 20vmin;
        height: 20vmin;
        margin: 8px;
        font-size: 3.5vmin;
        word-wrap: break-word;
        white-space: normal;
    }

    .btn-tile > * {
        display: flex;
        justify-content: center;
    }
</style>

<div data-bind="foreach: $data.entries" class="flex-container">
    <button class="btn btn-secondary btn-lg btn-tile storageType-3" data-bind="click: e, class: 'storageType-' + d.type">
        <i class="fas fa-ramp-loading" data-bind="class: d.icon"></i>
        <span data-bind="text: d.desc1"></span>
        <span data-bind="text: d.desc2" style=" /* Experimental */
            transform: scaleX(.8);
            white-space: nowrap;
        ">This text is way too long</span>
    </button>
</div>

If the texts desc1 or desc2 are too long, a scaleX should be applied, so that they fit inside the fixed size button.

Is this even possible with pure CSS, or do I need to iterate over the tiles with Javascript, read the actual widths and calculate the scaling factor like that?

I'm using knockout binding, by the way.

Edit:

I did some experimenting, but I still can't calculate the correct scaling factor. The correct factor should be somewhere around .3 for a longer text. It returns 0.477, though.

1 / $('.btn-tile').first().children().last()[0].scrollWidth * ($('.btn-tile')[0].offsetWidth)

Solution

  • The idea is to use the difference in scrollWidth and clientWidth in overflowing elements to compute the ideal scaling that would fit the entire parent element, and apply that as a scaleX transform to the overflowing element.

    Don't forget to set the transform origin to the left side of the object, since it is overflowing on the right.

    Unfortunately I don't think you can do this in pure CSS as you need to compute the scrollWidth and clientWidth. But here is a minimal example of how you can do this:

    els = document.querySelectorAll(".possibly-scaled");
    for (let el of els) {
        let xScale = el.clientWidth / el.scrollWidth;
        if (xScale < 1) { 
            el.style.transform = "scaleX(" + xScale + ")";
        }
    }
    .fixed-width {
        font-size: 30px;
        margin: 8px;
        padding: 4px;
        text-align: center;
        background-color: yellow;
    
        /* necessary styling */
        width: 80px;
        white-space: nowrap; 
    }
    
    .possibly-scaled {
        /* necessary styling */
        display: block;
        transform-origin: 0 0;
    }
    <div class="fixed-width">
        <span class="possibly-scaled">OK</span>
    </div>
    <div class="fixed-width">
        <span class="possibly-scaled">Good</span>
    </div>
    <div class="fixed-width">
        <span class="possibly-scaled">Fantastic!</span>
    </div>
    <div class="fixed-width">
        <span class="possibly-scaled">Amazingly, this also fits.</span>
    </div>