Showcase
const css = `
@keyframes card-showcase-turn {
from { transform: rotateX(-18deg) rotateY(0deg); }
to { transform: rotateX(-18deg) rotateY(360deg); }
}
@keyframes card-showcase-sweep {
from { transform: translateX(-120%) skewX(-18deg); }
to { transform: translateX(220%) skewX(-18deg); }
}
.card-showcase {
position: relative;
width: 220px;
height: 196px;
border-radius: 18px;
background: radial-gradient(ellipse at 50% 30%, light-dark(#ebedf0, #232529), light-dark(#fbfcfd, #121417) 75%);
box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035);
overflow: hidden;
cursor: pointer;
}
.card-showcase:focus-visible { outline: 2px solid light-dark(#42454c, #e5e6e9); outline-offset: 3px; }
.card-showcase__stage { position: absolute; left: 50%; top: 44px; width: 0; height: 0; perspective: 500px; }
.card-showcase__product,
.card-showcase__reflection {
position: absolute;
left: -26px;
top: -26px;
width: 52px;
height: 52px;
transform-style: preserve-3d;
transform: rotateX(-18deg) rotateY(0deg);
animation: card-showcase-turn 14s linear infinite;
}
.card-showcase__reflection { top: 66px; opacity: 0.12; scale: 1 -1; mask-image: linear-gradient(transparent 20%, #000); }
.card-showcase__face {
position: absolute;
inset: 0;
border: 1px solid light-dark(#5f626a, #a7abb2);
background: linear-gradient(135deg, light-dark(#50535b, #c6c8cd), light-dark(#cbced3, #393c41));
}
.card-showcase__face:nth-child(1) { transform: translateZ(26px); }
.card-showcase__face:nth-child(2) { transform: rotateY(90deg) translateZ(26px); background: linear-gradient(135deg, light-dark(#5f626a, #a7abb2), light-dark(#dbdde1, #2e3035)); }
.card-showcase__face:nth-child(3) { transform: rotateY(180deg) translateZ(26px); }
.card-showcase__face:nth-child(4) { transform: rotateY(-90deg) translateZ(26px); background: linear-gradient(135deg, light-dark(#5f626a, #a7abb2), light-dark(#dbdde1, #2e3035)); }
.card-showcase__face:nth-child(5) { transform: rotateX(90deg) translateZ(26px); background: light-dark(#42454c, #e5e6e9); }
.card-showcase__plinth {
position: absolute;
left: 50%;
top: 80px;
width: 110px;
height: 22px;
margin-left: -55px;
border-radius: 50%;
background: radial-gradient(ellipse, light-dark(#dbdde1, #2e3035), transparent 70%);
box-shadow: 0 0 0 1px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 10%, transparent);
}
.card-showcase__sweep {
position: absolute;
inset: 0;
width: 40%;
background: linear-gradient(90deg, transparent, color-mix(in oklab, light-dark(#373a40, #f1f2f3) 14%, transparent), transparent);
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
.card-showcase__info { position: absolute; left: 14px; right: 14px; bottom: 12px; display: flex; justify-content: space-between; align-items: end; }
.card-showcase__info b { display: block; color: light-dark(#373a40, #f1f2f3); font-size: 13px; font-weight: 600; }
.card-showcase__info span { color: light-dark(#5f626a, #a7abb2); font-size: 11px; }
.card-showcase__price { color: light-dark(#373a40, #f1f2f3); font-size: 13px; font-weight: 600; }
.card-showcase:hover .card-showcase__sweep { opacity: 1; animation: card-showcase-sweep 1.6s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.card-showcase__product, .card-showcase__reflection, .card-showcase:hover .card-showcase__sweep { animation: none; }
}
`;
function Product({ className }: { className: string }) {
return (
<div className={className}>
{[0, 1, 2, 3, 4].map((i) => (
<span key={i} className="card-showcase__face" />
))}
</div>
);
}
export function ShowcaseCard() {
// Speed the turntable up without a jump: same animation, faster playback.
const rate = (el: HTMLElement, r: number) =>
el.querySelectorAll<HTMLElement>('.card-showcase__product, .card-showcase__reflection').forEach((p) =>
p.getAnimations().forEach((a) => a.updatePlaybackRate(r)),
);
return (
<>
<style>
{css}
</style>
<div
className="card-showcase"
tabIndex={0}
role="group"
aria-label="Cube speaker, 149 dollars"
onPointerEnter={(e) => rate(e.currentTarget, 6)}
onPointerLeave={(e) => rate(e.currentTarget, 1)}
onFocus={(e) => rate(e.currentTarget, 6)}
onBlur={(e) => rate(e.currentTarget, 1)}
>
<span className="card-showcase__plinth" aria-hidden="true" />
<div className="card-showcase__stage" aria-hidden="true">
<Product className="card-showcase__reflection" />
<Product className="card-showcase__product" />
</div>
<span className="card-showcase__sweep" aria-hidden="true" />
<div className="card-showcase__info">
<div>
<b>Cube speaker</b>
<span>Graphite</span>
</div>
<span className="card-showcase__price">$149</span>
</div>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initCardShowcase(el) on the mounted element instead.
<div class="card-showcase" tabindex="0" role="group" aria-label="Cube speaker, 149 dollars">
<span class="card-showcase__plinth" aria-hidden="true"></span>
<div class="card-showcase__stage" aria-hidden="true">
<div class="card-showcase__reflection">
<span class="card-showcase__face"></span>
<span class="card-showcase__face"></span>
<span class="card-showcase__face"></span>
<span class="card-showcase__face"></span>
<span class="card-showcase__face"></span>
</div>
<div class="card-showcase__product">
<span class="card-showcase__face"></span>
<span class="card-showcase__face"></span>
<span class="card-showcase__face"></span>
<span class="card-showcase__face"></span>
<span class="card-showcase__face"></span>
</div>
</div>
<span class="card-showcase__sweep" aria-hidden="true"></span>
<div class="card-showcase__info">
<div>
<b>Cube speaker</b>
<span>Graphite</span>
</div>
<span class="card-showcase__price">$149</span>
</div>
</div>@keyframes card-showcase-turn {
from { transform: rotateX(-18deg) rotateY(0deg); }
to { transform: rotateX(-18deg) rotateY(360deg); }
}
@keyframes card-showcase-sweep {
from { transform: translateX(-120%) skewX(-18deg); }
to { transform: translateX(220%) skewX(-18deg); }
}
.card-showcase {
position: relative;
width: 220px;
height: 196px;
border-radius: 18px;
background: radial-gradient(ellipse at 50% 30%, light-dark(#ebedf0, #232529), light-dark(#fbfcfd, #121417) 75%);
box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035);
overflow: hidden;
cursor: pointer;
}
.card-showcase:focus-visible { outline: 2px solid light-dark(#42454c, #e5e6e9); outline-offset: 3px; }
.card-showcase__stage { position: absolute; left: 50%; top: 44px; width: 0; height: 0; perspective: 500px; }
.card-showcase__product,
.card-showcase__reflection {
position: absolute;
left: -26px;
top: -26px;
width: 52px;
height: 52px;
transform-style: preserve-3d;
transform: rotateX(-18deg) rotateY(0deg);
animation: card-showcase-turn 14s linear infinite;
}
.card-showcase__reflection { top: 66px; opacity: 0.12; scale: 1 -1; mask-image: linear-gradient(transparent 20%, #000); }
.card-showcase__face {
position: absolute;
inset: 0;
border: 1px solid light-dark(#5f626a, #a7abb2);
background: linear-gradient(135deg, light-dark(#50535b, #c6c8cd), light-dark(#cbced3, #393c41));
}
.card-showcase__face:nth-child(1) { transform: translateZ(26px); }
.card-showcase__face:nth-child(2) { transform: rotateY(90deg) translateZ(26px); background: linear-gradient(135deg, light-dark(#5f626a, #a7abb2), light-dark(#dbdde1, #2e3035)); }
.card-showcase__face:nth-child(3) { transform: rotateY(180deg) translateZ(26px); }
.card-showcase__face:nth-child(4) { transform: rotateY(-90deg) translateZ(26px); background: linear-gradient(135deg, light-dark(#5f626a, #a7abb2), light-dark(#dbdde1, #2e3035)); }
.card-showcase__face:nth-child(5) { transform: rotateX(90deg) translateZ(26px); background: light-dark(#42454c, #e5e6e9); }
.card-showcase__plinth {
position: absolute;
left: 50%;
top: 80px;
width: 110px;
height: 22px;
margin-left: -55px;
border-radius: 50%;
background: radial-gradient(ellipse, light-dark(#dbdde1, #2e3035), transparent 70%);
box-shadow: 0 0 0 1px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 10%, transparent);
}
.card-showcase__sweep {
position: absolute;
inset: 0;
width: 40%;
background: linear-gradient(90deg, transparent, color-mix(in oklab, light-dark(#373a40, #f1f2f3) 14%, transparent), transparent);
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
.card-showcase__info { position: absolute; left: 14px; right: 14px; bottom: 12px; display: flex; justify-content: space-between; align-items: end; }
.card-showcase__info b { display: block; color: light-dark(#373a40, #f1f2f3); font-size: 13px; font-weight: 600; }
.card-showcase__info span { color: light-dark(#5f626a, #a7abb2); font-size: 11px; }
.card-showcase__price { color: light-dark(#373a40, #f1f2f3); font-size: 13px; font-weight: 600; }
.card-showcase:hover .card-showcase__sweep { opacity: 1; animation: card-showcase-sweep 1.6s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.card-showcase__product, .card-showcase__reflection, .card-showcase:hover .card-showcase__sweep { animation: none; }
}function initCardShowcase(root) {
// Speed the turntable up without a jump: same animation, faster playback.
function rate(r) {
root.querySelectorAll('.card-showcase__product, .card-showcase__reflection').forEach(function (p) {
p.getAnimations().forEach(function (a) { a.updatePlaybackRate(r); });
});
}
function fast() { rate(6); }
function slow() { rate(1); }
root.addEventListener('pointerenter', fast);
root.addEventListener('pointerleave', slow);
root.addEventListener('focus', fast);
root.addEventListener('blur', slow);
return function () {
root.removeEventListener('pointerenter', fast);
root.removeEventListener('pointerleave', slow);
root.removeEventListener('focus', fast);
root.removeEventListener('blur', slow);
};
}
document.querySelectorAll('.card-showcase').forEach(initCardShowcase);Goal: Create a "Showcase" card.
Visual behavior: A product on a turntable: a small 3D cube speaker turns slowly on a plinth above its own floor reflection. Hover spins the turntable up without a jump and sweeps a band of light across the card.
Technique:
- The product is a five-faced box in preserve-3d, tilted rotateX(-18deg) and turning on Y once every 14s.
- A second copy below is flipped with scale: 1 -1, kept at 12% opacity and faded with a mask, so it reads as a floor reflection; it shares the same animation.
- Hover speeds both to 6x with getAnimations() and updatePlaybackRate(), so the turn never restarts.
- A skewed light band sweeps across the card on a loop while hovered.
Accessibility: The card is focusable (tabIndex 0) with role="group" and an aria-label that sums it up; keyboard focus plays the same reveal as hover. Under prefers-reduced-motion the product stands still.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Showcase" card.
Visual behavior: A product on a turntable: a small 3D cube speaker turns slowly on a plinth above its own floor reflection. Hover spins the turntable up without a jump and sweeps a band of light across the card.
Technique:
- The product is a five-faced box in preserve-3d, tilted rotateX(-18deg) and turning on Y once every 14s.
- A second copy below is flipped with scale: 1 -1, kept at 12% opacity and faded with a mask, so it reads as a floor reflection; it shares the same animation.
- Hover speeds both to 6x with getAnimations() and updatePlaybackRate(), so the turn never restarts.
- A skewed light band sweeps across the card on a loop while hovered.
Accessibility: The card is focusable (tabIndex 0) with role="group" and an aria-label that sums it up; keyboard focus plays the same reveal as hover. Under prefers-reduced-motion the product stands still.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
About this component
A product on a turntable: a small 3D cube speaker turns slowly on a plinth above its own floor reflection. Hover spins the turntable up without a jump and sweeps a band of light across the card.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- hover, 3d, continuous, card
- Techniques
- preserve 3d, 3d transform, keyframes, gradient
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
The card is focusable (tabIndex 0) with role="group" and an aria-label that sums it up; keyboard focus plays the same reveal as hover. Under prefers-reduced-motion the product stands still.