As long as you're using Bootstrap "default" styles, you're going to have problems animating changes of .btn
from btn-something
to btn-somethingElse
. Because Twitter Bootstrap uses background-image
property on top of background-color
to create those gradient shadows. And background-image
is not exactly animatable.
So you have two options:
- You have to remove the
background-image
property (see example below),
Example:
let interval = 300,
duration = 300;
$('#students-list .btn').each(function(i, e) {
$(e).css({
transitionDuration:interval+'ms'
}).addClass('removeBgImg');
setTimeout(function() {
$(e).removeClass('btn-primary').addClass('btn-danger');
setTimeout(function(){
$(e).removeClass('btn-danger').addClass('btn-primary');
}, duration + interval)
}, interval * i);
})
.btn {
transition-property: background-color;
transition-timing-function: linear
}
.btn.removeBgImg {
background-image:none;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<div id="students-list" class="container">
<div class="row">
<div class="col-xs-3">
<button class="btn btn-block btn-lg btn-primary viable"> John
Smith </button>
</div>
<div class="col-xs-3">
<button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
</div>
<div class="col-xs-3">
<button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
</div>
<div class="col-xs-3">
<button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
</div>
<div class="col-xs-3">
<button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
</div>
<div class="col-xs-3">
<button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
</div>
<div class="col-xs-3">
<button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
</div>
<div class="col-xs-3">
<button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
</div>
<div class="col-xs-3">
<button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
</div>
</div>
- ...or, if you can't/don't want to, just create a copy of your button positioned absolute on top of the old one, fade it in, animating
opacity
. If you're using JavaScript
to do it (which makes sense), take special care at cloning elements with IDs, because they issue some extremely hard to debug errors: proper order is:
- store element ID,
- remove ID from element,
- clone,
- add ID to clone,
- fade clone in,
- remove original.
Example:
let interval = 500,
duration = 100;
$('#students-list .btn').each(function(i, e) {
setTimeout(function() {
let clone = fadeCloneIn($(e), 'btn-primary', 'btn-danger');
setTimeout(function(){
fadeCloneIn(clone, 'btn-danger', 'btn-primary')
}, duration + interval)
}, interval * i);
})
function fadeCloneIn(el, from, to) {
let _t = this
_t.cloner = $('<div />', {style:'position:relative;'});
_t.id = el.attr('id') ? el.attr('id') : false;
_t.style = el.attr('style') ? el.attr('style') : false;
if (_t.id) el.removeAttr('id');
_t.clone = el.clone(true).removeClass(from).addClass(to).css({
opacity:0,
position:'absolute'
}).appendTo(_t.cloner);
if (_t.id) _t.clone.attr('id', _t.id);
_t.cloner.insertBefore(el);
_t.clone.animate({opacity:1}, interval, function(){
el.remove();
$(this).removeAttr('style').unwrap();
if (_t.style) $(this).attr('style', _t.style);
});
return _t.clone;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<div id="students-list" class="container">
<div class="row">
<div class="col-xs-3">
<button class="btn btn-block btn-lg btn-primary viable"> John
Smith </button>
</div>
<div class="col-xs-3">
<button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
</div>
<div class="col-xs-3">
<button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
</div>
<div class="col-xs-3">
<button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
</div>
<div class="col-xs-3">
<button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
</div>
<div class="col-xs-3">
<button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
</div>
<div class="col-xs-3">
<button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
</div>
<div class="col-xs-3">
<button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
</div>
<div class="col-xs-3">
<button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
</div>
</div>
Cloning is a bit more complex because you need to preserve existing id
, style
, data
and events
on the original element and pass them to the clone, making sure the cloning function doesn't modify anything. Also, with cloning, you need to return the clone from the cloning function, because it deletes the original element and therefore any subsequent animations would end up without a subject.
The simple way to stagger is using $.each()
, because it creates the required closure internally. If you want/have to do it in vanilla, you'll have to use a classic for()
and wrap setTimeout()
in a closure to which you pass the current element, otherwise this
becomes err!?
and the staggered animation is gone.
Regarding the design pattern:
In general, when animating collections, if the interval/step is constant, binding the start of the next animation to the end of the previous is regarded as bad practice and a major limitation, because the step has to be longer than the duration. You only do this when the duration of the effect can vary significantly and usually you do it using a recurrent defer/promise construct.
But normally you should keep step (interval) and duration of effect in two separate variables. See my answer to a similar question for details.