Animations
Intro animations
Timing functions
ease-out
: starts fast, ends slow. Good for enter animations, like a modal appearing.ease-in
: starts slow, ends fast. Good for exit animations, like a modal disappearing.ease-in-out
: symmetrical easing animationease
: default easing function, assymmetrical. Use this for most animations
will-change
Use the will-change
property to tell the browser that an element will change in the future. This will allow the browser to optimize the rendering of the element.
Here are the numerous benefits:
- Uses hardware-acceleration to optimize animations and make them smoother.
.element {
/* says that the transform property will be animated, so optimize for it */
will-change: transform;
}
Transforms
Translate
The transform: translate()
function is especially powerful because you can use percentages.
100% is relative to an element's size, so transform: translateX(100%)
will move an element to the right by its width in px.
.element {
/* if an element is 500px wide, move it to right 500px */
transform: translateX(100%);
}
Animation accessibility
Use the media query below for accessible animations
@media (prefers-reduced-motion: reduce) {
.btn {
transition: none;
}
}
And here is a react hook to get the value in javascript
const QUERY = "(prefers-reduced-motion: no-preference)";
const getInitialState = () => !window.matchMedia(QUERY).matches;
function usePrefersReducedMotion() {
const [prefersReducedMotion, setPrefersReducedMotion] =
React.useState(getInitialState);
React.useEffect(() => {
const mediaQueryList = window.matchMedia(QUERY);
const listener = (event) => {
setPrefersReducedMotion(!event.matches);
};
mediaQueryList.addEventListener("change", listener);
return () => {
mediaQueryList.removeEventListener("change", listener);
};
}, []);
return prefersReducedMotion;
}
Practical Web Animations
Button Background Swipe
These animations involve using the ::before
psuedoselector to create an element that will slide over the button on hover.
We variate how the overlay is shaped, and where it spawns.
- Set
overflow: hidden
: this is essential to hide the before psuedoelement that we translate out of view withtransform: translateX(-100%)
- Create psuedoelement: Create this pseudelement that is the same size as the button, and translate it out of view
On Hover:
Create transition for transform
and color
properties
- Translate psuedoelement back into view with
transform: translateX(0)
- Change text color of button
$button-bg-color: #3ba6d8;
$button-text-color: white;
.bg-swipe {
// 1. apply basic button styles
padding: 0.75rem 2rem;
cursor: pointer;
border: none;
font-weight: 600;
font-size: 1.1rem;
border-radius: 0.5rem;
background-color: $button-bg-color;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
color: $button-text-color;
// 2. Set relative positioning and set z-index to 1
position: relative;
z-index: 1;
transition: color 0.5s ease;
// 3. Essential: set overflow hidden to hide before psuedoelement
overflow: hidden;
// 4. Create before psuedoelement that starts outside of view
&::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: $button-text-color;
z-index: -1;
transition: transform 0.5s ease;
transform: translateX(-100%);
}
&:hover::before {
transform: translateX(0);
}
&:hover {
color: $button-bg-color;
}
}
The important thing from when creating a bg swipe from center is to realize that we must horizontally center the psuedoelement and change transform origin.
- Center Element: this can be achieved by setting
left: 50%
and then keeping thetransform: translateX(-50%)
property. This will center the psuedoelement - Transform Origin: set transform origin to center.
Then we simply transition over the horizontal scale, from scaleX(0)
to scaleX(1)
.bg-swipe-center {
&::before {
top: 0;
left: 50%;
transform-origin: center center;
// top 0, left 50%, and translateX(-50%) will center the element horizontally
transform: translateX(-50%) scaleX(0);
}
&:hover::before {
transform: translateX(-50%) scaleX(1);
}
}
Same exact idea as center but we also skewX(-30deg)
for an even cooler animation.
.bg-swipe-skew {
&::before {
top: 0;
left: 50%;
transform-origin: center center;
transform: translateX(-50%) scaleX(0) skewX(-30deg);
}
&:hover::before {
transform: translateX(-50%) scaleX(1.5) skewX(-30deg);
}
}
Border Gradients
Using pseudoelements and a conic gradient, we can animate the border gradient going around an element by animating the angle of a conic gradient.
Basic glow
- Create a container element that has
position: relative
since we need that to absolutely position the pseudoelements. - Create pseudoelements using
::before
and::after
that are the same size as the container, add some padding so that they are slightly bigger, move them behind the container, and then center them on the container so they form a halo.
.card::after, .card::before{
content: '';
/* 1. Make same size and center */
position: absolute;
height: 100%;
width: 100%;
top: 50%;
left: 50%;
translate: -50% -50%;
/* 2. Move behind container */
z-index: -1;
/* 3. Add padding to make bigger than container and poke out from behind */
padding: 3px;
border-radius: 10px;
}
- Apply a conic gradient on both pseudoelements.
.card::after, .card::before{
background-image: conic-gradient(from 0deg, #ff4545, #ff0095, #ff4545);
}
- On one of the pseudoelements, add a blur filter so that it produces more of a glow effect.
.card::before{
filter: blur(1.5rem);
opacity: 1; /* decrease opacity if you want to reduce glow */
}
/* 1. relative positioned container */
.card{
margin: 0 auto;
padding: 2em;
width: 300px;
background: #1c1f2b;
text-align: center;
border-radius: 10px;
position: relative;
}
.card::after, .card::before{
content: '';
position: absolute;
height: 100%;
width: 100%;
background-image: conic-gradient(from 0deg, #ff4545, #00ff99, #006aff, #ff0095, #ff4545);
top: 50%;
left: 50%;
translate: -50% -50%;
z-index: -1;
padding: 3px;
border-radius: 10px;
animation: 3s spin linear infinite;
}
.card::before{
filter: blur(1.5rem);
opacity: 1; /* decrease opacity if you want to reduce glow */
}
NOTE
Color Stop Tip The last color in the color stop should always be the same the same as the first color in order to ensure a smooth transition.
Animating normally
To animate the border going around the container, we need to animate the angle of the conic gradient. You might think to do this by animating a CSS variable, which you can do only through JavaScript, but a native CSS way of doing it is using the @property
syntax.
- Declare that you want to create an animatable CSS property named
--angle
@property --angle {
syntax: "<angle>";
initial-value: 0deg;
inherits: false;
}
- Use that degree as the starting angle in the conic gradient
.card::after,
.card::before {
background-image: conic-gradient(
from var(--angle),
#ff4545,
#00ff99,
#006aff,
#ff0095,
#ff4545
);
}
- Create a keyframes animation that animates the animatable property and apply that animation on the pseudoelements
@keyframes spin {
from {
--angle: 0deg;
}
to {
--angle: 360deg;
}
}
.card::after,
.card::before {
animation: 3s spin linear infinite;
background-image: conic-gradient(
from var(--angle),
#ff4545,
#00ff99,
#006aff,
#ff0095,
#ff4545
);
}
.card {
margin: 0 auto;
padding: 2em;
width: 300px;
background: #1c1f2b;
text-align: center;
border-radius: 0.5rem;
position: relative;
transition: 0.3s background-color;
}
@property --angle {
syntax: "<angle>";
initial-value: 0deg;
inherits: false;
}
.card::after,
.card::before {
content: "";
position: absolute;
height: 100%;
width: 100%;
background-image: conic-gradient(
from var(--angle),
#ff4545,
#00ff99,
#006aff,
#ff0095,
#ff4545
);
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 0.25rem;
z-index: -1;
border-radius: 0.5rem;
animation: 3s spin linear infinite;
}
.card::before {
filter: blur(1.5rem);
opacity: 0.5;
}
@keyframes spin {
from {
--angle: 0deg;
}
to {
--angle: 360deg;
}
}
Animating with transparency
For the illusion of a ray racing around the button, we can add the transparent
color to the conic gradient and play around with color stops.
background-image: conic-gradient(
from var(--angle),
transparent 70%,
blue,
transparent
);
Full Solution
Here is a pure CSS Solution, where we use the experimental @property
syntax to animate CSS Variables since you can't do that by default.
.card{
position: relative;
}
/* for animating CSS variables */
@property --angle{
syntax: "<angle>";
initial-value: 0deg;
inherits: false;
}
.card::after, .card::before{
content: '';
position: absolute;
height: 100%;
width: 100%;
z-index: -1;
padding: 3px;
background-image: conic-gradient(from var(--angle), #ff4545, #00ff99, #006aff, #ff0095, #ff4545);
top: 50%;
left: 50%;
translate: -50% -50%;
border-radius: 10px;
animation: 3s spin linear infinite;
}
.card::before{
filter: blur(1.5rem);
opacity: 0.5;
}
@keyframes spin{
from{
--angle: 0deg;
}
to{
--angle: 360deg;
}
}
Image Animations
All of these image animation require an image container. What all of these effects have in common is setting overflow: hidden
and animating opacity or transform.
Image Overlay
We make a hidden before psuedoelement that has a semi-transparent black background to act as an overlay.
- Set overflow to hidden and translate overlay out of view to hide the overlay
- On hover, translate the overlay from left to right
<div class="overlay">
<img src="gojo.png" alt="" id="gojo" />
</div>
.overlay {
// essential properties
position: relative;
overflow: hidden;
// 1. create overlay
&::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.3);
// 2. translate out of view
transition: transform 0.5s ease;
transform: translateX(-100%);
}
// 3. translate into view on hover
&:hover::before {
transform: translateX(0);
}
}
Image Zoom
We set overflow to hidden on image container, and on hover, we scale the image nested inside up to create this zoom effect
<div class="zoom">
<img src="gojo.png" alt="" id="gojo" />
</div>
.zoom {
overflow: hidden;
&:hover img {
transform: scale(1.1);
}
img {
transition: transform 0.5s ease;
}
}
Image Tint
- Add a background color to the image container
- On hover, reduce image opacity so some the background color bleeds on top of image, creating tint effect.
<div class="tint">
<img src="gojo.png" alt="" id="gojo" />
</div>
.tint {
overflow: hidden;
background-color: royalblue;
&:hover img {
opacity: 0.8;
}
img {
transition: opacity 0.5s ease;
}
}
Hamburger menu animation
Web Animations API
The new JavaScript Web Animations API is a way of programatically using CSS animations and transitions with stuff like keyframes
Keyframes and basic animation
You can animate an element with the HTMLElement.animate()
method. This method takes two arguments: an array of keyframes as Keyframes[]
and an options object of KeyframeAnimationOptions
.
element.animate(keyframes, options);
// 1. Provide keyframes. Split percentage evenly between number of objects
const keyframes: Keyframe[] = [
{ transform: "translateX(15rem)" },
{ transform: "translateX(-15rem)" },
];
// 2. Provide animation options
const animationOptions: KeyframeAnimationOptions = {
duration: 5000,
iterations: Infinity,
easing: "ease-in-out",
direction: "alternate",
fill: "both",
delay: 1000,
playbackRate: 3,
};
const animation = ball.animate(keyframes, animationOptions);
But besides this basic way, there are three other ways of doing Keyframe animations:
1. Using offset
const keyframes: Keyframe[] = [
{ transform: "translateX(0)" },
{ transform: "translateX(15rem)", offset: 0.25 },
{ transform: "translateX(-15rem)", offset: 0.5 },
{ transform: "translateX(0)" },
];
The default behavior of the HTMLElement.animate()
method is to evenly distribute the keyframes across the animation duration. But you can use the offset
property to specify the exact percentage point of each keyframe.
2. Specifying properties instead
const keyframes: PropertyIndexedKeyframes = {
opacity: [1, 0.5, 1],
transform: [
"translateX(0)",
"translateX(15rem)",
"translateX(-15rem)",
"translateX(0)",
],
};
In this approach, we use the PropertyIndexedKeyframes
type, which is an object where the keys are the properties to animate, and the values are the keyframes for that property, spread evenly.
3. Implicit to
If you supply only one keyframe, JS will recognize that as the desired ending state of your animation, or the properties that will be animated to.
// animation to fade out right.
const animation = toast.element.animate(
[{ opacity: 0, transform: "translateX(250px)" }],
{
duration: 250,
}
);
Controlling animation playback
The Element.animate()
method returns an Animation
object instance, which can be used to control the playback of the animation.
Here are useful methods:
Animation.play()
: Starts or resumes the animation sequence playbackAnimation.pause()
: Pauses the animation sequence playbackAnimation.reverse()
: Reverses the animation sequence playbackAnimation.finish()
: Jumps to the end of the animation sequenceAnimation.cancel()
: Cancels the animation sequenceAnimation.updatePlaybackRate(n)
: Updates the playback rate of the animation sequence to the passed in number, where 1 is normal speed. Negative numbers will reverse the animation.
And here are some useful properties:
Animation.playState
: Returns the current playback state of an animationAnimation.currentTime
: Gets or sets the current time value of an animationAnimation.startTime
: Gets or sets the start time value of an animationAnimation.playbackRate
: Gets or sets the playback rate of an animation. 1 is normal speed. Negative numbers will reverse the animation.
Here are some events you can listen to on the animation using the addEventListener()
method:
"cancel"
: Fired when the animation is cancelled"finish"
: Fired when the animation finishes
Better fill mode
There are some problems with the animation-fill-mode
property, where it is not only inefficient and expensive to use, but also confusing because of CSS cascading.
The Animation.commitStyles()
method can be used to apply the styles of the animation to the element before the animation runs. This is useful for animations that have a fill
value of both
or forwards
.