- Published on
Creating the Apple TV Card Animation
- Authors
- Name
- Terence Bezman
- @b_ez_man
Demo
🔥🔨 Building it out
📃 The Markup
First, let's create a basic setup for our card. We'll create a wrapper div
to take up the entire screen with a black background color. Then we'll crate a 400x400 white card which we'll use for the animation.
<div className="card-wrapper" ref={wrapperRef}>
<div className="card" />
</div>
🎨 Basic CSS
Not much CSS involved here. We'll get to some fun stuff in the JavaScript section later. I've outlined the important parts with /* Important Part */
so it's easier to see what really matters here.
Perspective
The way I understand perspective is the following. How far away from the element are you on the Z axis. When you're really close to something and it tilts / skews, the transform would look really drastic. The larger we set our perspective
property, the less jarring the animation will look.
Transition
We're just doing a simple 500 millisecond ease-out animation. I found this looks pretty good.
body {
margin: 0;
padding: 0;
}
.card-wrapper {
display: flex;
justify-content: center;
align-items: center;
width: 100vw;
height: 100vh;
background: black;
/* Important Part */
perspective: 1000px;
}
.card {
width: 400px;
height: 400px;
background: white;
border-radius: 4px;
/* Important Part */
transition: all 500ms ease-out;
}
🎥 JavaScript
I'll paste the whole snippet here just so you can see it at a high level, but we'll do a deep dive on each section below.
import { useEffect, useRef } from 'react'
import './styles.css'
export default function App() {
const wrapperRef = useRef<HTMLDivElement | null>(null)
useEffect(() => {
const wrapper = wrapperRef.current
if (!wrapper) {
return
}
const handleMove = (event: MouseEvent) => {
/**
* (x,y)
* -1,1 1,1
* ------|------
* ------|------
* ------|------
* -1,-1 1,-1
*/
const rect = wrapper.getBoundingClientRect()
const x = ((event.clientX - rect.left) / rect.width - 0.5) * 2
const y = ((event.clientY - rect.top) / rect.height - 0.5) * 2 * -1
const rotationAroundYAxis = x * 10
const rotationAroundXAxis = y * 10
wrapper.style.transform = `rotateY(${rotationAroundYAxis}deg) rotateX(${rotationAroundXAxis}deg) scale3d(1.025, 1.025, 1.025)`
wrapper.style.boxShadow = '0 5px 50px -12px yellow'
}
const handleMouseOut = () => {
wrapper.style.transform = `rotateX(0deg) rotateY(0deg) scale3d(1,1,1) perspective(0)`
wrapper.style.boxShadow = ''
}
wrapper.addEventListener('mousemove', handleMove)
wrapper.addEventListener('mouseleave', handleMouseOut)
return () => {
wrapper.removeEventListener('mousemove', handleMove)
wrapper.removeEventListener('mouseleave', handleMouseOut)
}
}, [])
return (
<div className="card-wrapper">
<div className="card" ref={wrapperRef} />
</div>
)
}
mousemove
into X and Y Coordinates
Turning the * (x,y)
* -1,1 1,1
* ------|------
* ------|------
* ------|------
* -1,-1 1,-1
*/
const rect = wrapper.getBoundingClientRect();
const x = ((event.clientX - rect.left) / rect.width - 0.5) * 2;
const y = ((event.clientY - rect.top) / rect.height - 0.5) * 2 * -1;
This little snippet takes the mouse event, and transforms those coordinates into a -1 to 1 scale. As the comment in the code suggests, top.
- Top Left: -1, 1
- Top Right: 1,1
- Bottom Left: -1,-1
- Bottom Right: 1,-1
Breaking down the transform
const x = ((event.clientX - rect.left) / rect.width - 0.5) * 2
Distance from the left of the card (event.clientX - rect.left)
Distance from the left of the card scaled from 0 to 1 (event.clientX - rect.left) / rect.width
Distance from the center of the of the card scaled from -.5 to .5 ((event.clientX - rect.left) / rect.width - 0.5)
Distance from the center of the card scaled from -1 to 1 ((event.clientX - rect.left) / rect.width - 0.5) * 2
Then we just do the same thing for the Y Coordinate but multiply it by -1 to make sure the top of our card is the positive side.
Axis Rotation
Now we want to take our -1 to 1 scale, and convert that to how many degrees around a given axis we want to rotate. My scale goes from -10 degrees to +10 degrees. So multiplying by 10 will do the trick.
const rotationAroundYAxis = x * 10
const rotationAroundXAxis = y * 10
Applying the Transformation
Now we'll take our values and update the transform on the element. Then we'll apply a 3d scale to dilate the card just a bit. We'll also apply a box shadow for decoration.
wrapper.style.transform = `rotateY(${rotationAroundYAxis}deg) rotateX(${rotationAroundXAxis}deg) scale3d(1.025, 1.025, 1.025)`
wrapper.style.boxShadow = '0 5px 50px -12px yellow'