Himanshu SharmaSoftware engineer
Let’s talk

jQuery · Lesson 33 of 45

CSS transitions driven by jQuery classes

Combine CSS transitions with jQuery class toggles for smooth effects, wait for transitionend, animate colours and transforms, and keep effects accessible.

11 minEffects and animationUpdated 23 October 2026

By the end of this lessonBuild smooth effects by letting CSS animate and jQuery switch classes, run code when a transition ends, and respect reduced-motion settings.

Let CSS do the animating

jQuery's effects work by changing a style many times a second from JavaScript. Modern browsers have a better tool: CSS transitions. You describe the two states in CSS, and the browser animates between them, smoothly and often on the graphics hardware.

jQuery's job becomes tiny: add or remove a class.

html
<style>
  .heart {
    display: inline-block; font-size: 28px; color: #94a3b8;
    transition: color .3s ease, transform .3s ease;
  }
  .heart.liked { color: #e11d48; transform: scale(1.3); }
</style>
<button id="like" aria-pressed="false"><span class="heart" aria-hidden="true">♥</span> Like</button>

<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script>
  $("#like").on("click", function () {
    const liked = $(this).find(".heart").toggleClass("liked").hasClass("liked");
    $(this).attr("aria-pressed", liked);
    console.log("liked:", liked);
  });
</script>
result
liked: true

This animates a colour and a transform (scale), neither of which .animate() can do. The JavaScript only toggles a class and updates aria-pressed, the attribute that tells assistive technology a toggle button is on.

Why this is better

  • Smoother. The browser can run the animation without waiting for JavaScript on every frame.
  • More properties. Colours, shadows, transforms, filters: anything CSS can transition.
  • Design in one place. The look and timing live in the stylesheet, where designers can change them.
  • Less code. No durations, easings or callbacks in JavaScript.

Showing and hiding with transitions

display cannot be transitioned, so to fade a panel in and out with CSS, transition opacity and visibility instead:

html
<style>
  .drawer {
    opacity: 0; visibility: hidden; transform: translateY(-8px);
    transition: opacity .2s, transform .2s, visibility .2s;
    background: #f1f5f9; padding: 10px;
  }
  .drawer.is-open { opacity: 1; visibility: visible; transform: none; }
</style>
<button id="open" aria-expanded="false" aria-controls="drawer">Account menu</button>
<div id="drawer" class="drawer">Profile · Orders · Sign out</div>

<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script>
  $("#open").on("click", function () {
    const open = $("#drawer").toggleClass("is-open").hasClass("is-open");
    $(this).attr("aria-expanded", open);
    console.log("drawer open:", open);
  });
</script>
result
drawer open: true

visibility: hidden keeps the hidden drawer out of the keyboard tab order and away from screen readers, which opacity: 0 alone would not.

Running code when a transition ends

.addClass() is instant and has no callback. To run code once the CSS transition has finished, listen for the browser's transitionend event:

html
<style>
  .alert { padding: 10px; background: #fee2e2; transition: opacity .25s, transform .25s; }
  .alert.leaving { opacity: 0; transform: translateX(30px); }
</style>
<div class="alert" id="alert">Payment failed. <button id="dismiss">Dismiss</button></div>

<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script>
  $("#dismiss").on("click", function () {
    $("#alert")
      .one("transitionend", function () {
        $(this).remove();
        console.log("removed after the transition");
      })
      .addClass("leaving");
  });
</script>
result
removed after the transition

Use .one() here, not .on(). The transition animates two properties (opacity and transform), and the browser fires transitionend once for each. With .one(), the code runs on the first and the handler removes itself.

If there is a chance the transition never runs (for example, the element is already hidden, or reduced motion turns transitions off), transitionend never fires. A safe pattern is to also set a timer slightly longer than the transition as a fallback.

Restarting a CSS animation

CSS @keyframes animations can be restarted by removing and re-adding their class. The browser needs to notice the removal, so read a layout property in between:

html
<style>
  @keyframes shake { 25% { transform: translateX(-6px); } 75% { transform: translateX(6px); } }
  .shake { animation: shake .3s; }
</style>
<input id="pw" type="password" placeholder="Password"> <button id="shake">Wrong password</button>

<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script>
  $("#shake").on("click", function () {
    const $pw = $("#pw").removeClass("shake");
    void $pw[0].offsetWidth;          // force the browser to apply the removal
    $pw.addClass("shake");
    console.log("shake class added:", $pw.hasClass("shake"));
  });
</script>
result
shake class added: true

Without the offsetWidth line, removing and adding the class in the same moment does nothing, because the browser never sees the class missing. A shake must not be the only error signal, so also show the error in text.

Respecting reduced motion

Wrap your transitions in a media query so that people who have asked their system for less motion get instant changes:

css
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
  }
}

A tiny duration (rather than none) keeps transitionend firing, so code that waits for it still runs. For jQuery effects, $.fx.off = true does the same job, as shown in the show and hide lesson.

When to use which

Use When
CSS transition + .toggleClass() hover-like states, menus, drawers, toggles, colour and transform changes
.fadeIn(), .slideDown() quick scripts, sliding content whose height you do not know, existing jQuery code
.animate() values calculated at run time, scroll positions, step-by-step updates

Common mistakes

  • Trying to transition display. Use opacity and visibility.
  • Using .on("transitionend") and running code once per property.
  • Relying only on transitionend with no fallback.
  • Animating width, height, top or left in CSS for frequent effects; transform and opacity are much smoother.

Exercise

Make a card that gets a shadow and moves up by 4px when it has the class raised, using a CSS transition. Toggle the class with a button, and update the button's aria-pressed attribute.

Show one possible answer
html
<style>
  .card { padding: 16px; background: white; border: 1px solid #e2e8f0; transition: box-shadow .2s, transform .2s; }
  .card.raised { box-shadow: 0 8px 20px rgba(0,0,0,.15); transform: translateY(-4px); }
</style>
<div class="card" id="card">A card</div>
<button id="raise" aria-pressed="false">Raise card</button>

<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script>
  $("#raise").on("click", function () {
    const raised = $("#card").toggleClass("raised").hasClass("raised");
    $(this).attr("aria-pressed", raised);
    console.log($(this).attr("aria-pressed"));
  });
</script>
result
true

Key points

  • Describe the start and end states in CSS with a transition; use jQuery only to add or remove the class.
  • CSS transitions can animate colours and transforms, which .animate() cannot, and they are smoother.
  • Listen for transitionend (with .one()) to run code when a CSS transition finishes.
  • Wrap motion in a prefers-reduced-motion media query so people who ask for less motion get instant changes.

Check yourself

1. In the class-toggle pattern, what does jQuery do?
2. Which can a CSS transition animate that .animate() cannot?
3. How do you run code when a CSS transition finishes?
4. Why use .one() rather than .on() for transitionend?