skip to Main Content

I have a widget on my page, which is a span element, styled as follows:

span.widget
{
        line-height: 0;
        font-size: 150%;
        transition: font-size 1s;
}

I have JavaScript which modifies its font-size:

var widget=document.getElementById('the_widget');
widget.style.fontSize='200%';

If I do this, the font size scales up continuously over 1 second, and it works great.

What I want to achieve, however, is for the font size to jump up instantly, and then scale down smoothly. I’ve tried the following code:

widget.style.transition='none';
widget.style.fontSize='200%';
widget.style.transition='font-size 1s';
widget.style.fontSize='150%';

However this does not work because the browser does not reflow the element after the first resize, so the second resize just leaves it unchanged and there is no animation and no sign that anything changed.

I’ve also thought of similar things with other css size transitions like height and width, but run into the same problem.

3

Answers


  1. Chosen as BEST ANSWER

    So, after typing out this question, I actually stumbled upon a workaround or hack that might be a sustainable solution even if it is a bit arcane; I discovered this question:

    Force browser to trigger reflow while changing CSS

    which says you can use JavaScript to force a reflow, using the offsetHeight attribute. If you time it

    I found this works in Chrome and Firefox, have not tested in any other browsers yet:

    widget.style.transition='none';
    widget.style.fontSize='200%';
    void(widget.offsetHeight);
    widget.style.transition='font-size 1s';
    widget.style.fontSize='150%';
    

    I think this works because passing an expression to void forces the evaluation of that expression which in this case forces a reflow.

    There might be a more elegant or concise way to do this by just changing CSS classes and then attaching changes in both the font-size and transition elements to the respective classes.


  2. A little bit javascript is needed.

    Try this:

    div {
        width: fit-content;
        margin: 5vh auto;
    }
    span#widget {
        font-size: 150%;
        color: red;
    }
    span.animate {
        animation: stayingalive 1.5s ease-out forwards;
    }
    @keyframes stayingalive {
    
        0%, 30%, 100% {
            font-size: 150%;
        }
        15% {
            font-size: 210%;
        }
        50% {
            font-size: 250%;
        }
    }
    <div>
        <p>
            <button onclick="document.getElementById('widget').setAttribute('class', 'animate')">
                Simulate programmatical trigger: add class 'animate'
            </button>
        </p>
        <p>
            <button onclick="document.getElementById('widget').setAttribute('class', '')">
                (Reset: remove classes)
            </button>
        </p>
    </div>
    <div>
        <span id="widget">Uh uh uh uh &#9886; &#10084; &#9887; staying alive!</span>
    </div>
    Login or Signup to reply.
  3. You can use either CSS transitions or animations. I’ll show examples of how to use both below.

    In this case I recommend using an animation over a transition. It seems more straight-forward and easier to implement.

    Animations

    Technically, animations can do everything that transitions can. They can easily be reused on multiple elements.

    But ‘classic’ CSS animations have rather ‘rigid’ keyframes as a trade-off: (Apart from using custom properties) values in keyframes cannot be set for each element individually.

    Additionally, each CSS animation requires a unique name. With many animations, this may be difficult to realize in an organized way.

    You can also animate with the Web Animations API (more in the section below). Such JavaScript animations do not come with the above limitations.

    CSS Animation

    A property’s declared value will be the implied initial or final value in an animation. But the initial and final values can also be explicitely declared in the from (or 0%) and to (or 100%) keyframes, respectively.

    By declaring font-size: 150% in the element’s rule, 150% will be the implied value ‘from’ and ‘to’ value in an animation. Since we want to animate from value 200% to 150%, we can:

    • Explicitely declare the final value:
      @keyframes animation-name {
        from {font-size: 200%}
        to {font-size: 150%}
      }
      
    • Use the implicit final value:
      @keyframes animation-name {
        from {font-size: 200%}
        /* 'To font-size:150%' is implied for elements with `font-size:150%` */
      }
      

    Example with implied final value:

    @keyframes from-doubled-font {
      from {font-size: 200%}
    }
    p {
      font-size: 150%;
      animation: from-doubled-font 1s linear;
    }
    <p>This is some text!</p>

    Web Animation

    We can also animate with the Web Animations API in JavaScript. As an API for JavaScript, it allows animations to be more dynamic (in terms of keyframes, property values and animation options).

    Using the Web Animations API may be more verbose, but is more readable than the short-hand animation property due to explicitely naming all properties.

    When passing an array containing only a single keyframe, that keyframe will be the to keyframe. This means we have to pass at least two keyframes to declare the ‘from’ properties.

    Example of using the Web Animations API:

    const p = document.querySelector("p");
    
    const from = {fontSize: "200%"};
    const to = {}; // Use implicit final value
    const keyframes = [from, to];
    
    const options = {
      duration: 1000, // ms
      easing: "linear"
    };
    
    p.animate(keyframes, options);
    p {font-size: 150%}
    <p>This is some text!</p>

    Transitions

    Transitions allow transitioning from one value to another (one change). This limits their use cases but also makes them easy to declare, even on multiple elements.

    Since we want font-size to change from the initial 150% to 200% and then transition to 150% again (which is two changes), we cannot simply use a transition.

    Instead, we will have to use JavaScript and cause a reflow, so that the intermediate 200% will be seen as the ‘transition-from’ value.

    Causing immediate reflows

    We require a reflow before declaring the ‘transition-to’ value. There are multiple causes for reflowing, some of which we can cause immediately ourselves (e.g. by reading offsetHeight).

    By declaring font-size: 200% and then cause an immediate reflow, we basically ‘commit’ the 200% as the ‘current state’. After that, we can start the transition by declaring the transition and redeclaring font-size, which will be the ‘transition-to’ value.

    Example:

    const p = document.querySelector("p");
    
    p.style.transition='none';
    p.style.fontSize='200%';
    
    reflow();
    
    p.style.transition='font-size 1s linear';
    p.style.fontSize='150%';
    
    function reflow() {
      // Example implementation:
      document.body.clientHeight;
    }
    p {font-size: 150%}
    <p>This is some text!</p>

    Waiting for reflows

    Instead of causing immediate reflows, we can also wait for them. We can cause a reflow to happen e.g. by changing styles.

    Repainting is always preceded by a reflow. By waiting for a repaint, we can wait for a reflow. We can do this with the requestAnimationFrame() function, whose callback will be called right before a repainting’s reflow.

    Calling requestAnimationFrame() from within a callback to requestAnimationFrame() will wait for the subsequent repaint’s reflow.

    This means we can write a helper function to wait for frames, e.g.:

    function waitFrames(n, callback) {
      if (n > 0) {
        requestAnimationFrame(() => waitFrames(n - 1, callback));
      } else {
        requestAnimationFrame(callback);
      }
    }
    

    Example transition after waiting for a reflow:

    const p = document.querySelector("p");
    
    p.style.fontSize = "200%";
    
    waitFrames(1, function afterWaiting() {
      p.style.transition = "font-size 1s linear";
      p.style.fontSize = "150%";
    });
    
    function waitFrames(n, callback) {
      if (n > 0) {
        requestAnimationFrame(() => waitFrames(n - 1, callback));
      } else {
        requestAnimationFrame(callback);
      }
    }
    p {font-size: 150%}
    <p>This is some text!</p>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search