DEV Community


CSS transitions 101: let's animate a toggle button icon

bnevilleoneill profile image Brian Neville-O'Neill Originally published at on ・12 min read

CSS transitions 101: let’s animate a toggle button icon

The role of web animation goes well beyond that of being a mere piece of decoration. You can use it to guide web visitors’ attention, to organize information and make it easier to digest, to make waiting for content to load feel snappier and more entertaining, and more.

The good news is that with CSS transitions in your front-end developer’s toolkit you’ll be able to add some flair and improve user experience on the web in as little as a line of code.

Here, you’ll learn when CSS transitions make a good choice for your project and how you can implement them to spruce up your website. At the end of this article, you’ll have created a morphing animation on a toggle button using CSS transitions.

Let’s begin!

CSS transitions or keyframe animations?

You can add smooth motion effects with CSS alone, no JavaScript necessary, either with CSS transitions or CSS keyframe animations. They’re both efficient in terms of performance, especially if you animate opacity and transformproperties, which browsers are very good at optimizing.


Transitions let you change the value of a property from its starting state to an end state in response to an event, e.g., mouseenter, mouseout, click, etc. This means that, if your animation has only these two states, CSS transitions will be the best and simplest tool at your disposal. Common use cases for transitions include sliding an off-canvas sidebar in and out on hover or mouse click, changing link or button colors on hover, fading a dialog in or out in response to a button click, etc.

One more advantage of using transitions is graceful degradation : if an error occurs or a browser doesn’t support them, the worst that can happen is that the element will change its state abruptly rather than gradually.


If you plan on creating animations which have more than a start and an end state and you’d like to have more control on what happens in all the in-between states, then CSS keyframes will be a better fit for your project.

Use cases for keyframe animations include loaders, which start playing as soon as the page loads and keep playing for an indefinite period of time until the requested page resource is ready to be displayed on the screen.

The Transition property

You create transitions with the CSS transition property:

.selector {
  transition: property duration transition-timing-function delay;

The code above is the shorthand version of these four properties:

.selector {
  transition-property: color;
  transition-duration: 0.5s;
  transition-timing-function: ease-in;
  transition-delay: 0.3s;


The value of the transition-property is the CSS property you wish to apply the transition to. This could be any of the CSS animatable properties like color, height, width, etc.

Not all CSS properties can be transitioned, but a good many are, in particular those that are expressed by numerical values. These can have a starting state and an end state which can be easily interpolated with in-between numbers expressing corresponding in-between states. You can’t transition the value auto.

If you plan on transitioning more than a couple of properties, I think using the keyword all , which applies the transition to all the animatable properties used to style the selected element, would be preferable.


The transition-duration property expresses the time it takes to transition a CSS property from the start value to the end value. You can express values in seconds (1s) or milliseconds (1000ms).


The transition-timing-functionproperty is crucial if you like your transition to feel natural and smooth. It has a cubic-bezier as value to express the rate of change in the animation. To keep things simple, you can use the following keywords:

  • ease : default value. It starts slow, then speeds up, then slows down, and finally ends very slowly:

  • linear : the rate of change remains constant:

  • ease-in : it starts slow, then picks up speed:

  • ease-out : it starts fast, then slows down:

  • ease-in-out : it starts slow, it’s faster in the middle, and slows down towards the end. It’s similar to ease , but not as slow at the end:

Alternatively, you can create your custom cubic bezier, which you can quickly build using a tool like


The transition-delay property expresses the time you want to wait before starting the duration. You can express this value either in seconds or in milliseconds, just like the duration property.

In this tutorial, you’ll be using the shorthand property.

Remember that the duration is the only required value, all other properties have defaults. The transition-property defaults to all , the transition-timing-function to ease , and the transition-delay to 0s . If you add two values for both duration and delay, the browser interprets the first one as transition-duration and the second one as transition-delay, so the order in which you add these values is super important.

Morphing the toggle button icon

Now it’s time to see some CSS transitions in action, let’s get into coding mode!

The goal will be to morph the icon on a toggle button from its hamburger shape into an X shape. Here’s what the result will look like:

An overview of the code

The HTML includes a simple button and a span element. The icon is made of the span together with two pseudo-elements before and after the span, which appropriately styled look like a hamburger icon. Here’s the relevant HTML code:

<button class="hamburger\_\_button">
  Menu <span class="hamburger\_\_icon"></span>

Now the CSS. Here are the default styles to render the hamburger icon (just the relevant rules):

/\* give the span element and related pseudo-elements the appearance of white lines \*/

.hamburger\_\_icon::after {
  position: absolute;
  width: 44px; 
  height: 4px;
  border-radius: 4px;
  background-color: #fff;

/\* set the span element in the middle of its containing div \*/
.hamburger\_\_icon {
  top: calc(50% - 2px);
  left: calc(50% - 22px);

/\* set the content property and left position of the two pseudo-elements\*/
.hamburger\_\_icon::after {
  content: "";
  left: 0;

/\* set the bottom property of the before pseudo-element \*/
.hamburger\_\_icon::before {
  bottom: 12px;

/\* set the top property of the after pseudo-element \*/
.hamburger\_\_icon::after {
  top: 12px;

The snippet above creates the three lines typical of the hamburger icon appearance.

Next, it’s time for the hover styles. When users hover over the button, the span and the pseudo-elements will be rotated (using the CSS transform rotate() function) and their background color, position, height and width will change to take up the shape of a typical close icon. Here’s the code:


/\* increase width and height of span element, \*/
/\* recalculate top and left position, rotate it and change background-color \*/
.hamburger\_\_button:hover .hamburger\_\_icon {
  height: 10px;
  width: 110px;
  left: 5px;
  top: calc(50% - 4px);
  transform: rotate(-45deg);
  background-color: #e20650;

/\* adjust properties on the after pseudo element \*/
.hamburger\_\_button:hover .hamburger\_\_icon::after {
  width: 110px;
  height: 10px;
  top: -1px;
  transform: rotate(-270deg);
  background-color: #e20650;

/\* hide the before pseudo-element by scaling it to 0 \*/
.hamburger\_\_button:hover .hamburger\_\_icon::before {
  transform: scale(0);

If you hover on the button now, you’ll see the hamburger icon immediately and abruptly morph into a close icon. Adding CSS transitions will achieve the gradual morphing effect we’re after.

The CSS transition code

If you add your transitions to the hover state, these will be applied when users hover over the button, but not when the mouse leaves the button. In other words, the hamburger icon will morph into the close icon gradually and smoothly, but the close icon will abruptly snap back into the hamburger icon. To ensure the animation effect takes place equally on mouse in and mouse out , you need to add the transition code to the global default styles, not the hover styles. Here’s how:

/\* this is one of the lines of the close icon \*/
.hamburger\_\_icon {
  /\* preceding code stays the same \*/
  transition: all 0.3s linear;

/\* this is the other line of the close icon \*/
  .hamburger\_\_icon::after {
  /\* preceding code stays the same \*/
  transition: all 0.3s linear;

In a single line of code, you’ve told the browser to apply a transition to all the animatable properties on the element over a period of 0.3 seconds without any variation in the rate of change (using the linear timing function).

Instead of using the keyword all , you could have listed each CSS property name. For instance:

.hamburger\_\_button:hover .hamburger\_\_icon {
  /\* preceding code stays the same \*/

  transition: height 0.3s linear,
              width 0.3s linear,
              left 0.3s linear,
              top 0.3s linear,
              transform 0.3s linear,
              background-color 0.3s linear;

Spelling out each single property is useful when you plan on applying a transition to just a few properties on a selector, or if you want to specify variations in any of the properties like different durations or timing functions. In fact, using this technique can improve code efficiency and performance. However, if you still need to animate quite a bit of properties, doing it like this can be verbose, repetitive and error-prone.

Test your code: the morphing effect on button hover should now look smooth and pleasant to interact with. Also, have a go at experimenting with different transition timing functions, try out different duration values, or why not, add a few milliseconds’ delay and see what happens.

Here’s the full code in action:

CSS transitions and JavaScript

When transitioning elements, you’re not limited to hover events. You can tie your transitions to a CSS class and then leverage JavaScript to add that CSS class to the element you want to animate.

In other words, the CSS will deal with the appearance, that is, the animation effect, and the JavaScript with the behavior or DOM manipulation by adding and removing a CSS class dynamically.

To illustrate this, here’s the same morphing effect as above, but executed on a button click.

The first step is to replace all instances of hover styles you wrote earlier with a class name of your choice. I called my class toggled. Here’s the relevant snippet:

.toggled.hamburger\_\_button .hamburger\_\_icon {
  height: 10px;
  width: 110px;
  left: 5px;
  top: calc(50% - 4px);
  transform: rotate(-45deg);
  background-color: #e20650;

.toggled.hamburger\_\_button .hamburger\_\_icon::after {
  width: 110px;
  height: 10px;
  top: -1px;
  transform: rotate(-270deg);
  background-color: #e20650;

.toggled.hamburger\_\_button .hamburger\_\_icon::before {
  transform: scale(0);

With the code above, each time the toggled class is added to the button, the icon morphs into a close icon. By the same token, when the toggled class is removed, the icon morphs back into a hamburger icon.

The JavaScript code has the single task of toggling the .toggled class on button click:

hamburgerButton.addEventListener( "click", () => hamburgerButton.classList.toggle("toggled") );

And that’s all you need. Test your code, the morphing effect should look exactly like the hover example you created earlier:

Applying CSS transitions to a dynamically created HTML element

The above example works fine right out of the box because transitions are applied to static DOM elements, that is, elements which are hard-coded in the HTML source. However, if you create your element dynamically and append it to the DOM using JavaScript, applying a class which should trigger a transition won’t achieve the expected result. Once the browser renders the element, the transition effect has ended and the animation is lost.

Here’s what I mean. Let’s say that when you click a button on the page, a box slides in from the top. The slide-in effect is achieved with the same technique you used above: a class is dynamically added to trigger a CSS transition while the transition property is applied to the box element:

.box {
  /\* more code above, transition below \*/
  transition: 1s;

What is different this time is that, instead of having the box already present in your HTML document, you create it on the fly with JavaScript.

The JavaScript code looks something like this:

// add event listener to the button element
button.addEventListener('click', () => {
  // create the box element with a class of .box
  const box = document.createElement("div");

  // append the new element box to the DOM

  // add the class that triggers the transition to the box element

Have a look at the working demo on CodePen:

When you click the button, the box will materialize onto the page immediately without animation.

To solve this problem, you’ll need to add the transition-triggering class after a tiny bit of time has elapsed from the creation of the new element. To do this, you can use the .setTimeout() method, or better still the .requestAnimationFrame() method. Since .requestAnimationFrame() is especially designed for web animation, this is the method I’m going to use to fix this demo.

As a first step, you need to write the function where you add the transition-triggering class to your element and call .requestAnimationFrame():

const doTransition = () => {
  // add the class that triggers the transition to the box element

  // call requestAnimationFrame passing this same function as argument

The next step is to call .requestAnimationFrame() inside the button click handler, once again passing in the doTransition() function as argument.

Add this bit after the code that appends the box to the DOM:

// call requestAnimationFrame passing the doTransition function as argument

And you’re done! Have a look at the working demo to see the difference:

Cool Resources

To dive deeper into CSS transitions, I’ve listed a few useful articles you can check out:


In this introduction to CSS transitions I’ve discussed when it’s appropriate to use transitions over CSS keyframe animations and how you can use transitions to morph a hamburger icon on a toggle button, both on hover and on click using JavaScript.

If you have interesting uses of CSS transitions you’d like to share, I’d love to hear from you.

Plug: LogRocket, a DVR for web apps

LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single page apps.

Try it for free.

Discussion (0)

Editor guide