1

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 or 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.

Rutik Patel
  • 183
  • 3
  • 15
cazort
  • 516
  • 6
  • 19
  • "However (...) there is no animation"; have you tried using an _animation_ instead of a transition? – Oskar Grosser Apr 18 '23 at 14:38
  • @OskarGrosser No, I haven't tried that yet. Animations seem a bit more complex and I haven't learned them yet. If it's possible to do it concisely with them though, that might be a good solution. I've stumbled upon a hack or workaround since posting the question though, which seems to achieve my desired goal. – cazort Apr 18 '23 at 14:45

3 Answers3

1

A little bit of 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>

So - let's break down the original question: "How can I use CSS transitions and JavaScript to make an element jump up in size instantly, then scale down smoothly?"

1. How can I use CSS transitions

You would use animation, since with the attached @keyframes you can give your animation any desired curve. animation has a bunch of properties, but regarding this question, two are of interest:

  • animation-iteration-count: how often shall the animation run? Default = 1, so it is not mentioned in the above code example. But it could also be a number or infinite.
  • animation-fill-mode: shall the animation have an impact on the concerned element outside the animation? forwards makes the value of the last keyframe persisting.

2. to make an element jump up in size instantly, then scale down smoothly

Scale up or down, quickly or smoothly, first this then that: this can be defined by @keyframes: they represent the timeline of the animation. If you have a simple move, you can use from (or 0%) and to (or 100%). If you have a more sophisticated move, you can use some or many percentages. In the code example I simulate a heart-bump ("bump-BUMP") with the (timeline-) steps 0%-15%-30%-50%-100%. And since 0%, 30% and 100% have the same value, I can put them together. For a better understanding: the 1st (smaller) "bump" has 30% of the whole animation-time and the lower value than the 2nd "BUMP", that has 70% of the animation-time and a highter value. As you see, from 30% to 50% the latter got 20% of the time to bump-up and from 50% to 100% it's got 50% of the time to bump-down.

3. and JavaScript

This is the funny part of it all. The animation is linked to a class by CSS (in the code example: animate but it could be hugo or johndoe as well as any other name). But the concerned element (<span>) does not have this class! Well, we add the class to the element with a single command, attached to the 1st button (which only simulates any event): document.getElementById('widget').setAttribute('class', 'animate'). Now the element has the class and the animation starts! It runs one time. If you want it to run again later, you have to remove the class (code of the 2nd button). Of course you can define any other behavior, but this one is connected to the question.

NicolasK
  • 31
  • 5
  • This is quite nice, I need it to do this only once, however, not repeatedly, so it's not immediately apparent without extra work, how to fit this into what I want to do. The context is that it's triggered by an AJAX callback, the user clicked a control and I want to emphasize some text visually, when the request is fulfilled. I.e. I could use Javascript to change a class that is styled in this way, but how do I get it to only do this once? Would it be sufficient to set `animation-iteration-count` in place of `infinite`? – cazort Apr 18 '23 at 17:31
  • 1
    In place of `infinite` you would use `forwards` which means that the value of the end of the animation shall persist. To simulate the a/m behavior I change the code example a little bit - please take another look. I changed the 'widget' from being a class to be an id to get a single instance with `getElementById`. – NicolasK Apr 18 '23 at 18:29
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Apr 21 '23 at 14:11
  • Ok -- big edit for better explanation above.. – NicolasK Apr 25 '23 at 22:45
1

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>
Oskar Grosser
  • 2,804
  • 1
  • 7
  • 18
0

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.

cazort
  • 516
  • 6
  • 19