Tailwind + JSMIT licensed

Tailwind 3D Floating Phone Mockup Card for App Landing Pages

An app-landing hero card: a CSS-drawn phone rests at a 3D angle and straightens toward you as the cursor approaches, while rating and download badges bob in front of it and a specular streak sweeps the glass. The port fixes a flattening bug — the phone now carries its own perspective(900px), because the card's overflow-hidden was collapsing its rotation to a squash — and adds a real download CTA.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time

Untitled UI React · Affiliate link

Get Untitled UI Pro

The code

<section class="ttc-13 w-full min-h-svh grid place-items-center bg-ttc13-bg bg-[radial-gradient(90%_100%_at_20%_0%,var(--color-ttc13-lobe),transparent_60%)] px-6 py-14 font-ttc13-sans text-white" aria-label="3D floating phone mockup card demo">
  <div class="grid w-full justify-items-center gap-8">
    <article data-ttc13-card class="group/card relative w-full max-w-[20rem] overflow-hidden rounded-[26px] border border-white/10 bg-linear-170 from-ttc13-card1 to-ttc13-card2 px-6 pt-6 shadow-[0_32px_64px_-28px_oklch(0_0_0/0.85)] [transform:perspective(1400px)_rotateX(var(--rx,0deg))_rotateY(var(--ry,0deg))] transition-transform duration-600 ease-[cubic-bezier(0.2,0.9,0.3,1.1)] data-live:duration-80 data-live:ease-linear motion-reduce:transition-none">
      <div class="flex items-start justify-between gap-3">
        <div>
          <h3 class="text-[24px] leading-none font-extrabold tracking-[-0.02em]">Tidepool</h3>
          <p class="mt-1.5 text-[13.5px] text-ttc13-mut">Sleep sounds, tuned to your night.</p>
        </div>
        <a href="#" class="inline-flex min-h-11 flex-none items-center rounded-full bg-white px-4 text-[13px] font-bold text-ttc13-bg no-underline transition-transform hover:scale-105 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-ttc13-acc">Get it free</a>
      </div>
      <div aria-hidden="true" class="relative mt-5 h-64">
        <div class="absolute -bottom-10 left-1/2 -ml-[75px] h-[300px] w-[150px] rounded-[30px] border border-white/18 bg-ttc13-frame p-[9px] shadow-[24px_30px_50px_-20px_oklch(0_0_0/0.7)] [transform:perspective(900px)_rotateY(calc(-18deg_+_var(--ry,0deg)*1.4))_rotateX(calc(8deg_+_var(--rx,0deg)*0.6))] transition-transform duration-600 ease-[cubic-bezier(0.2,0.9,0.3,1.1)] group-data-live/card:duration-80 group-data-live/card:ease-linear motion-reduce:transition-none">
          <span class="absolute top-4 left-1/2 z-10 h-[9px] w-11 -translate-x-1/2 rounded-full bg-ttc13-notch"></span>
          <div class="relative flex size-full flex-col items-center overflow-hidden rounded-[22px] bg-linear-190 from-ttc13-screen1 to-ttc13-screen2 px-3 pt-9 after:absolute after:-inset-[60%] after:-translate-x-[70%] after:bg-[linear-gradient(115deg,transparent_44%,oklch(1_0_0/0.28)_50%,transparent_56%)] after:transition-transform after:duration-900 after:content-[''] group-data-live/card:after:translate-x-[55%]">
            <p class="text-[22px] leading-none font-bold tabular-nums">23:42</p>
            <p class="mt-1 text-[9px] font-semibold tracking-[0.14em] text-white/75">OCEAN DRIFT · 45 MIN</p>
            <div class="mt-5 size-20 rounded-full bg-[conic-gradient(var(--color-ttc13-acc)_0_62%,oklch(1_0_0/0.14)_62%)] p-[5px]"><div class="size-full rounded-full bg-ttc13-screen2"></div></div>
            <div class="mt-5 h-6 w-full rounded-lg bg-white/16"></div>
            <div class="mt-2 h-6 w-full rounded-lg bg-white/10"></div>
          </div>
        </div>
        <span class="absolute top-5 left-0 rounded-full border border-white/20 bg-white/10 px-3 py-2 text-[11.5px] font-bold backdrop-blur-sm animate-ttc13-bob motion-reduce:animate-none">★ 4.9 rating</span>
        <span class="absolute top-28 right-0 rounded-full border border-white/20 bg-white/10 px-3 py-2 text-[11.5px] font-bold backdrop-blur-sm animate-ttc13-bob [animation-delay:-1.7s] motion-reduce:animate-none">2M+ downloads</span>
      </div>
    </article>
    <p aria-hidden="true" class="rounded-full border border-white/12 bg-white/5 px-3.5 py-2 font-ttc13-mono text-[11.5px] text-ttc13-mut">perspective(900px)_rotateY(calc(-18deg_+_var(--ry)*1.4))</p>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-ttc13-bg: #0d1220;
  --color-ttc13-bg: oklch(0.15 0.02 250);
  --color-ttc13-lobe: #1b2f52e6;
  --color-ttc13-lobe: oklch(0.26 0.06 240 / 0.9);
  --color-ttc13-card1: #182a3f;
  --color-ttc13-card1: oklch(0.22 0.04 235);
  --color-ttc13-card2: #0f1726;
  --color-ttc13-card2: oklch(0.16 0.025 250);
  --color-ttc13-frame: #232a36;
  --color-ttc13-frame: oklch(0.25 0.02 250);
  --color-ttc13-notch: #0b0e14;
  --color-ttc13-notch: oklch(0.12 0.01 250);
  --color-ttc13-screen1: #2e6fa8;
  --color-ttc13-screen1: oklch(0.45 0.12 230);
  --color-ttc13-screen2: #14203f;
  --color-ttc13-screen2: oklch(0.22 0.06 260);
  --color-ttc13-acc: #52b8e8;
  --color-ttc13-acc: oklch(0.74 0.12 225);
  --color-ttc13-mut: #bcc5d4;
  --color-ttc13-mut: oklch(0.82 0.02 255);
  --font-ttc13-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
  --font-ttc13-mono: 'JetBrains Mono', ui-monospace, monospace;
  --animate-ttc13-bob: ttc13-bob 3.4s ease-in-out infinite;

  @keyframes ttc13-bob {
    50% {
      margin-top: -7px;
    }
  }
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
   stage is sized before the browser build compiles. */

.ttc-13 {
  width: 100%;
  min-height: 100svh;
}
(() => {
  document.querySelectorAll('[data-ttc13-card]').forEach((card) => {
    if (card.dataset.wired) return;
    card.dataset.wired = '1';
    if (!matchMedia('(hover:hover) and (pointer:fine)').matches) return;
    if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
    const MAX = 9; // the phone amplifies this 1.4x through its calc() pose
    card.addEventListener('pointermove', (e) => {
      const r = card.getBoundingClientRect();
      const px = (e.clientX - r.left) / r.width, py = (e.clientY - r.top) / r.height;
      card.style.setProperty('--rx', ((0.5 - py) * MAX).toFixed(2) + 'deg');
      card.style.setProperty('--ry', ((px - 0.5) * MAX).toFixed(2) + 'deg');
      card.setAttribute('data-live', ''); // also fires the specular sweep across the glass
    });
    card.addEventListener('pointerleave', () => {
      card.removeAttribute('data-live');
      card.style.setProperty('--rx', '0deg');
      card.style.setProperty('--ry', '0deg');
    });
  });
})();

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

Compiles in the browser — perfect for a CodePen or a quick test, not for production.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-ttc13-bg: #0d1220;
  --color-ttc13-bg: oklch(0.15 0.02 250);
  --color-ttc13-lobe: #1b2f52e6;
  --color-ttc13-lobe: oklch(0.26 0.06 240 / 0.9);
  --color-ttc13-card1: #182a3f;
  --color-ttc13-card1: oklch(0.22 0.04 235);
  --color-ttc13-card2: #0f1726;
  --color-ttc13-card2: oklch(0.16 0.025 250);
  --color-ttc13-frame: #232a36;
  --color-ttc13-frame: oklch(0.25 0.02 250);
  --color-ttc13-notch: #0b0e14;
  --color-ttc13-notch: oklch(0.12 0.01 250);
  --color-ttc13-screen1: #2e6fa8;
  --color-ttc13-screen1: oklch(0.45 0.12 230);
  --color-ttc13-screen2: #14203f;
  --color-ttc13-screen2: oklch(0.22 0.06 260);
  --color-ttc13-acc: #52b8e8;
  --color-ttc13-acc: oklch(0.74 0.12 225);
  --color-ttc13-mut: #bcc5d4;
  --color-ttc13-mut: oklch(0.82 0.02 255);
  --font-ttc13-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
  --font-ttc13-mono: 'JetBrains Mono', ui-monospace, monospace;
  --animate-ttc13-bob: ttc13-bob 3.4s ease-in-out infinite;

  @keyframes ttc13-bob {
    50% {
      margin-top: -7px;
    }
  }
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
   stage is sized before the browser build compiles. */

.ttc-13 {
  width: 100%;
  min-height: 100svh;
}
3Fonts used by this demooptional — skip if you have your own type stack
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400..800&family=JetBrains+Mono:wght@400..600&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • The phone, notch, screen and streak became four elements' utilities; the screen's ::after sweep is after: + group-data-live/card:after:translate-x-[55%].
  • The translate:-50% 0 notch and margin-left:-75px phone centring became -translate-x-1/2 and -ml-[75px].
  • UI upgrade from the CSS original: the phone now includes perspective(900px) in its own transform — the card's overflow:hidden was flattening it, so the "3D" pose rendered as a flat squash.
  • UI upgrade: a real 44px "Get it free" CTA replaces a tabindex card with no action; the placeholder screen became a readable sleep-timer UI; pitch copy rose from 60% white to oklch(0.82).
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind 3D Floating Phone Mockup Card for App Landing Pages
Source: https://codefronts.com/components/tailwind-css-3d-tilt-hover-cards/tailwind-3d-floating-phone-mockup-card/

An app-landing hero card: a CSS-drawn phone rests at a 3D angle and straightens toward you as the cursor approaches, while rating and download badges bob in front of it and a specular streak sweeps the glass. The port fixes a flattening bug — the phone now carries its own perspective(900px), because the card's overflow-hidden was collapsing its rotation to a squash — and adds a real download CTA.
## HTML
```html
<section class="ttc-13 w-full min-h-svh grid place-items-center bg-ttc13-bg bg-[radial-gradient(90%_100%_at_20%_0%,var(--color-ttc13-lobe),transparent_60%)] px-6 py-14 font-ttc13-sans text-white" aria-label="3D floating phone mockup card demo">
  <div class="grid w-full justify-items-center gap-8">
    <article data-ttc13-card class="group/card relative w-full max-w-[20rem] overflow-hidden rounded-[26px] border border-white/10 bg-linear-170 from-ttc13-card1 to-ttc13-card2 px-6 pt-6 shadow-[0_32px_64px_-28px_oklch(0_0_0/0.85)] [transform:perspective(1400px)_rotateX(var(--rx,0deg))_rotateY(var(--ry,0deg))] transition-transform duration-600 ease-[cubic-bezier(0.2,0.9,0.3,1.1)] data-live:duration-80 data-live:ease-linear motion-reduce:transition-none">
      <div class="flex items-start justify-between gap-3">
        <div>
          <h3 class="text-[24px] leading-none font-extrabold tracking-[-0.02em]">Tidepool</h3>
          <p class="mt-1.5 text-[13.5px] text-ttc13-mut">Sleep sounds, tuned to your night.</p>
        </div>
        <a href="#" class="inline-flex min-h-11 flex-none items-center rounded-full bg-white px-4 text-[13px] font-bold text-ttc13-bg no-underline transition-transform hover:scale-105 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-ttc13-acc">Get it free</a>
      </div>
      <div aria-hidden="true" class="relative mt-5 h-64">
        <div class="absolute -bottom-10 left-1/2 -ml-[75px] h-[300px] w-[150px] rounded-[30px] border border-white/18 bg-ttc13-frame p-[9px] shadow-[24px_30px_50px_-20px_oklch(0_0_0/0.7)] [transform:perspective(900px)_rotateY(calc(-18deg_+_var(--ry,0deg)*1.4))_rotateX(calc(8deg_+_var(--rx,0deg)*0.6))] transition-transform duration-600 ease-[cubic-bezier(0.2,0.9,0.3,1.1)] group-data-live/card:duration-80 group-data-live/card:ease-linear motion-reduce:transition-none">
          <span class="absolute top-4 left-1/2 z-10 h-[9px] w-11 -translate-x-1/2 rounded-full bg-ttc13-notch"></span>
          <div class="relative flex size-full flex-col items-center overflow-hidden rounded-[22px] bg-linear-190 from-ttc13-screen1 to-ttc13-screen2 px-3 pt-9 after:absolute after:-inset-[60%] after:-translate-x-[70%] after:bg-[linear-gradient(115deg,transparent_44%,oklch(1_0_0/0.28)_50%,transparent_56%)] after:transition-transform after:duration-900 after:content-[''] group-data-live/card:after:translate-x-[55%]">
            <p class="text-[22px] leading-none font-bold tabular-nums">23:42</p>
            <p class="mt-1 text-[9px] font-semibold tracking-[0.14em] text-white/75">OCEAN DRIFT · 45 MIN</p>
            <div class="mt-5 size-20 rounded-full bg-[conic-gradient(var(--color-ttc13-acc)_0_62%,oklch(1_0_0/0.14)_62%)] p-[5px]"><div class="size-full rounded-full bg-ttc13-screen2"></div></div>
            <div class="mt-5 h-6 w-full rounded-lg bg-white/16"></div>
            <div class="mt-2 h-6 w-full rounded-lg bg-white/10"></div>
          </div>
        </div>
        <span class="absolute top-5 left-0 rounded-full border border-white/20 bg-white/10 px-3 py-2 text-[11.5px] font-bold backdrop-blur-sm animate-ttc13-bob motion-reduce:animate-none">★ 4.9 rating</span>
        <span class="absolute top-28 right-0 rounded-full border border-white/20 bg-white/10 px-3 py-2 text-[11.5px] font-bold backdrop-blur-sm animate-ttc13-bob [animation-delay:-1.7s] motion-reduce:animate-none">2M+ downloads</span>
      </div>
    </article>
    <p aria-hidden="true" class="rounded-full border border-white/12 bg-white/5 px-3.5 py-2 font-ttc13-mono text-[11.5px] text-ttc13-mut">perspective(900px)_rotateY(calc(-18deg_+_var(--ry)*1.4))</p>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-ttc13-bg: #0d1220;
  --color-ttc13-bg: oklch(0.15 0.02 250);
  --color-ttc13-lobe: #1b2f52e6;
  --color-ttc13-lobe: oklch(0.26 0.06 240 / 0.9);
  --color-ttc13-card1: #182a3f;
  --color-ttc13-card1: oklch(0.22 0.04 235);
  --color-ttc13-card2: #0f1726;
  --color-ttc13-card2: oklch(0.16 0.025 250);
  --color-ttc13-frame: #232a36;
  --color-ttc13-frame: oklch(0.25 0.02 250);
  --color-ttc13-notch: #0b0e14;
  --color-ttc13-notch: oklch(0.12 0.01 250);
  --color-ttc13-screen1: #2e6fa8;
  --color-ttc13-screen1: oklch(0.45 0.12 230);
  --color-ttc13-screen2: #14203f;
  --color-ttc13-screen2: oklch(0.22 0.06 260);
  --color-ttc13-acc: #52b8e8;
  --color-ttc13-acc: oklch(0.74 0.12 225);
  --color-ttc13-mut: #bcc5d4;
  --color-ttc13-mut: oklch(0.82 0.02 255);
  --font-ttc13-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
  --font-ttc13-mono: 'JetBrains Mono', ui-monospace, monospace;
  --animate-ttc13-bob: ttc13-bob 3.4s ease-in-out infinite;

  @keyframes ttc13-bob {
    50% {
      margin-top: -7px;
    }
  }
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
   stage is sized before the browser build compiles. */

.ttc-13 {
  width: 100%;
  min-height: 100svh;
}
```

## JavaScript
```js
(() => {
  document.querySelectorAll('[data-ttc13-card]').forEach((card) => {
    if (card.dataset.wired) return;
    card.dataset.wired = '1';
    if (!matchMedia('(hover:hover) and (pointer:fine)').matches) return;
    if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
    const MAX = 9; // the phone amplifies this 1.4x through its calc() pose
    card.addEventListener('pointermove', (e) => {
      const r = card.getBoundingClientRect();
      const px = (e.clientX - r.left) / r.width, py = (e.clientY - r.top) / r.height;
      card.style.setProperty('--rx', ((0.5 - py) * MAX).toFixed(2) + 'deg');
      card.style.setProperty('--ry', ((px - 0.5) * MAX).toFixed(2) + 'deg');
      card.setAttribute('data-live', ''); // also fires the specular sweep across the glass
    });
    card.addEventListener('pointerleave', () => {
      card.removeAttribute('data-live');
      card.style.setProperty('--rx', '0deg');
      card.style.setProperty('--ry', '0deg');
    });
  });
})();
```

How this works

Resting pose plus additive tilt. The phone starts at rotateY(-18deg) rotateX(8deg); the pointer's values are ADDED through calc() and amplified 1.4×, so hovering right straightens the device to face you and hovering left deepens the angle.

Why the phone owns its perspective. The card clips with overflow-hidden (the phone rises from the bottom edge), and overflow clipping flattens preserve-3d. In the original that silently turned the phone's rotateY into a flat horizontal squash. Putting perspective(900px) first in the phone's own transform restores real foreshortening regardless of the parent.

The sweep is an after: gradient stripe on the screen that transitions its translate across the glass over 900ms whenever data-live is set.

Make it yours

  • Replace the drawn screen with a real screenshot: an <img> with rounded-[inherit] object-cover inside the screen.
  • Re-pose via the two calc() base angles — try -25deg/10deg for drama.
  • Add a third badge (award, press quote) with a different bob delay.
  • Point the streak's 115deg angle at your page's global light direction.

Gotchas — read before shipping

  • Pose and tilt must live in ONE transform; a posed wrapper plus a tilted child doubles the perspective and warps the phone.
  • Bob on margin-top or translate-y, never Z — Z-bobbing swims against the parallax.
  • Keep the phone radius ≥ 28px at this size or the notch pill visually detaches.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

Broadly supported 3D transforms; the floor is Tailwind v4's sheet.

Techniques used in this demo

Arbitraryneeds bracket syntax

Plain CSSno Tailwind surface

Search CodeFronts

Loading…