Design Lounge
Install

Nº 140 of 520

Product window tilt hero

A dark SaaS hero whose div-built app window tilts up to 8° toward the cursor, with sidebar, chart, chip and toast parallaxing at four translateZ depths.

Preview

Open full size
1280 × 800 · 15.5 KB · 194 lines

Use it

Free to use · credit appreciated

1 Once, in your project

npx skills add SusanAcharya/Design-Lounge

2 Then ask

Using the Design Lounge skill, build "Product window tilt hero" (piece hero-product-window-tilt) in my stack. Match its brief; restyle it onto my product's theme.

No skill? Paste the brief instead. It carries the numbers, tokens and behaviour.

Brief

205 lines

Build brief for a coding agent. Rebuild this piece in the reader’s stack. If they haven’t said which stack, ask once, then default to semantic HTML + CSS + a little vanilla JS. Match the numbers below; don’t “improve” them.

What it is

The top of a marketing site for Ledgerline, a fictional expansion-revenue tool. The hero has a centred headline and two CTAs. Below them, a full app window built entirely from divs and SVG sits in perspective and leans toward the cursor by up to 8°. Four layers inside the window live at different translateZ depths: sidebar 24px, chart card 56px, status chip 84px, notification toast 120px. As the window turns, they slide over each other like real stacked glass. The detail worth copying: the window is a product screenshot that never goes stale, and its depth reads as quality before anyone reads a word.

Reference behaviour

  1. Initial state: near-black page with a 24px dot grid. Nav at the top. A “New” pill, a 60px two-tone headline, a subline and two CTAs, all centred. The window sits at top:418px, 1000×520, resting at rotateX(6deg) rotateY(-3deg) and cropped by a 120px fade at the bottom of the viewport.
  2. On load, the window rises 60px → 0 and fades in over 900ms on expo-out after 120ms. The chip pops in at 1050ms and the toast at 900ms (700ms each, translateY 14px → 0, fade in).
  3. Moving the pointer anywhere in the viewport sets a target tilt. rotateY = nx × 8°, where nx is −1…1 across the window’s horizontal centre. rotateX = clamp(2 − ny × 6, −8, 8)°, where ny is −1…1 from a third of the way down the window. The current tilt follows the target with a 0.09 lerp per animation frame, which settles in about 500ms. The loop stops once the difference drops below 0.01°.
  4. Because each layer has its own Z, the toast (Z 120) visibly travels further than the chart (Z 56), which travels further than the sidebar (Z 24), which moves past the flat window shell.
  5. When the pointer leaves the document, the window eases back to its resting tilt.
  6. The chip’s mint dot pulses its ring outward every 2.4s. That is the only looping motion.
  7. Hovering a button lightens its background over 160ms. CTAs are links; the demo cancels navigation.

Structure

1280 × 800
┌───────────────────────────────────────────────────────────────────┐
│ ▣ Ledgerline  Product Signals Customers Pricing Changelog [Sign in][Start free] │ nav 64
│                     ( New  Seat-usage signals… )                  │
│              See expansion revenue before                         │ h1 60/1.02
│                your customers ask for it.                         │ (2nd half ink-3)
│                    subline 16px, 520 max                          │
│              [Start a 14-day trial] [Book a demo]                 │ 44px buttons
│  (● Forecast recalculated)  Z84                                   │
│   ┌──────────────────────── window 1000×520 ─────────────────┐    │ top 418
│   │ ●●●  app.ledgerline.io/signals                       ⌘K  │ 38 │
│   │ side 196 Z24 │ Expansion signals        [7d|30d|90d]     │    │
│   │ Brightloom   │ [NRR 128.4%] [$1.84M] [12 at risk]        │    │
│   │ Overview     │ ┌ chart card Z56 ────────── tip ┐         │    │
│   │ Signals 18   │ │ mint area + dashed forecast  ┌┴ toast Z120 ┐ │
│   │ …            │ └──────────────────────────────│ Halcyon 95% │ │
│ ░░░░░░░░░░░░░░░░░░░░░ fade 120px ░░░░░░░░░░░░░░░░░└─────────────┘░│
└───────────────────────────────────────────────────────────────────┘
  • <nav aria-label="Primary">: logo, <ul> of 5 links, right-aligned Sign in and Start free.
  • <section class="hero">: centred text block, padding-top:44px.
  • .stage (role="img", aria-label="Ledgerline app preview") is absolute, 1000×520, centred, perspective:1700px, perspective-origin:50% 20%.
    • .win has transform-style:preserve-3d and the rotate transform from --rx/--ry.
      • .shell: background, border, radius 14, grid rows 38px 1fr. It holds the title bar and .body (a grid of 196px 1fr).
        • .side at translateZ(24px), with its own border and shadow.
        • .main holds the header, the KPI row and .chart at translateZ(56px) with an inner .tip (+30px), then account rows.
      • .chip (Z 84) at left:-36px; top:-16px. .toast (Z 120) at right:-60px; top:282px, 300px wide.
  • .fade is a 120px gradient to --bg at the viewport bottom, pointer-events:none.

Tokens

:root {
  /* colour */
  --bg: #07080a;        /* page */
  --bg-2: #0c0e11;      /* window shell, pill */
  --panel: #111418;     /* sidebar, KPI cards */
  --panel-2: #161a1f;   /* chart card, button hover */
  --panel-3: #1c2128;   /* active nav, chip */
  --line: #22272e;      /* inner hairlines */
  --line-2: #2d343c;    /* outer borders */
  --ink: #eef1f4;
  --ink-2: #9aa3ad;
  --ink-3: #626b75;
  --mint: #7cf2c4;      /* the one accent */
  --mint-dim: rgba(124,242,196,.14);
  --mint-ink: #03140d;  /* text on mint */
  --warn: #f2b37c;      /* risk delta only */

  /* type */
  --sans: "Onest", system-ui, sans-serif;
  --mono: "Geist Mono", ui-monospace, monospace;

  /* depth */
  --tilt-max: 8deg;
  --z-side: 24px;
  --z-chart: 56px;
  --z-chip: 84px;
  --z-toast: 120px;
  --perspective: 1700px;

  /* shape */
  --r-win: 14px;
  --r: 8px;
  --shadow-lift: 0 18px 40px -12px rgba(0,0,0,.7);
  --shadow-win: 0 0 0 1px rgba(0,0,0,.6), 0 40px 120px -20px rgba(0,0,0,.9), inset 0 1px 0 rgba(255,255,255,.06);

  /* motion */
  --t-micro: 160ms;
  --t-in: 900ms;
  --lerp: .09;
  --ease: cubic-bezier(.2, .7, .2, 1);
  --expo: cubic-bezier(.16, 1, .3, 1);
}

Typography

RoleFamilySizeWeightLine-heightTrackingNotes
HeadlineOnest60px6001.02−0.045em2nd clause --ink-3
SublineOnest16px4001.550max 520px
Nav linksOnest13.5px4001.50--ink-2
ButtonsOnest13.5 / 14.5px5001036 / 44px tall
Window h2Onest16px6001.3−0.015em
KPI valueOnest22px6001.2−0.03em
Window bodyOnest12–12.5px400–5001.40
URL, KPI labels, deltasGeist Mono10–11.5px4001.40
Sidebar groupGeist Mono9.5px5001+0.12emUPPERCASE

Motion

ElementTriggerPropertyFrom → ToDurationEasingDelay
.winloadtranslate, opacity0 60px, 0 → 0 0, 1900msexpo120ms
.toastloadtranslate, opacity0 14px, 0 → 0, 1700msexpo900ms
.chiploadtranslate, opacitysame700msexpo1050ms
.win tiltpointermoverotateX / rotateYrest (6°, −3°) → target, max ±8°lerp 0.09/framen/arAF
.win tiltpointerleaverotatecurrent → restlerpn/a
chip dotloopbox-shadow ring3px → 6px, fades2400ms--easeinfinite
buttonshoverbackground, border→ lighter160ms--ease

Use the individual translate property for the entrance so it does not fight the transform that holds the rotation. Reduced motion: no entrance, no pulse, and the window stays at its resting tilt with no pointer tracking.

States

  • Primary button: mint fill, --mint-ink text. Hover #9bf6d3.
  • Secondary button: transparent with a --line-2 border. Hover fills --panel-2 with a #3a434d border.
  • Nav link hover: --ink-2 → --ink.
  • Focus-visible: 2px mint outline, 3px offset, 6px radius.
  • In-window active states (static, for realism): Signals nav row on --panel-3 with a mint count badge, and the 30d segment selected.
  • Resting vs tracking: resting is a 6° lean back and 3° turn left. While tracking, the window always faces the cursor.

Accessibility

  • The whole window is decoration plus a picture of the product: role="img" with aria-label="Ledgerline app preview", and nothing inside it is focusable. Use non-link <a> elements or divs inside it.
  • Real focus order: logo, 5 nav links, Sign in, Start free, trial CTA, demo CTA.
  • Pointer tracking is cosmetic and has no keyboard equivalent, by design.
  • Contrast: --ink-2 on --bg is 7.6:1. --ink-3 is used for the headline’s second clause at 60px (large text, 3.6:1) and for small meta inside the decorative window.
  • Buttons are 36px in the nav and 44px in the hero.

Responsive rules

  • ≥ 1280: as specified.
  • 1024–1279: scale the stage with transform: scale(.86) from top centre (keep the inner pixel values). The headline drops to 52px.
  • 768–1023: hide the toast’s overflow beyond the window (move it to right:16px), headline 44px, and the stage scales to 0.72.
  • < 640: drop pointer tracking (touch has no hover). Show the window flat at rotateX(8deg), scaled to the width, with the sidebar hidden. The headline goes to 36px with left-aligned text, and the CTAs stack at full width.

Acceptance checklist

  • The window is built from HTML/SVG. No images.
  • Resting transform is rotateX(6deg) rotateY(-3deg) inside a perspective:1700px stage.
  • Tilt never exceeds ±8° on either axis.
  • Sidebar, chart, chip and toast sit at Z 24, 56, 84 and 120px and visibly parallax against each other while tilting.
  • No element between .win and a raised layer has overflow:hidden, opacity < 1 or filter, any of which would flatten the 3D.
  • Tilt follows the cursor with easing, not instantly, and the rAF loop stops when settled.
  • On pointer leave, the window returns to rest.
  • The toast and chip enter after the window, at 900ms and 1050ms.
  • The headline’s second clause is --ink-3.
  • Only the chip dot loops. Nothing else moves at rest.
  • Focus rings show on all nav links and CTAs. Nothing inside the window takes focus.
  • Reduced motion leaves a static, complete composition.

Implementation notes

The preserve-3d chain must be unbroken. Every ancestor between the rotating element and a raised layer needs transform-style: preserve-3d, and none of them may clip:

.stage { perspective: 1700px; perspective-origin: 50% 20%; }
.win { transform-style: preserve-3d;
       transform: rotateX(var(--rx, 6deg)) rotateY(var(--ry, -3deg)); }
.win * { transform-style: preserve-3d; }
.side  { transform: translateZ(24px); }
.chart { transform: translateZ(56px); }
.toast { transform: translateZ(120px); }

Lerp loop that sleeps when idle:

function step() {
  c.x += (t.x - c.x) * .09; c.y += (t.y - c.y) * .09;
  win.style.setProperty('--rx', c.x.toFixed(2) + 'deg');
  win.style.setProperty('--ry', c.y.toFixed(2) + 'deg');
  raf = (Math.abs(t.x - c.x) > .01 || Math.abs(t.y - c.y) > .01)
    ? requestAnimationFrame(step) : 0;
}
addEventListener('pointermove', (e) => {
  /* nx, ny in −1…1 */ t.x = clamp(2 - ny * 6, -8, 8); t.y = nx * 8;
  if (!raf) raf = requestAnimationFrame(step);
});

Rounded window without overflow:hidden: give the sidebar its own border-bottom-left-radius:14px and keep every child inside the shell’s box. Clipping is what kills the depth.

Common mistakes: putting the entrance on transform, which overwrites the rotation (use translate). Tilting toward the screen centre instead of the window centre. Using a CSS transition on transform for tracking, which stutters on every pointermove.

Details

Palette
Type
Onest · Geist Mono
Motion
Rich motion
Build
Multi-step
Designed bySusan AcharyaKathmandu

Studied

Linear

Show the product doing the work. Fade it at the edges and keep one colour for what matters.

All sources