Source Code

<section class="tlt-hero">
  <div class="tlt-copy">
    <span class="tlt-eyebrow">New — Studio 4.0</span>
    <h1 class="tlt-h1">Design software<br>that moves with you</h1>
    <p class="tlt-sub">A canvas that responds to every gesture. Move your cursor over the mockup — it's really tracking you, not just playing an animation.</p>
    <a href="#" class="tlt-cta">Download for free</a>
  </div>

  <div class="tlt-stage" id="tltStage">
    <div class="tlt-card" id="tltCard">
      <div class="tlt-glare" id="tltGlare"></div>
      <div class="tlt-screen">
        <div class="tlt-topbar">
          <span></span><span></span><span></span>
        </div>
        <div class="tlt-bars">
          <div class="tlt-bar" style="width:78%"></div>
          <div class="tlt-bar" style="width:52%"></div>
          <div class="tlt-bar" style="width:64%"></div>
        </div>
        <div class="tlt-blocks">
          <div class="tlt-block"></div>
          <div class="tlt-block"></div>
          <div class="tlt-block"></div>
        </div>
      </div>
    </div>
  </div>
</section>

Hero with 3D Tilting Product Mockup — Free HTML CSS JS Snippet

Hero with 3D Tilting Product Mockup · Heroes · Plain HTML, CSS & JS · Live preview

What's included

Features

Real pointer-tracked rotation
rotateX/rotateY derived from live cursor position.
Proportional tilt
Rotation scales continuously with distance from center.
3D depth via translateZ
Screen content sits visibly above the card base.
Cursor-synced glare
Highlight position matches the same px/py as the tilt.
Smooth return to rest
pointerleave eases back to flat, not an instant cut.
perspective + preserve-3d
Correct CSS 3D context for real depth.
Abstract mockup UI
Drop-in placeholder for a real screenshot.
No dependencies
Pure vanilla JS pointer math.

About this UI Snippet

Hero with 3D Tilting Product Mockup — Cursor-Tracked Tilt and Glare

Screenshot of the Hero with 3D Tilting Product Mockup snippet rendered live

A product mockup that "tilts toward" the cursor is a small detail that reads as expensive craftsmanship — but only if it's actually tracking the pointer. This snippet computes real rotateX/rotateY values from the cursor's live position over the hero every frame, rather than looping a fixed CSS keyframe animation that ignores where the mouse actually is.

The math behind the tilt

On every pointermove over the stage, the handler reads getBoundingClientRect() to get the stage's live position and size, then computes the cursor's position as a 0–1 fraction of the stage's width and height (px, py). Centering that fraction around 0.5 and multiplying by MAX_TILT produces a signed rotation: cursor at the left edge yields a negative rotateY, cursor at the right edge yields a positive one, and the card sits flat at dead center. This is a direct, continuous function of pointer position — move the cursor one pixel, the rotation updates by a proportional fraction of a degree.

Why perspective and transform-style matter

.tlt-stage sets perspective: 1200px — without it, rotateX/rotateY on the card would have no visible depth, just a 2D squash. .tlt-card sets transform-style: preserve-3d so its child elements (the screen content) can be pushed forward in 3D space rather than being flattened onto the card's own plane; .tlt-screen uses translateZ(30px) to sit visibly above the card's base surface, giving the tilt actual depth rather than looking like a rotated flat image.

A glare that moves with the tilt, not against it

The glare layer's radial-gradient center position is recalculated every pointermove from the same px/py fractions driving the rotation — so the highlight tracks the same point the cursor is "pushing" toward, the way light behaves on a real glossy surface being tilted, rather than sitting static or drifting independently of the tilt.

A clean return to rest

pointerleave resets both the transform and the glare opacity, with the CSS transition on .tlt-card easing the snap back to flat rather than the card cutting instantly to rotateX(0) rotateY(0).

Customizing it

Adjust MAX_TILT for a subtler or more dramatic effect, add more translateZ layers inside .tlt-screen for a deeper parallax stack, or swap the abstract UI mockup for a real product screenshot. Pair it with 3D card tilt or tilt glow card for the same technique on a smaller card component, or gradient mesh hero for a different hero backdrop.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Rather than eyeballing the tilt feel, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how the pointermove handler turns a raw clientX/clientY into a signed rotateX/rotateY pair, and why perspective on the parent and transform-style: preserve-3d plus translateZ on the inner content are both required for the tilt to read as real depth rather than a 2D squash. The same assistant can help you tune the feel — ask whether MAX_TILT of 14 degrees is too subtle or too aggressive for a hero-sized card, or whether the transition duration on pointerleave should be longer for a softer settle. It's also useful for extending the effect: ask it to add a second, more distant layer that tilts at a slower rate for extra parallax depth, make the effect touch-friendly with a gyroscope fallback on mobile, or throttle the pointermove handler with requestAnimationFrame for very high-refresh-rate displays. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a hero section in plain HTML, CSS, and vanilla JavaScript featuring a product mockup card that tilts in genuine 3D based on live cursor position (no library, no CDN).

Requirements:
- A hero with headline, subheading, and CTA above a mockup "stage" containing a card styled like an abstract app screenshot (a fake window topbar with three colored dots, a few horizontal bar elements of varying widths, and a small grid of block placeholders).
- The stage must have CSS perspective set, and the card must use transform-style: preserve-3d with its inner content pushed forward via translateZ so the tilt has real visible depth, not a flat 2D skew.
- On pointermove over the stage, compute the cursor's position as a fraction (0 to 1) of the stage's actual width and height using getBoundingClientRect (not fixed/hardcoded dimensions), center that fraction around the midpoint, and use it to set a proportional rotateX and rotateY on the card — cursor near an edge should produce a noticeably larger rotation than cursor near the center, and the relationship must be continuous and derived from the real pointer position, not a preset animation sequence.
- Add a radial-gradient glare layer overlaid on the card whose highlight position is recalculated from the same cursor-fraction values on every pointermove, so the glare visually tracks the same point the card is tilting toward — hidden (opacity 0) when the pointer is not present.
- On pointerleave, smoothly transition the card back to a flat rotateX(0) rotateY(0) state and fade the glare back out, using a CSS transition rather than an abrupt cut.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSThe hero renders with the mockup flat at rest.
  2. 2
    Move the cursor over the mockup areaThe card tilts toward your cursor in real 3D, tracking it continuously.
  3. 3
    Move to the edgesRotation increases toward MAX_TILT as the cursor approaches the stage bounds.
  4. 4
    Move the cursor awayThe card eases back to flat and the glare fades out.
  5. 5
    Tune MAX_TILTIncrease for a more dramatic tilt, decrease for subtlety.
  6. 6
    Swap the mockup contentReplace .tlt-screen's contents with a real screenshot or iframe.

Real-world uses

Common Use Cases

Design/creative tool landing pages
Show off a canvas product with tactile feel.
Hardware and device launches
Tilt a 3D-rendered device mockup toward the cursor.
App landing pages
Pair with app hero for a phone mockup tilt.
Portfolio hero pieces
Feature a flagship project with interactive depth.
Premium/luxury product pages
Signal craftsmanship through responsive motion.
Interactive UI showcases
Complement 3D card tilt grids.
Related: Hero with Feature Tabs Preview
See the Hero with Feature Tabs Preview for a related heroes pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

On pointermove, the handler reads the stage's live bounding rect, converts the cursor's clientX/clientY into a 0-1 fraction of the stage width and height, centers that fraction around 0.5, and multiplies by MAX_TILT to get signed rotateX/rotateY degrees. Because this runs on every pointermove event with the current cursor position, the rotation is a continuous function of where the cursor actually is — not a looping animation.

perspective on the parent stage establishes the 3D viewing distance — without it, rotateX/rotateY would just squash the card in 2D with no sense of depth. transform-style: preserve-3d on the card lets its child .tlt-screen element (pushed forward with translateZ) render in true 3D space relative to the card's rotation, instead of being flattened onto the card's own 2D plane.

The glare's radial-gradient center position is set from the exact same px and py fractions (0-1 cursor position within the stage) used to compute the rotation, on the same pointermove event. Since both are derived from one shared position, the highlight always sits toward the side the card is tilting toward, mimicking how light reflects off a real glossy surface as it's turned.

A pointerleave listener resets the card's transform to rotateX(0deg) rotateY(0deg) scale(1) and the glare's opacity to 0. Because .tlt-card has a CSS transition on transform, the card eases smoothly back to flat rather than snapping instantly, which reads as a natural settle rather than a glitch.

Attach the pointermove and pointerleave listeners in a mount effect (useEffect in React, onMounted in Vue) scoped to the stage ref, and clean them up on unmount. Since the effect only ever writes to style.transform and the glare's background via refs, it works the same way regardless of framework — no state re-renders are needed per pointer move if you write directly to the DOM node.