Accordion expandWeb · App
Target
Implement the "Card flip" effect in vanilla HTML/CSS/JS. Context: A card rotates around its vertical axis to reveal a back face, with perspective so the edge comes toward the viewer. Trigger: the element is pressed (pointerdown → pointerup). Timeline: time-based (duration + easing). Motion tokens (normal): - rotateY (card): 0deg → 180deg - duration: 600ms - easing: cubic-bezier(0.4, 0.2, 0.2, 1) - note: perspective 1000px on the parent; faces use backface-visibility hidden Motion tokens (prefers-reduced-motion): - opacity (front): 1 → 0 - opacity (back): 0 → 1 - duration: 200ms - easing: ease-out - note: crossfade, no rotation Technique: prefer pure CSS transitions/animations; use JS only to toggle classes. Acceptance: - animate only transform and opacity unless the spec lists other properties - no layout shift; content is visible without the animation - when prefers-reduced-motion is set, use the reduced tokens only - back face content is not focusable while hidden (inert or visibility) - the trigger is a button; the card announces its state via aria-pressed
Generated from the spec. Prompts differ by target name only; the tokens are the source of truth.
demo.html
<div class="fx-card-flip" data-fx-root>
<button type="button" class="fx-scene" aria-pressed="false" aria-label="Flip card">
<span class="fx-card"><span class="fx-face fx-front"><span class="fx-t"></span><span class="fx-s"></span></span><span class="fx-face fx-back"><span class="fx-t"></span><span class="fx-s"></span><span class="fx-s"></span></span></span>
</button>
</div>
demo.css
.fx-card-flip { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; }
.fx-card-flip .fx-scene { width: 50%; max-width: 240px; aspect-ratio: 3 / 2; padding: 0; border: 0; background: transparent; perspective: 1000px; cursor: pointer; }
.fx-card-flip .fx-card { position: relative; display: block; width: 100%; height: 100%; transform-style: preserve-3d; transition: transform 600ms cubic-bezier(.4, .2, .2, 1); }
.fx-card-flip .fx-scene[aria-pressed="true"] .fx-card { transform: rotateY(180deg); }
.fx-card-flip .fx-face { position: absolute; inset: 0; display: flex; flex-direction: column; justify-content: flex-end; gap: 8px; padding: 16px; border-radius: 14px; backface-visibility: hidden; -webkit-backface-visibility: hidden; }
.fx-card-flip .fx-front { background: linear-gradient(135deg, #e0e7ff, #a5b4fc); }
.fx-card-flip .fx-back { background: var(--fx-ink, #18181b); transform: rotateY(180deg); }
.fx-card-flip .fx-t { height: 12px; width: 55%; border-radius: 4px; background: rgba(255,255,255,.9); }
.fx-card-flip .fx-s { height: 8px; width: 80%; border-radius: 4px; background: rgba(255,255,255,.55); }
@media (prefers-reduced-motion: reduce) {
.fx-card-flip .fx-card { transition: none; } .fx-card-flip .fx-scene[aria-pressed="true"] .fx-card { transform: none; }
.fx-card-flip .fx-face { transition: opacity 200ms ease-out; } .fx-card-flip .fx-back { transform: none; opacity: 0; }
.fx-card-flip .fx-scene[aria-pressed="true"] .fx-front { opacity: 0; } .fx-card-flip .fx-scene[aria-pressed="true"] .fx-back { opacity: 1; }
}
demo.js
export default function mount(root) {
const scene = root.querySelector('.fx-scene');
scene.addEventListener('click', () => scene.setAttribute('aria-pressed', scene.getAttribute('aria-pressed') === 'true' ? 'false' : 'true'));
let t1, t2;
return {
play() { clearTimeout(t1); clearTimeout(t2); scene.setAttribute('aria-pressed', 'false'); t1 = setTimeout(() => scene.setAttribute('aria-pressed', 'true'), 400); t2 = setTimeout(() => scene.setAttribute('aria-pressed', 'false'), 1900); },
stop() { clearTimeout(t1); clearTimeout(t2); scene.setAttribute('aria-pressed', 'false'); },
};
}
Inspired by 3dtransforms.desandro.com — David DeSandro's card flip. The preview is an original re-creation; the effect name belongs to its author.
Perspective goes on the parent (the "scene"), `preserve-3d` on the rotating element, and each face hides its backface. The reduced-motion branch swaps the rotation for a crossfade of the two faces, which keeps the same state model.
Added 2026-11-09 · Report an issue