jQuery / .animate() — Create Custom Animation Effects
.animate() changes CSS properties from current values to targets over time. For example, .animate({ left: 100, width: 160, opacity: 0.4 }) changes position, width and opacity together. Duration and easing control the transition; completion runs once per selected element.
Choose properties and timing
The first .animate() argument is a target-property object. Numeric left and width use pixels here; opacity is unitless from zero to one. Properties in one object change together, unlike separate calls that enter the default effects queue.
left requires positioning other than static to move visibly. relative here offsets from the original place, not document x=100. width measures content; borders and padding can alter the outer extent, so the demo uses a simple content-box.
duration is milliseconds, default 400. Easing changes progress speed; compare built-in swing and linear with identical targets. Do not assume core jQuery interpolates arbitrary color or transform strings; check supported numeric properties.
Complete two-element example
Save everything as UTF-8 animate-demo.html and open it in a browser. Internet loads the official full jQuery file 4.0.0. Timed animation requires effects, so use full rather than slim. No server or installation is needed. Intermediate values, targets, stop and replay were checked in Windows Edge with 3.7.1 and 4.0.0.
Both boxes have the same targets. Start restores known initial values each time. Reset preserves duration/easing choices while restoring boxes and counters. Busy playback prevents a duplicate run.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Animate CSS properties and observe completion</title>
<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<style>
body { font: 16px/1.6 sans-serif; max-width: 760px; margin: 20px; }
label { display: block; margin: 8px 0; }
select, button { font: inherit; margin: 4px; }
#stage { background: #eef4f4; padding: 10px; overflow-x: auto; }
.box { position: relative; left: 0; width: 80px; height: 45px; margin: 12px 0; background: teal; color: white; box-sizing: content-box; }
pre { white-space: pre-wrap; overflow-wrap: anywhere; }
</style>
</head>
<body>
<label>Duration <select id="duration"><option value="600">600ms</option><option value="1200">1200ms</option><option value="0">0ms</option></select></label>
<label>Easing <select id="easing"><option value="swing">swing</option><option value="linear">linear</option></select></label>
<button type="button" id="start">Start</button>
<button type="button" id="stop">Stop here</button>
<button type="button" id="reset">Reset</button>
<div id="stage"><div class="box" id="alpha">Alpha</div><div class="box" id="beta">Beta</div></div>
<pre id="result" aria-live="polite"></pre>
<script>
jQuery(function ($) {
const $boxes = $("#stage > .box");
let generation = 0, busy = false, state = "ready", completed = 0, batches = 0;
function report() {
const values = $boxes.map(function () {
const $box = $(this);
return this.id + ": left=" + Math.round(parseFloat($box.css("left"))) + "; width=" + Math.round($box.width()) + "; opacity=" + Number(parseFloat($box.css("opacity")).toFixed(2));
}).get();
$("#result").text(["state=" + state, "completed=" + completed + "; batches=" + batches, ...values].join("\n"));
}
function reset() {
generation++; $boxes.stop(true, false).css({ left: 0, width: 80, opacity: 1 });
busy = false; state = "ready"; completed = batches = 0;
$("#start").prop("disabled", false); report();
}
$("#start").on("click", function () {
if (busy) return;
reset(); busy = true; state = "running"; $(this).prop("disabled", true);
const run = generation;
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const duration = reduce ? 0 : Number($("#duration").val());
$boxes.animate({ left: 100, width: 160, opacity: 0.4 }, {
duration: duration, easing: $("#easing").val(),
complete: function () { if (run === generation) { completed++; report(); } }
});
$boxes.promise("fx").done(function () {
if (run !== generation) return;
batches++; busy = false; state = "done";
$("#start").prop("disabled", false); report();
});
report();
});
$("#stop").on("click", function () {
if (!busy) return;
generation++; $boxes.stop(true, false);
busy = false; state = "stopped"; $("#start").prop("disabled", false); report();
});
$("#reset").on("click", reset);
reset();
});
</script>
</body>
</html>
Initial output: width 80, relative left zero, opacity one. Display rounding does not restrict actual animation to integers.
state=ready
completed=0; batches=0
alpha: left=0; width=80; opacity=1
beta: left=0; width=80; opacity=1
- Start with 600ms and swing. Both boxes change three properties together.
- At completion: state=done, completed=2; batches=1. Each box is left=100; width=160; opacity=0.4.
- Reset, change only easing to linear and compare. Targets match; intermediate speed differs. Restore swing if desired.
- Choose 1200ms, start and press Stop here. It stays at intermediate values with state=stopped, without later moving to done.
- Reset and replay. Duration 0 immediately reaches targets and completion counts.
- With reduced motion set to reduce, even the 1200ms choice finishes immediately.
Individual versus collection completion
complete runs twice here, once per box; this inside it is the corresponding DOM element. The first callback is not completion of the entire collection. Calling .animate() does not block JavaScript until animation finishes.
.promise("fx") observes the collection’s effects work. Here it increments batches once after the boxes finish and enables Start. Additional queued default effects can extend what it waits for.
An empty effects queue is not always proof of natural completion. .stop(true, false) can clear queues and resolve the fx promise even though targets were not reached. Stop and Reset first change generation so old callbacks cannot overwrite the canceled state with done.
Repeated effect calls normally queue. This example disables Start and checks busy. An interface that redirects toward a new target from the current position needs a different explicit interruption policy.
Stopping, recovery and actual layout
Stop here uses jumpToEnd=false, preserving current values. Reset then specifies left, width and opacity. Keeping old intermediate values would change the conditions for the next comparison.
prefers-reduced-motion reduce selects duration zero for this run and retains the same target/completion path. Global jQuery effect settings remain untouched.
Position and width changes can affect surrounding layout and overlap. Check narrow screens, zoom, long text and scrolling containers. Opacity zero does not automatically mean display:none or hidden accessibility state; design those separately.
Avoid having CSS transitions or another plugin control the same properties simultaneously. Define ownership of elements, properties and queues, and scope selectors and events to the feature.




