Effects wait in line
Each element has an animation queue. When you start an effect while another is running on the same element, the new one waits its turn. Chaining effects therefore plays them in order:
<div id="box" style="width: 120px; height: 50px; background: #fb7185"></div>
<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script>
const start = Date.now();
const at = label => console.log(label, "after about", Math.round((Date.now() - start) / 100) * 100, "ms");
$("#box")
.slideUp(200, () => at("1. slid up"))
.slideDown(200, () => at("2. slid down"))
.animate({ width: 240 }, 200, () => at("3. widened"));
at("0. chain set up");
</script>0. chain set up after about 0 ms
1. slid up after about 200 ms
2. slid down after about 400 ms
3. widened after about 600 msThe whole chain was set up instantly (step 0), and then each effect ran when the previous one finished, about 200ms apart. Effects on different elements do not share a queue, so they run at the same time.
Pausing with .delay()
.delay(ms) adds a pause to the queue:
<p id="toast" style="display: none; background: #1e293b; color: white; padding: 8px">Saved</p>
<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script>
$("#toast")
.fadeIn(150)
.delay(500)
.fadeOut(150, function () {
console.log("toast shown, waited, and hidden; display =", $(this).css("display"));
});
</script>SavedThat is a complete "toast" notification in three lines: appear, wait, disappear.
.delay() only delays queued effects. Instant methods such as .css(), .text() or .addClass() do not wait in the queue; they run straight away:
<p id="msg">Hello</p>
<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script>
$("#msg").fadeOut(200).delay(300).css("color", "red");
console.log("colour straight away:", $("#msg").css("color"));
// To wait, put instant changes in the queue
$("#msg").delay(300).queue(function (next) {
$(this).text("Changed after the queue");
console.log("queued change ran");
next();
}).fadeIn(200);
</script>colour straight away: rgb(255, 0, 0).queue(fn) adds your own function to the queue. Call the next function it receives, or the queue stops there.
Stopping runaway animations
Queues cause a classic bug. Hover over a menu many times quickly, and each hover adds another slide to the queue, so the menu keeps opening and closing long after the pointer has gone:
<button id="bad">Toggle A (queues)</button> <button id="good">Toggle B (stops first)</button>
<button id="fast">Click both four times quickly</button>
<div id="a" style="height: 40px; background: #93c5fd">Panel A</div>
<div id="b" style="height: 40px; background: #86efac">Panel B</div>
<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script>
$("#bad").on("click", () => $("#a").slideToggle(300));
$("#good").on("click", () => $("#b").stop(true).slideToggle(300));
$("#fast").on("click", function () {
for (let i = 0; i < 4; i++) { $("#bad").trigger("click"); $("#good").trigger("click"); }
console.log("A queue:", $("#a").queue().length, "| B queue:", $("#b").queue().length);
});
</script>A queue: 4 | B queue: 1After four quick clicks, Panel A has four slides in its queue (the running one plus three waiting) and will keep moving for over a second. Panel B only ever has the latest one, because .stop(true) cleared the queue before each new slide. Click the first two buttons quickly yourself in the editor to feel the difference.
The forms of stopping:
| Call | Effect |
|---|---|
.stop() |
stop the current animation where it is; the next queued one starts |
.stop(true) |
stop the current one and clear the queue |
.stop(true, true) |
as above, but jump the current one to its end values |
.finish() |
stop everything and jump all queued animations to their end values |
.stop(true) before starting an effect in a hover or click handler is the standard fix.
Waiting for everything: .promise()
A callback runs once per element. If you fade out ten items and want to do something once all have finished, use .promise():
<ul><li>One</li><li>Two</li><li>Three</li></ul>
<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script>
let callbacks = 0;
$("li").fadeOut(200, () => callbacks++)
.promise()
.then(function () {
console.log("per-element callbacks:", callbacks);
console.log("all", this.length, "items have faded; now update the counter once");
});
</script>per-element callbacks: 3
all 3 items have faded; now update the counter onceThe callback ran three times, but the .then() function ran once, with this set to the whole selection. The Ajax section uses the same promise idea for requests.
Running effects in parallel
Two effects on the same element normally queue. To run an .animate() at the same time as another, pass queue: false:
$("#card")
.slideDown(300)
.animate({ opacity: 1 }, { duration: 300, queue: false });Common mistakes
- Expecting
.delay()to delay.css()or.text(). - Starting effects from hover or click without
.stop(true), so they pile up. - Using a per-element callback for code that should run once.
- Forgetting
next()in.queue(), which freezes later effects on that element.
Exercise
Make a "Save" button that shows a message, waits 400ms and fades the message out. Repeated clicks must not queue up several messages: each click should restart the sequence.
Show one possible answer
<button id="save">Save</button>
<p id="saved" style="display: none">Saved!</p>
<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script>
$("#save").on("click", function () {
$("#saved").stop(true, true).show().delay(400).fadeOut(200);
console.log("queued effects:", $("#saved").queue().length);
});
</script>queued effects: 2
queued effects: 2
queued effects: 2.stop(true, true) clears any old sequence and finishes the current step, so each click starts fresh.
Key points
- Effects on the same element wait in a queue and run one after another; chaining effects plays them in order.
- .delay(ms) pauses the queue; it only affects queued effects, not .css() or other instant methods.
- .stop() halts the current animation; .stop(true) also clears the queue; .finish() jumps everything to the end.
- .promise().then(fn) runs fn once every selected element has finished all its animations.