Published on

Creating the Apple TV Card Animation

Authors

🔥 Demo

hover apple tv card animation

🔨 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>
  )
}

Turning the mousemove into X and Y Coordinates

 * (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'