3D Pin Card — Free HTML CSS JS Perspective Hover Snippet

3D Pin Card · Cards · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Real 3D perspective
A perspective stage gives transforms true depth.
preserve-3d layering
Card, label, beam, and rings hold their own depth.
Backward card tilt
rotateX lays the card into the scene on hover.
Floating pin label
translateZ lifts the label above the card.
Depth-axis beam
A rotated shaft reads as the pin stem.
Pinging base rings
Staggered ripples expand around the pin.
Cursor-tracking lean
pointermove tilts the assembly toward the mouse.
Clipboard link
Clicking copies the label as a share handle.

About this UI Snippet

3D Pin Card — Perspective Tilt with a Rising Anchor Pin

Screenshot of the 3D Pin Card snippet rendered live

The 3D pin card is the standout hover interaction from modern component libraries: a card that tilts back in three dimensions when you hover it, while a floating "pin" — a label, a beam, and expanding rings — rises above it as if pinning the card to a point in space. This snippet recreates the whole illusion with plain HTML, CSS 3D transforms, and a little vanilla JavaScript for cursor tracking.

Establishing 3D space

The effect lives inside a perspective: 1000px stage, which is what gives child transforms real depth — without it, rotateX would look like a flat squash. The pin wrapper uses transform-style: preserve-3d so its children (the card, the label, the beam, the rings) each keep their own position in 3D rather than being flattened into the wrapper's plane. This is the foundational setup for any layered 3D effect.

The card's backward tilt

On hover, the inner card rotates with rotateX(28deg), tipping its top edge away from you so you appear to look down at it on a surface. Because the perspective is set on the parent, this reads as the card laying back into the scene. The transition makes it ease in and out smoothly. A radial gradient overlay in the top corner adds a subtle light source consistent with the tilt.

The pin: label, beam, and rings

What makes this more than a tilt is the pin that appears to hold the card. Three elements animate in on hover: a rounded .pc-label pill that floats up and forward via translateZ(60px) so it hovers above the card in 3D; a thin .pc-beam that grows in height with rotateX(70deg) so it lies along the depth axis like a pin's shaft; and two .pc-ring circles, also rotated 70° into the floor plane, that pcPing outward and fade on a staggered loop, like ripples around the pin's base. Together they create the impression of a physical pin staking the card to the ground.

Cursor-tracking tilt

On top of the base rotateX, JavaScript adds pointer-aware lean. A pointermove handler converts the cursor's position within the card into a -0.5…0.5 range and applies rotateY and rotateX to the wrapper, so the whole assembly — card and pin — leans toward the cursor. On pointerleave it resets to flat. Because the wrapper has preserve-3d, the pin elements track the lean correctly, keeping the illusion coherent from any angle.

A real link affordance

The component is an <a>, and the label reads like a URL, so it behaves like a clickable, pinned link. Clicking copies the label text via the navigator.clipboard API and briefly swaps the label to "Copied ✓" — demonstrating how the pin can double as a shareable handle. Swap this for real navigation by removing the preventDefault.

Customizing it

Tune the rotateX(28deg) for a steeper or gentler tilt, change the translateZ to float the label higher, adjust the ring pcPing size and timing, and set the label text to your link. Recolor the beam, rings, and gradient to your accent. Pair it with a focus cards grid or a meteor card for a set of eye-catching 3D cards.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA card renders flat with a hidden pin label above it.
  2. 2
    Hover the cardIt tilts back in 3D while a pin label, beam, and rings rise.
  3. 3
    Move the cursorThe whole assembly leans toward the pointer.
  4. 4
    Watch the ringsTwo circles ping outward around the pin base on a loop.
  5. 5
    Click the cardThe label URL copies and shows a Copied confirmation.
  6. 6
    Set your linkChange the label text and remove preventDefault to navigate.

Real-world uses

Common Use Cases

Featured links
Showcase a profile beside a focus cards grid.
Portfolio highlights
Pin a project above a portfolio hero.
Product spotlights
Pair with a meteor card for variety.
Profile cards
A 3D upgrade to a flat profile card.
Link-in-bio pages
Make a shareable handle the centerpiece.
3D transform demos
A reference for perspective and preserve-3d.

Got questions?

Frequently Asked Questions

perspective: 1000px on the stage is what gives 3D transforms real depth — it defines how strongly things recede. Without it, rotateX(28deg) just looks like a vertical squash. The pin wrapper also uses transform-style: preserve-3d so the card, label, beam, and rings each keep their own 3D position instead of flattening into one plane.

Three elements animate in on hover: a label pill pushed forward with translateZ(60px) so it hovers above the card, a thin beam rotated 70 degrees into the depth axis that grows in height like a pin shaft, and two rings also rotated into the floor plane that expand and fade on a staggered ping loop, reading as ripples around the pin's base.

A pointermove handler converts the cursor's position inside the card to a -0.5 to 0.5 range and applies rotateY and rotateX to the wrapper, so the whole assembly leans toward the pointer on top of the base tilt. pointerleave resets it to flat. Because the wrapper is preserve-3d, the pin elements lean coherently with the card.

The card is an anchor with a URL-style label. The click handler prevents navigation, copies the label text via navigator.clipboard, and briefly swaps it to Copied so it acts as a shareable handle. To use it as a normal link instead, remove the preventDefault and set a real href.

Render the structure as a component and keep the perspective and preserve-3d CSS. Handle pointermove and pointerleave with framework events, writing the transform via a ref or state-driven inline style. Use the clipboard API in the click handler. The hover-driven pin animations are pure CSS and need no JS. In Tailwind, use perspective and transform utilities with arbitrary translate-z and rotate-x values.

3D Pin Card — Export as HTML, React, Vue, Angular & Tailwind

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>3D Pin Card</title>
  <style>
    *{box-sizing:border-box;margin:0;padding:0}
    body{font-family:system-ui,-apple-system,sans-serif;background:#06060c;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh}
    
    .pc-stage{perspective:1000px;padding:40px}
    .pc-pin{position:relative;display:block;width:260px;text-decoration:none;color:inherit;transform-style:preserve-3d;transition:transform .3s ease}
    
    .pc-card{position:relative;border-radius:18px;padding:24px;background:#101019;border:1px solid #23233a;transform:translateZ(0);transition:transform .3s ease;overflow:hidden}
    .pc-pin:hover .pc-card{transform:rotateX(28deg) translateZ(0)}
    .pc-card h3{font-size:20px;font-weight:800;margin-bottom:8px;position:relative;z-index:1}
    .pc-card p{font-size:13.5px;color:#a3a3bd;line-height:1.55;position:relative;z-index:1}
    .pc-grad{position:absolute;inset:0;background:radial-gradient(circle at 30% 0,rgba(129,140,248,.35),transparent 60%)}
    
    /* The pin label, beam, and rings appear above the tilting card on hover */
    .pc-label{position:absolute;top:-46px;left:50%;transform:translateX(-50%) translateZ(0);opacity:0;font-size:11px;font-weight:700;letter-spacing:.03em;background:#1e1b4b;border:1px solid #4f46e5;color:#c7d2fe;padding:5px 12px;border-radius:999px;white-space:nowrap;transition:opacity .3s,transform .3s;pointer-events:none}
    .pc-pin:hover .pc-label{opacity:1;transform:translateX(-50%) translateY(-6px) translateZ(60px)}
    
    .pc-beam{position:absolute;top:-22px;left:50%;width:1px;height:0;background:linear-gradient(180deg,#818cf8,transparent);transform:translateX(-50%) rotateX(70deg);transform-origin:top;opacity:0;transition:height .35s ease,opacity .3s;pointer-events:none}
    .pc-pin:hover .pc-beam{height:90px;opacity:1}
    
    .pc-ring{position:absolute;top:64px;left:50%;width:8px;height:8px;border-radius:50%;border:1px solid rgba(129,140,248,.6);transform:translate(-50%,-50%) rotateX(70deg);opacity:0;pointer-events:none}
    .pc-pin:hover .pc-ring{animation:pcPing 2.4s ease-out infinite}
    .pc-pin:hover .pc-ring2{animation-delay:1.2s}
    @keyframes pcPing{0%{opacity:.7;width:8px;height:8px}100%{opacity:0;width:90px;height:90px}}
  </style>
</head>
<body>
  <div class="pc-stage">
    <a href="#" class="pc-pin" id="pcPin">
      <span class="pc-label" id="pcLabel">/aceternity.dev</span>
      <span class="pc-beam" aria-hidden="true"></span>
      <span class="pc-ring" aria-hidden="true"></span>
      <span class="pc-ring pc-ring2" aria-hidden="true"></span>
      <div class="pc-card">
        <h3>Build the future</h3>
        <p>Hover this card — it tilts in 3D and a perspective pin rises to anchor it in space.</p>
        <div class="pc-grad"></div>
      </div>
    </a>
  </div>
  <script>
    var pin = document.getElementById('pcPin');
    var label = document.getElementById('pcLabel');
    
    // Pointer-aware tilt: the card leans toward the cursor while hovered, layered
    // on top of the base rotateX so the 3D pin illusion tracks the mouse.
    pin.addEventListener('pointermove', function (e) {
      var r = pin.getBoundingClientRect();
      var px = (e.clientX - r.left) / r.width - 0.5;   // -0.5 .. 0.5
      var py = (e.clientY - r.top) / r.height - 0.5;
      pin.style.transform = 'rotateY(' + (px * 14) + 'deg) rotateX(' + (-py * 10) + 'deg)';
    });
    pin.addEventListener('pointerleave', function () {
      pin.style.transform = 'rotateY(0) rotateX(0)';
    });
    
    // Clicking copies the label "url" to show the pin as a real link affordance.
    pin.addEventListener('click', function (e) {
      e.preventDefault();
      if (navigator.clipboard) {
        navigator.clipboard.writeText(label.textContent.trim()).then(function () {
          var prev = label.textContent;
          label.textContent = 'Copied ✓';
          setTimeout(function () { label.textContent = prev; }, 1100);
        });
      }
    });
  </script>
</body>
</html>
Tailwind
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>3D Pin Card</title>
  <!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
  <script src="https://cdn.tailwindcss.com"></script>
  <style>
    @keyframes pcPing{0%{opacity:.7;width:8px;height:8px}100%{opacity:0;width:90px;height:90px}}

    * {
      box-sizing:border-box;margin:0;padding:0
    }

    body {
      font-family:system-ui,-apple-system,sans-serif;background:#06060c;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh
    }

    .pc-pin:hover .pc-card {
      transform:rotateX(28deg) translateZ(0)
    }

    .pc-card h3 {
      font-size:20px;font-weight:800;margin-bottom:8px;position:relative;z-index:1
    }

    .pc-card p {
      font-size:13.5px;color:#a3a3bd;line-height:1.55;position:relative;z-index:1
    }

    .pc-pin:hover .pc-label {
      opacity:1;transform:translateX(-50%) translateY(-6px) translateZ(60px)
    }

    .pc-pin:hover .pc-beam {
      height:90px;opacity:1
    }

    .pc-pin:hover .pc-ring {
      animation:pcPing 2.4s ease-out infinite
    }

    .pc-pin:hover .pc-ring2 {
      animation-delay:1.2s
    }
  </style>
</head>
<body>
  <div class="[perspective:1000px] p-10">
    <a href="#" class="pc-pin relative block w-[260px] no-underline text-inherit [transform-style:preserve-3d] [transition:transform_.3s_ease]" id="pcPin">
      <span class="pc-label absolute top-[-46px] left-1/2 [transform:translateX(-50%)_translateZ(0)] opacity-0 text-[11px] font-bold tracking-[.03em] bg-[#1e1b4b] border border-[#4f46e5] text-[#c7d2fe] py-[5px] px-3 rounded-[999px] whitespace-nowrap [transition:opacity_.3s,transform_.3s] pointer-events-none" id="pcLabel">/aceternity.dev</span>
      <span class="pc-beam absolute top-[-22px] left-1/2 w-px h-0 [background:linear-gradient(180deg,#818cf8,transparent)] [transform:translateX(-50%)_rotateX(70deg)] [transform-origin:top] opacity-0 [transition:height_.35s_ease,opacity_.3s] pointer-events-none" aria-hidden="true"></span>
      <span class="pc-ring absolute top-16 left-1/2 w-2 h-2 rounded-full border border-[rgba(129,140,248,.6)] [transform:translate(-50%,-50%)_rotateX(70deg)] opacity-0 pointer-events-none" aria-hidden="true"></span>
      <span class="pc-ring absolute top-16 left-1/2 w-2 h-2 rounded-full border border-[rgba(129,140,248,.6)] [transform:translate(-50%,-50%)_rotateX(70deg)] opacity-0 pointer-events-none pc-ring2" aria-hidden="true"></span>
      <div class="pc-card relative rounded-[18px] p-6 bg-[#101019] border border-[#23233a] [transform:translateZ(0)] [transition:transform_.3s_ease] overflow-hidden">
        <h3>Build the future</h3>
        <p>Hover this card — it tilts in 3D and a perspective pin rises to anchor it in space.</p>
        <div class="absolute inset-0 [background:radial-gradient(circle_at_30%_0,rgba(129,140,248,.35),transparent_60%)]"></div>
      </div>
    </a>
  </div>
  <script>
    var pin = document.getElementById('pcPin');
    var label = document.getElementById('pcLabel');
    
    // Pointer-aware tilt: the card leans toward the cursor while hovered, layered
    // on top of the base rotateX so the 3D pin illusion tracks the mouse.
    pin.addEventListener('pointermove', function (e) {
      var r = pin.getBoundingClientRect();
      var px = (e.clientX - r.left) / r.width - 0.5;   // -0.5 .. 0.5
      var py = (e.clientY - r.top) / r.height - 0.5;
      pin.style.transform = 'rotateY(' + (px * 14) + 'deg) rotateX(' + (-py * 10) + 'deg)';
    });
    pin.addEventListener('pointerleave', function () {
      pin.style.transform = 'rotateY(0) rotateX(0)';
    });
    
    // Clicking copies the label "url" to show the pin as a real link affordance.
    pin.addEventListener('click', function (e) {
      e.preventDefault();
      if (navigator.clipboard) {
        navigator.clipboard.writeText(label.textContent.trim()).then(function () {
          var prev = label.textContent;
          label.textContent = 'Copied ✓';
          setTimeout(function () { label.textContent = prev; }, 1100);
        });
      }
    });
  </script>
</body>
</html>
React
import React, { useEffect } from 'react';

// CSS — optionally move to Snippet3DPinCard.module.css
const css = `
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#06060c;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh}

.pc-stage{perspective:1000px;padding:40px}
.pc-pin{position:relative;display:block;width:260px;text-decoration:none;color:inherit;transform-style:preserve-3d;transition:transform .3s ease}

.pc-card{position:relative;border-radius:18px;padding:24px;background:#101019;border:1px solid #23233a;transform:translateZ(0);transition:transform .3s ease;overflow:hidden}
.pc-pin:hover .pc-card{transform:rotateX(28deg) translateZ(0)}
.pc-card h3{font-size:20px;font-weight:800;margin-bottom:8px;position:relative;z-index:1}
.pc-card p{font-size:13.5px;color:#a3a3bd;line-height:1.55;position:relative;z-index:1}
.pc-grad{position:absolute;inset:0;background:radial-gradient(circle at 30% 0,rgba(129,140,248,.35),transparent 60%)}

/* The pin label, beam, and rings appear above the tilting card on hover */
.pc-label{position:absolute;top:-46px;left:50%;transform:translateX(-50%) translateZ(0);opacity:0;font-size:11px;font-weight:700;letter-spacing:.03em;background:#1e1b4b;border:1px solid #4f46e5;color:#c7d2fe;padding:5px 12px;border-radius:999px;white-space:nowrap;transition:opacity .3s,transform .3s;pointer-events:none}
.pc-pin:hover .pc-label{opacity:1;transform:translateX(-50%) translateY(-6px) translateZ(60px)}

.pc-beam{position:absolute;top:-22px;left:50%;width:1px;height:0;background:linear-gradient(180deg,#818cf8,transparent);transform:translateX(-50%) rotateX(70deg);transform-origin:top;opacity:0;transition:height .35s ease,opacity .3s;pointer-events:none}
.pc-pin:hover .pc-beam{height:90px;opacity:1}

.pc-ring{position:absolute;top:64px;left:50%;width:8px;height:8px;border-radius:50%;border:1px solid rgba(129,140,248,.6);transform:translate(-50%,-50%) rotateX(70deg);opacity:0;pointer-events:none}
.pc-pin:hover .pc-ring{animation:pcPing 2.4s ease-out infinite}
.pc-pin:hover .pc-ring2{animation-delay:1.2s}
@keyframes pcPing{0%{opacity:.7;width:8px;height:8px}100%{opacity:0;width:90px;height:90px}}
`;

export default function Snippet3DPinCard() {
  // Auto-generated escape hatch: the original snippet's vanilla JS runs once
  // after mount and queries the rendered DOM. For idiomatic React, lift this
  // into state + handlers.
  useEffect(() => {
    const _listeners = [];
    const _originalAddEventListener = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function(type, listener, options) {
      _listeners.push({ target: this, type, listener, options });
      return _originalAddEventListener.call(this, type, listener, options);
    };
    var pin = document.getElementById('pcPin');
    var label = document.getElementById('pcLabel');
    
    // Pointer-aware tilt: the card leans toward the cursor while hovered, layered
    // on top of the base rotateX so the 3D pin illusion tracks the mouse.
    pin.addEventListener('pointermove', function (e) {
      var r = pin.getBoundingClientRect();
      var px = (e.clientX - r.left) / r.width - 0.5;   // -0.5 .. 0.5
      var py = (e.clientY - r.top) / r.height - 0.5;
      pin.style.transform = 'rotateY(' + (px * 14) + 'deg) rotateX(' + (-py * 10) + 'deg)';
    });
    pin.addEventListener('pointerleave', function () {
      pin.style.transform = 'rotateY(0) rotateX(0)';
    });
    
    // Clicking copies the label "url" to show the pin as a real link affordance.
    pin.addEventListener('click', function (e) {
      e.preventDefault();
      if (navigator.clipboard) {
        navigator.clipboard.writeText(label.textContent.trim()).then(function () {
          var prev = label.textContent;
          label.textContent = 'Copied ✓';
          setTimeout(function () { label.textContent = prev; }, 1100);
        });
      }
    });
    // Cleanup: restore addEventListener and remove all listeners
    return () => {
      EventTarget.prototype.addEventListener = _originalAddEventListener;
      for (const { target, type, listener, options } of _listeners) {
        target.removeEventListener(type, listener, options);
      }
    };
  }, []);

  return (
    <>
      <style>{css}</style>
      <div className="pc-stage">
        <a href="#" className="pc-pin" id="pcPin">
          <span className="pc-label" id="pcLabel">/aceternity.dev</span>
          <span className="pc-beam" aria-hidden="true"></span>
          <span className="pc-ring" aria-hidden="true"></span>
          <span className="pc-ring pc-ring2" aria-hidden="true"></span>
          <div className="pc-card">
            <h3>Build the future</h3>
            <p>Hover this card — it tilts in 3D and a perspective pin rises to anchor it in space.</p>
            <div className="pc-grad"></div>
          </div>
        </a>
      </div>
    </>
  );
}
React + Tailwind
import React, { useEffect } from 'react';
// Requires Tailwind CSS v3+ — https://tailwindcss.com/docs/installation
// Arbitrary value classes (e.g. bg-[#0f172a]) are valid Tailwind v3+

export default function Snippet3DPinCard() {
  // Auto-generated escape hatch: the original snippet's vanilla JS runs once
  // after mount and queries the rendered DOM. For idiomatic React, lift this
  // into state + handlers.
  useEffect(() => {
    const _listeners = [];
    const _originalAddEventListener = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function(type, listener, options) {
      _listeners.push({ target: this, type, listener, options });
      return _originalAddEventListener.call(this, type, listener, options);
    };
    var pin = document.getElementById('pcPin');
    var label = document.getElementById('pcLabel');
    
    // Pointer-aware tilt: the card leans toward the cursor while hovered, layered
    // on top of the base rotateX so the 3D pin illusion tracks the mouse.
    pin.addEventListener('pointermove', function (e) {
      var r = pin.getBoundingClientRect();
      var px = (e.clientX - r.left) / r.width - 0.5;   // -0.5 .. 0.5
      var py = (e.clientY - r.top) / r.height - 0.5;
      pin.style.transform = 'rotateY(' + (px * 14) + 'deg) rotateX(' + (-py * 10) + 'deg)';
    });
    pin.addEventListener('pointerleave', function () {
      pin.style.transform = 'rotateY(0) rotateX(0)';
    });
    
    // Clicking copies the label "url" to show the pin as a real link affordance.
    pin.addEventListener('click', function (e) {
      e.preventDefault();
      if (navigator.clipboard) {
        navigator.clipboard.writeText(label.textContent.trim()).then(function () {
          var prev = label.textContent;
          label.textContent = 'Copied ✓';
          setTimeout(function () { label.textContent = prev; }, 1100);
        });
      }
    });
    // Cleanup: restore addEventListener and remove all listeners
    return () => {
      EventTarget.prototype.addEventListener = _originalAddEventListener;
      for (const { target, type, listener, options } of _listeners) {
        target.removeEventListener(type, listener, options);
      }
    };
  }, []);

  return (
    <>
      <style>{`
@keyframes pcPing{0%{opacity:.7;width:8px;height:8px}100%{opacity:0;width:90px;height:90px}}

* {
  box-sizing:border-box;margin:0;padding:0
}

body {
  font-family:system-ui,-apple-system,sans-serif;background:#06060c;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh
}

.pc-pin:hover .pc-card {
  transform:rotateX(28deg) translateZ(0)
}

.pc-card h3 {
  font-size:20px;font-weight:800;margin-bottom:8px;position:relative;z-index:1
}

.pc-card p {
  font-size:13.5px;color:#a3a3bd;line-height:1.55;position:relative;z-index:1
}

.pc-pin:hover .pc-label {
  opacity:1;transform:translateX(-50%) translateY(-6px) translateZ(60px)
}

.pc-pin:hover .pc-beam {
  height:90px;opacity:1
}

.pc-pin:hover .pc-ring {
  animation:pcPing 2.4s ease-out infinite
}

.pc-pin:hover .pc-ring2 {
  animation-delay:1.2s
}
      `}</style>
      <div className="[perspective:1000px] p-10">
        <a href="#" className="pc-pin relative block w-[260px] no-underline text-inherit [transform-style:preserve-3d] [transition:transform_.3s_ease]" id="pcPin">
          <span className="pc-label absolute top-[-46px] left-1/2 [transform:translateX(-50%)_translateZ(0)] opacity-0 text-[11px] font-bold tracking-[.03em] bg-[#1e1b4b] border border-[#4f46e5] text-[#c7d2fe] py-[5px] px-3 rounded-[999px] whitespace-nowrap [transition:opacity_.3s,transform_.3s] pointer-events-none" id="pcLabel">/aceternity.dev</span>
          <span className="pc-beam absolute top-[-22px] left-1/2 w-px h-0 [background:linear-gradient(180deg,#818cf8,transparent)] [transform:translateX(-50%)_rotateX(70deg)] [transform-origin:top] opacity-0 [transition:height_.35s_ease,opacity_.3s] pointer-events-none" aria-hidden="true"></span>
          <span className="pc-ring absolute top-16 left-1/2 w-2 h-2 rounded-full border border-[rgba(129,140,248,.6)] [transform:translate(-50%,-50%)_rotateX(70deg)] opacity-0 pointer-events-none" aria-hidden="true"></span>
          <span className="pc-ring absolute top-16 left-1/2 w-2 h-2 rounded-full border border-[rgba(129,140,248,.6)] [transform:translate(-50%,-50%)_rotateX(70deg)] opacity-0 pointer-events-none pc-ring2" aria-hidden="true"></span>
          <div className="pc-card relative rounded-[18px] p-6 bg-[#101019] border border-[#23233a] [transform:translateZ(0)] [transition:transform_.3s_ease] overflow-hidden">
            <h3>Build the future</h3>
            <p>Hover this card — it tilts in 3D and a perspective pin rises to anchor it in space.</p>
            <div className="absolute inset-0 [background:radial-gradient(circle_at_30%_0,rgba(129,140,248,.35),transparent_60%)]"></div>
          </div>
        </a>
      </div>
    </>
  );
}
Vue
<template>
  <div class="pc-stage">
    <a href="#" class="pc-pin" id="pcPin">
      <span class="pc-label" id="pcLabel">/aceternity.dev</span>
      <span class="pc-beam" aria-hidden="true"></span>
      <span class="pc-ring" aria-hidden="true"></span>
      <span class="pc-ring pc-ring2" aria-hidden="true"></span>
      <div class="pc-card">
        <h3>Build the future</h3>
        <p>Hover this card — it tilts in 3D and a perspective pin rises to anchor it in space.</p>
        <div class="pc-grad"></div>
      </div>
    </a>
  </div>
</template>

<script setup>
import { onMounted } from 'vue';

let pin;
let label;

onMounted(() => {
  pin = document.getElementById('pcPin');
  label = document.getElementById('pcLabel');
  // Pointer-aware tilt: the card leans toward the cursor while hovered, layered
  // on top of the base rotateX so the 3D pin illusion tracks the mouse.
  pin.addEventListener('pointermove', function (e) {
    var r = pin.getBoundingClientRect();
    var px = (e.clientX - r.left) / r.width - 0.5;   // -0.5 .. 0.5
    var py = (e.clientY - r.top) / r.height - 0.5;
    pin.style.transform = 'rotateY(' + (px * 14) + 'deg) rotateX(' + (-py * 10) + 'deg)';
  });
  pin.addEventListener('pointerleave', function () {
    pin.style.transform = 'rotateY(0) rotateX(0)';
  });
  // Clicking copies the label "url" to show the pin as a real link affordance.
  pin.addEventListener('click', function (e) {
    e.preventDefault();
    if (navigator.clipboard) {
      navigator.clipboard.writeText(label.textContent.trim()).then(function () {
        var prev = label.textContent;
        label.textContent = 'Copied ✓';
        setTimeout(function () { label.textContent = prev; }, 1100);
      });
    }
  });
});
</script>

<style scoped>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#06060c;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh}

.pc-stage{perspective:1000px;padding:40px}
.pc-pin{position:relative;display:block;width:260px;text-decoration:none;color:inherit;transform-style:preserve-3d;transition:transform .3s ease}

.pc-card{position:relative;border-radius:18px;padding:24px;background:#101019;border:1px solid #23233a;transform:translateZ(0);transition:transform .3s ease;overflow:hidden}
.pc-pin:hover .pc-card{transform:rotateX(28deg) translateZ(0)}
.pc-card h3{font-size:20px;font-weight:800;margin-bottom:8px;position:relative;z-index:1}
.pc-card p{font-size:13.5px;color:#a3a3bd;line-height:1.55;position:relative;z-index:1}
.pc-grad{position:absolute;inset:0;background:radial-gradient(circle at 30% 0,rgba(129,140,248,.35),transparent 60%)}

/* The pin label, beam, and rings appear above the tilting card on hover */
.pc-label{position:absolute;top:-46px;left:50%;transform:translateX(-50%) translateZ(0);opacity:0;font-size:11px;font-weight:700;letter-spacing:.03em;background:#1e1b4b;border:1px solid #4f46e5;color:#c7d2fe;padding:5px 12px;border-radius:999px;white-space:nowrap;transition:opacity .3s,transform .3s;pointer-events:none}
.pc-pin:hover .pc-label{opacity:1;transform:translateX(-50%) translateY(-6px) translateZ(60px)}

.pc-beam{position:absolute;top:-22px;left:50%;width:1px;height:0;background:linear-gradient(180deg,#818cf8,transparent);transform:translateX(-50%) rotateX(70deg);transform-origin:top;opacity:0;transition:height .35s ease,opacity .3s;pointer-events:none}
.pc-pin:hover .pc-beam{height:90px;opacity:1}

.pc-ring{position:absolute;top:64px;left:50%;width:8px;height:8px;border-radius:50%;border:1px solid rgba(129,140,248,.6);transform:translate(-50%,-50%) rotateX(70deg);opacity:0;pointer-events:none}
.pc-pin:hover .pc-ring{animation:pcPing 2.4s ease-out infinite}
.pc-pin:hover .pc-ring2{animation-delay:1.2s}
@keyframes pcPing{0%{opacity:.7;width:8px;height:8px}100%{opacity:0;width:90px;height:90px}}
</style>
Angular
// @ts-nocheck
// Note: vanilla JS DOM manipulation is preserved as-is inside ngAfterViewInit().
// For idiomatic Angular, replace document.getElementById() with @ViewChild() refs
// and move state into component properties with two-way binding.
import { Component, AfterViewInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-snippet3-d-pin-card',
  standalone: true,
  imports: [CommonModule],
  encapsulation: ViewEncapsulation.None,
  template: `
    <div class="pc-stage">
      <a href="#" class="pc-pin" id="pcPin">
        <span class="pc-label" id="pcLabel">/aceternity.dev</span>
        <span class="pc-beam" aria-hidden="true"></span>
        <span class="pc-ring" aria-hidden="true"></span>
        <span class="pc-ring pc-ring2" aria-hidden="true"></span>
        <div class="pc-card">
          <h3>Build the future</h3>
          <p>Hover this card — it tilts in 3D and a perspective pin rises to anchor it in space.</p>
          <div class="pc-grad"></div>
        </div>
      </a>
    </div>
  `,
  styles: [`
    *{box-sizing:border-box;margin:0;padding:0}
    body{font-family:system-ui,-apple-system,sans-serif;background:#06060c;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh}
    
    .pc-stage{perspective:1000px;padding:40px}
    .pc-pin{position:relative;display:block;width:260px;text-decoration:none;color:inherit;transform-style:preserve-3d;transition:transform .3s ease}
    
    .pc-card{position:relative;border-radius:18px;padding:24px;background:#101019;border:1px solid #23233a;transform:translateZ(0);transition:transform .3s ease;overflow:hidden}
    .pc-pin:hover .pc-card{transform:rotateX(28deg) translateZ(0)}
    .pc-card h3{font-size:20px;font-weight:800;margin-bottom:8px;position:relative;z-index:1}
    .pc-card p{font-size:13.5px;color:#a3a3bd;line-height:1.55;position:relative;z-index:1}
    .pc-grad{position:absolute;inset:0;background:radial-gradient(circle at 30% 0,rgba(129,140,248,.35),transparent 60%)}
    
    /* The pin label, beam, and rings appear above the tilting card on hover */
    .pc-label{position:absolute;top:-46px;left:50%;transform:translateX(-50%) translateZ(0);opacity:0;font-size:11px;font-weight:700;letter-spacing:.03em;background:#1e1b4b;border:1px solid #4f46e5;color:#c7d2fe;padding:5px 12px;border-radius:999px;white-space:nowrap;transition:opacity .3s,transform .3s;pointer-events:none}
    .pc-pin:hover .pc-label{opacity:1;transform:translateX(-50%) translateY(-6px) translateZ(60px)}
    
    .pc-beam{position:absolute;top:-22px;left:50%;width:1px;height:0;background:linear-gradient(180deg,#818cf8,transparent);transform:translateX(-50%) rotateX(70deg);transform-origin:top;opacity:0;transition:height .35s ease,opacity .3s;pointer-events:none}
    .pc-pin:hover .pc-beam{height:90px;opacity:1}
    
    .pc-ring{position:absolute;top:64px;left:50%;width:8px;height:8px;border-radius:50%;border:1px solid rgba(129,140,248,.6);transform:translate(-50%,-50%) rotateX(70deg);opacity:0;pointer-events:none}
    .pc-pin:hover .pc-ring{animation:pcPing 2.4s ease-out infinite}
    .pc-pin:hover .pc-ring2{animation-delay:1.2s}
    @keyframes pcPing{0%{opacity:.7;width:8px;height:8px}100%{opacity:0;width:90px;height:90px}}
  `]
})
export class Snippet3DPinCardComponent implements AfterViewInit {
  ngAfterViewInit(): void {
    var pin = document.getElementById('pcPin');
    var label = document.getElementById('pcLabel');
    
    // Pointer-aware tilt: the card leans toward the cursor while hovered, layered
    // on top of the base rotateX so the 3D pin illusion tracks the mouse.
    pin.addEventListener('pointermove', function (e) {
      var r = pin.getBoundingClientRect();
      var px = (e.clientX - r.left) / r.width - 0.5;   // -0.5 .. 0.5
      var py = (e.clientY - r.top) / r.height - 0.5;
      pin.style.transform = 'rotateY(' + (px * 14) + 'deg) rotateX(' + (-py * 10) + 'deg)';
    });
    pin.addEventListener('pointerleave', function () {
      pin.style.transform = 'rotateY(0) rotateX(0)';
    });
    
    // Clicking copies the label "url" to show the pin as a real link affordance.
    pin.addEventListener('click', function (e) {
      e.preventDefault();
      if (navigator.clipboard) {
        navigator.clipboard.writeText(label.textContent.trim()).then(function () {
          var prev = label.textContent;
          label.textContent = 'Copied ✓';
          setTimeout(function () { label.textContent = prev; }, 1100);
        });
      }
    });
  }
}