Introduction
This card reads as a single glass object floating over a gradient surface, but it is really three flat layers stacked on the z-axis inside one perspective wrapper: a colored surface at the base, a frosted glass panel raised 25px above it, and a cluster of five translucent rings raised even higher, each at its own depth. Nothing here is a 3D model — every 'depth' is just a translate3d(0, 0, Npx) inside a parent with transform-style: preserve-3d, and the tilt on hover is one rotate3d call on the outermost surface.
The illusion depends on three things happening together: a perspective value on the wrapper so child transforms actually recede or approach the viewer instead of just scaling flatly, preserve-3d on every layer in the stack so children keep their own z-offsets instead of being flattened into their parent's plane, and a hover/focus state that only changes one rotation on the outer surface — every ring and the footer already have their resting z-depth baked in, so the tilt reveals depth that was there all along rather than animating it into existence.
How perspective on a wrapper and preserve-3d on its children turn flat translate3d offsets into real depth — how rotate3d(1, 1, 0, 28deg) tilts a card on a diagonal axis rather than a flat X or Y spin — how backdrop-blur and a top-to-bottom white gradient fake a frosted glass panel without any image — how staggering five rings across both z-depth and transition-delay reads as parallax with a single shared hover trigger — and how :focus-within lets the exact same tilt work for a keyboard user tabbing onto the card's own buttons.
Step 1 — A Wrapper That Only Holds Perspective
The outermost element, .orb-card, does nothing visual itself — its only job is to set perspective: 1000px, which becomes the vanishing point every descendant transform is measured against. The actual surface, .orb-card__surface, is the element that receives the gradient background and the rotate3d on hover. Splitting these two responsibilities matters: if perspective and the rotating transform lived on the same element, the element would be rotating relative to its own vanishing point instead of a fixed one, and the tilt would feel wrong at the edges.
.orb-card {
width: 340px;
height: 300px;
perspective: 1000px; /* vanishing point lives on the wrapper */
}
.orb-card__surface {
height: 100%;
border-radius: 50px;
background: var(--card-gradient);
transform-style: preserve-3d; /* let children keep their own z-depth */
transition: transform .55s cubic-bezier(.22, 1, .36, 1);
}
.orb-card:hover .orb-card__surface,
.orb-card:focus-within .orb-card__surface {
transform: rotate3d(1, 1, 0, 28deg); /* diagonal axis, not a flat X/Y spin */
}rotate3d(1, 1, 0, 28deg) spins the surface around the line x = y — the card tips away from the viewer on a diagonal, which is what makes the fall feel like a single dropped object rather than a flat card rotating on a hinge.
Step 2 — Faking Glass With a Gradient and a Blur
The .orb-card__glass panel sits inset 8px from the surface's edges and is pushed forward with translate3d(0, 0, 25px) — a small offset, but enough that when the surface tilts, the glass visibly separates from the base color beneath it. The glass look itself comes from a vertical gradient running from a faint white at the top to a much stronger white near the bottom, plus a single light border on the leading edge and the bottom edge, which reads as a highlight catching the surface below.
.orb-card__glass {
position: absolute;
inset: 8px;
border-radius: 55px;
border-top-right-radius: 100%; /* one soft, asymmetric corner */
background: linear-gradient(0deg,
rgba(255,255,255,.34) 0%,
rgba(255,255,255,.80) 100%);
border-inline-start: 1px solid rgba(255,255,255,.82);
border-bottom: 1px solid rgba(255,255,255,.7);
transform: translate3d(0, 0, 25px); /* raised above the base surface */
pointer-events: none;
}The one rounded corner instead of four matching ones is deliberate — a perfectly symmetric card reads as a UI mockup, while a single oversized radius on the top-right corner reads as a physical object with a front face, which sells the 3D tilt much harder than uniform corners would.
Step 3 — Parallax From Depth Alone, Not Position
The five .orb-card__ring elements are concentric circles anchored to the same top-right corner, shrinking in size as they get closer to the viewer. Each ring has its own resting translate3d(0, 0, Npx) — 20px, 40px, 60px, 80px, and 100px — so with no interaction at all they already look like a stack of glass discs receding into the card. On hover, rings two through five each jump forward by another 20px, and each transition carries its own transition-delay in 0.05s increments, so the front rings visibly lag half a beat behind the back ones.
.orb-card__ring:nth-child(1) { width: 170px; transform: translate3d(0,0,20px); }
.orb-card__ring:nth-child(2) { width: 140px; transform: translate3d(0,0,40px); transition-delay: .05s; }
.orb-card__ring:nth-child(3) { width: 110px; transform: translate3d(0,0,60px); transition-delay: .10s; }
.orb-card__ring:nth-child(4) { width: 80px; transform: translate3d(0,0,80px); transition-delay: .15s; }
.orb-card__ring:nth-child(5) { width: 50px; transform: translate3d(0,0,100px); transition-delay: .20s; }
.orb-card:hover .orb-card__ring:nth-child(2),
.orb-card:focus-within .orb-card__ring:nth-child(2) { transform: translate3d(0,0,60px); }
.orb-card:hover .orb-card__ring:nth-child(5),
.orb-card:focus-within .orb-card__ring:nth-child(5) { transform: translate3d(0,0,120px); }
/* rings 3 and 4 follow the same +20px pattern */Nothing about the rings' x or y coordinates ever changes — only their z-depth does. Combined with the shared perspective on the wrapper, that single axis of movement is enough to produce a convincing parallax stack, without any JavaScript computing offsets.
Step 4 — Text and Actions Get Their Own Depth Too
Both the .orb-card__content block and the .orb-card__footer row sit at translate3d(0, 0, 26px) — just past the glass panel, so text and buttons always read as sitting on top of the frosted surface rather than behind it. The three round action buttons inside the footer go a step further: on hover or focus-within they push out to translate3d(0, 0, 50px), each with its own short transition-delay, so they visibly separate from the footer bar and catch a harder drop-shadow, the same staggered-lift trick used on the rings.
.orb-card__content,
.orb-card__footer {
position: relative;
z-index: 3;
transform: translate3d(0, 0, 26px); /* above the glass, below the rings */
}
.orb-card:hover .orb-card__action,
.orb-card:focus-within .orb-card__action {
transform: translate3d(0, 0, 50px);
box-shadow: rgba(15,40,90,.25) -5px 20px 12px 0;
}Step 5 — Making the Tilt Work Without a Mouse
The card itself carries tabindex="0" and an aria-label, and every hover rule in the component is paired with an identical :focus-within rule — so tabbing onto the card, or onto any button inside it, triggers the exact same tilt, ring separation, and action lift that a mouse hover would. A single prefers-reduced-motion query then strips every transition on the surface, rings, and buttons, and forces the resting (non-tilted) transform, so the layout is still fully readable, just without the animated fall.
@media (prefers-reduced-motion: reduce) {
.orb-card__surface,
.orb-card__ring,
.orb-card__action,
.orb-card__link {
transition: none !important;
}
.orb-card:hover .orb-card__surface,
.orb-card:focus-within .orb-card__surface {
transform: none;
}
}Tuning Reference
| Property | Example | Effect |
|---|---|---|
| perspective | 1000px | Vanishing point for every 3D transform inside the card; lower values exaggerate depth |
| rotate3d(1, 1, 0, deg) | 28deg | Tilt angle and axis of the whole surface on hover/focus |
| ring translate3d z | 20px – 100px | Resting depth of each of the five rings; wider spacing reads as more separation |
| transition-delay stagger | 0.05s increments | Time offset between rings/buttons; larger gaps read as a slower parallax cascade |
| glass gradient stops | 0.34 → 0.80 alpha | Controls how frosted vs. transparent the glass panel looks |
| --card-gradient | per theme | Base surface color; every other token in the file derives its contrast from this one gradient |
Full Source Code
The complete component is a single self-contained HTML document — the surface, glass panel, five rings, content block, and footer, plus the hover/focus-within rules and the reduced-motion override, and nothing else. No build step, no external assets, no JavaScript at all.
// Full source available in the Aduok GitHub repository
// https://github.com/aduok/aduok-code-snippets/blob/main/blog/orb-glass-card.html