Showcase

Cube speakerGraphite
$149
No dependencies
Code format
React + Tailwind
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>
    </>
  );
}

Free to use in any project, commercial included. MIT-0, no attribution required. Licence

Install with shadcn

npx shadcn@latest add @movin/card-showcase
Source on GitHub

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.

More cards

See all
Skyline
Freq228 Hz
ShapeTriangle
Synth
Tank
Unfold