3D✨ Premium

Perspective Tilt

A 200 × 140 px indigo card tilts toward the pointer, up to 12° at its edges, a spot of light slides exactly under the mouse and the text floats 40 px in front: it answers a finger and the arrow keys too, every copy on the page gets its own card, and when the system asks for less animation it stops leaning but keeps its glint.

JS3DMouse

Updated

This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. 3D has 50 effects, including 8 free. Explore the category →

3 usage examples

Enamel street sign under the light — shop of an enamel sign maker — Perspective Tilt example 1

① Enamel street sign under the light — shop of an enamel sign maker

WhenThe product page shows the blue "Rue des Tanneurs" sign on a stone wall; when the mouse moves over it, the sign turns toward it and a white glint slides across the enamel, then it returns flat when the mouse leaves.
WhyA glint that follows the pointer exactly conveys the shine of fired enamel, which a still photo cannot show. One sign, one material: the Parallax Tilt Gallery grid would scatter the attention.
SettingsCard enlarged to 400 × 200 px, #2457b5 → #14336f gradient, 7 px white border; ::before gradient switched to white (rgba(255, 255, 255, .55), transparent at 46%); root without background set on the scenery; 36 px Georgia text. Tilt left at 12°.
Collectible card of a volleyball player — shop of a volleyball club — Perspective Tilt example 2

② Collectible card of a volleyball player — shop of a volleyball club

WhenThe club shop presents card no. 7, "Inès, setter, 2027 season", in trading card format; the card tilts under the mouse and the light sweeps its glossy surface, with the price and the add button next to it.
WhyMaking a card shimmer in the light is exactly the collector's gesture. Because the angle is a fraction of the card, this portrait format tilts by the same amount as the original card.
Settings240 × 330 px portrait card, yellow → orange → pink → purple gradient, 6 px cream border; bluish white light (rgba(255, 255, 255, .6) then rgba(190, 240, 255, .18)); 96 px number inside the content pushed 40 px forward.
Engraved plaque of a patron — online donor wall of a theater — Perspective Tilt example 3

③ Engraved plaque of a patron — online donor wall of a theater

WhenOn the patrons' page, the brass plaque "The Martin family — seat 112" sits among the other donors' plaques; the mouse plays the light across the engraving and tilts the plaque.
WhyThe localized glint gives the feel of engraved metal: the thank-you is something to look at, not a name read in a list. The text, pushed 40 px forward, stresses the engraving when the plaque tilts.
Settings380 × 186 px card, brass gradient #b48a35 → #e9cf83 → #9c7429, 6 px corners; warm white light (rgba(255, 250, 225, .7)); brown #4d3708 text with two 1 px shadows for the engraved look; neighboring plaques as scenery, aria-hidden="true".

How it works

On the bluish black of .fx-0003-root (300 px tall or more), a 280 × 200 px .demo-3d frame carries the card .perspective-tilt: 200 × 140 px, #1e1b4b → #312e81 gradient, translucent indigo border, 1rem corners. Each root gets its own instance of the script, which only looks after its own card. As the pointer glides over the card, its position is mapped from 0 to 1 across and down, and the script writes transform: perspective(1000px) rotateX(…) rotateY(…): the side under the pointer comes toward you, the opposite side sinks back, up to 12° at the edges. Read near the top left corner (5% in): 11.0° and 10.6°, the same in Chromium 151 and Firefox 153. A 0.1 s transition irons out the jolts; when the pointer leaves, the card returns flat.

The light is a ::before pseudo-element covering the whole card: an indigo radial gradient (rgba(99, 102, 241, .3), transparent at 50%) centered on two CSS variables, --mouse-x and --mouse-y, that the script updates as percentages. The spot therefore follows the pointer to the pixel, whatever the card size, and stays at the last point touched when the card flattens. The text .perspective-tilt-content is pushed 40 px forward (translateZ) inside a card set to transform-style: preserve-3d: it keeps that depth when the card tilts and slides against the background, by 9.6 px with the pointer at the right edge (measured in Chromium and Firefox).

Three choices make the card usable everywhere. pointer events drive the card, so a pen or a finger makes it lean just like the mouse, and touch-action: none keeps the page from scrolling while the finger slides over the card; it returns flat when the finger lifts. Thanks to tabindex="0" the card takes focus, and a role="group" with an aria-label names it. From the keyboard, each arrow walks the glint a tenth of the card; Escape or leaving the card straightens it. Finally the angle is computed as a fraction of the card, not in pixels: enlarged to 400 × 200 px as in the scenes, it still never goes past 12°, whereas a pixel-based formula would double the angle each time the width doubles.

Accessibility

  • Reduced motion: with prefers-reduced-motion: reduce, the @media rule written under .fx-0003-root pins the card flat (transform: none, no transition). The spot of light still follows the pointer, so the hint of where you point is kept without any movement in space. Checked in all three engines (Chromium, Firefox, WebKit): 0° under the mouse, against 11° without the setting.
  • Keyboard: the card is a tab stop; on focus a 2 px lavender #a5b4fc line surrounds it (:focus-visible, 9.9:1 against the background). Pressing → four times then ↑ twice leans it by 9.6° and 4.9°; Escape or moving focus away straightens it.
  • Screen readers: the card is announced by its name, aria-label="Tilt card: the arrow keys tilt it" set on a role="group", then "Move Mouse" is read. On a site in another language, write that name in it. If the card leads somewhere, put a real link in its content.
  • Contrast: white text on #312e81 at 11.4:1 and on #1e1b4b at 16:1. The indigo spot only lightens it slightly: keep light text if you recolor the card in a dark tone.
  • Touch: touch-action: none blocks page scrolling when the finger starts on the card. At 200 × 140 px that is no trouble; for a card spanning the full width of a phone, switch to touch-action: pan-y: vertical scrolling comes back, and only a sideways drag tilts the card.

Browser compatibility

CSS (transform with perspective(), rotateX and rotateY, transform-style: preserve-3d for the text depth, CSS variables inside a gradient, touch-action, :focus-visible) + JS with pointer events, no dependencies. Measured in Chromium 151 and Firefox 153: same tilt, same spot of light, two independent copies on one page, flat card under reduced motion, finger tilt (Chromium in touch mode). Playwright's WebKit (26.5) does show the card tilting, but it flattens all nested 3D, even on a reference cube: the text depth was not checked on a real Safari.

Chrome 55+✓ Full
Firefox 59+✓ Full
Safari 13+✓ Tilt measured on WebKit; text depth not checked on a real Safari
Edge 79+✓ Full
Mobile iOS 13+✓ Not checked on a real iPhone
Android Chrome✓ Full, with the finger

Without 3D transforms, the card stays flat and only the spot of light follows the pointer. Without :focus-visible (older browser), the browser's default focus ring shows instead.

The code

HTML structure to paste into your page (the CSS + JS come with your purchase):

index.html — structure
<div class="fx-0003-root">
  <div class="demo-3d">
    <div class="perspective-tilt" tabindex="0" role="group" aria-label="…">
      <span class="perspective-tilt-content">Move Mouse</span>
    </div>
  </div>
</div>

One-time payment, no subscription — the code is yours forever

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Maximum tilt (JS — MAX_TILT = 12) 12° Angle reached at the edges of the card, whatever its size. 6 gives a subtle effect for a button or a product visual; past 20, the text becomes hard to read while moving.
Perspective (JS — perspective(1000px) in the transform) 1000px The smaller the value, the stronger the distortion at the same angle: 500px makes the near side clearly larger than the far side. Change it in both places in the script (tilt and reset).
Text depth (CSS — translateZ(40px) on .perspective-tilt-content) 40 px Distance of the text in front of the card: 9.6 px of slide measured at the edge with 40 px. At 0 the text sticks to the card; past 60 it seems to come loose.
Light (CSS — radial-gradient of ::before) rgba(99, 102, 241, .3), transparent 50% Color and size of the spot. White at .5 gives the glint of a glossy material (enamel, card, metal); lower the second percentage (30%) for a smaller, sharper spot.
Card size (CSS — .perspective-tilt, .demo-3d) 200 × 140 px in a 280 × 200 px frame The angle does not depend on the size. Enlarge .demo-3d as well, or remove its dimensions, or the card overflows the frame.
Responsiveness (CSS — transition: transform .1s ease-out) 0.1 s 0.3 s gives a heavier card that catches up with the pointer after a delay; 0 glues it to the pointer with no smoothing at all.
Keyboard step (JS — 0.1 in the keydown listener) 10% of the card per press 0.25 reaches the edge in two presses; keep a value that lets each press show a visible movement.

FAQ

Leaving only resets the tilt: the glint stays at the last point touched, like on an object just put down. To recenter it, add card.style.setProperty('--mouse-x', '50%') and the same line for --mouse-y to the script's reset() function.
Perspective Tilt is a single card that follows the pointer over its own surface, with a glint and raised text. Parallax Tilt Gallery makes the six tiles of a grid react according to their distance from the pointer: nearby tiles tilt a lot, distant ones barely. One piece to showcase: Perspective Tilt; a collection to browse at a glance: Parallax Tilt Gallery.
Yes. Each copy has its own .fx-0003-root, and the script launches one per root, without any id. Tried with two copies on the same page, in all three engines: only the hovered card moves. For a row of cards, give each card its own root and paste the script once.