Components Cards & Layout CardStackMotionFlip

CardStackMotionFlip

4-direction rolling deck with scoped keyboard control.

Live demo

01
Database Connected
Mountain Vista

Mountain Vista

Breathtaking views from the highest peaks, where the air is crisp and the horizon endless.
Ocean Waves

Ocean Waves

The rhythmic dance of waves meeting shore, a timeless symphony of nature.
Forest Path

Forest Path

Wandering through ancient woods, where sunlight filters through emerald canopies.
Desert Dunes

Desert Dunes

Golden sands sculpted by wind, creating an ever-changing landscape of beauty.
City Lights

City Lights

Urban brilliance illuminating the night, a testament to human achievement.

Compact

Smaller cards for constrained panels and mobile-first layouts.

Mountain Vista

Mountain Vista

Breathtaking views from the highest peaks, where the air is crisp and the horizon endless.
Ocean Waves

Ocean Waves

The rhythmic dance of waves meeting shore, a timeless symphony of nature.
Forest Path

Forest Path

Wandering through ancient woods, where sunlight filters through emerald canopies.
Desert Dunes

Desert Dunes

Golden sands sculpted by wind, creating an ever-changing landscape of beauty.

Flat motion

Same deck choreography with 3D rotation disabled.

Mountain Vista

Mountain Vista

Breathtaking views from the highest peaks, where the air is crisp and the horizon endless.
Ocean Waves

Ocean Waves

The rhythmic dance of waves meeting shore, a timeless symphony of nature.
Forest Path

Forest Path

Wandering through ancient woods, where sunlight filters through emerald canopies.
Desert Dunes

Desert Dunes

Golden sands sculpted by wind, creating an ever-changing landscape of beauty.

Implementation

02
CardStackMotionFlip.svelte
<script>
  import CardStackMotionFlip from '$lib/components/CardStackMotionFlip.svelte';
</script>
​
<CardStackMotionFlip
  cards={cards}
  cardWidth={280}
  cardHeight={380}
  cardGap={38}
  swipeThreshold={80}
  enable3D={true}
/>

CardStackMotionFlip is a focused 3D deck. Drag in any direction to roll the top card off-screen with full rotation, then watch it reappear at the back of the stack. Keyboard control is intentionally scoped to focus inside the deck so global arrow handlers stay free for page navigation.

Logic explainer

03

What Does It Do? (Plain English)

CardStackMotionFlip is a 3D card deck where you can flick the top card away in any direction (left, right, up, down) and it rolls off-screen with a 3D spinning effect, then reappears at the back of the deck.

Think of it like: A deck of cards on a table. Grab the top card and flick it away - it spins off and magically teleports to the bottom of the deck!


How It Works (Pseudo-Code)

WHEN component mounts:
  1. CREATE cardOrder array [0, 1, 2, 3, ...n]
  2. SET currentState to 'idle'
  3. LISTEN for keyboard events

WHEN pointer DOWN on top card:
  1. LOCK page scroll (prevents page scrolling during drag)
  2. RECORD start position
  3. SET currentState to 'dragging'
  4. CAPTURE pointer (keeps events even if cursor leaves card)

WHILE dragging:
  1. CALCULATE delta from start position
  2. APPLY damped translation (card follows at 50% speed)
  3. APPLY rotation preview based on drag direction:
     - Horizontal drag β†’ rotateY
     - Vertical drag β†’ rotateX

WHEN pointer UP:
  1. UNLOCK page scroll
  2. CALCULATE final delta
  3. IF delta > swipeThreshold:
     - DETERMINE direction (L/R/U/D)
     - TRIGGER roll animation
  4. ELSE:
     - SNAP back to 'idle'

ROLL ANIMATION (state machine):
  1. 'rolling-*' β†’ Card flies off-screen with 180Β° rotation
  2. WAIT rollDuration ms
  3. 'repositioning' β†’ Card teleports to back (invisible)
  4. WAIT 16ms (one frame)
  5. 'entering' β†’ Card fades in at back position
  6. WAIT enterDuration ms
  7. 'idle' β†’ Ready for next interaction

The State Machine

CardStackMotionFlip uses a finite state machine to manage complex multi-phase animations:

                                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                    β”‚    idle     │◄────────────────┐
                                    β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜                 β”‚
                                          β”‚ pointer down            β”‚
                                          β–Ό                         β”‚
                                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                 β”‚
                                    β”‚  dragging   β”‚                 β”‚
                                    β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜                 β”‚
                                          β”‚ pointer up              β”‚
                          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”‚
                          β”‚               β”‚               β”‚         β”‚
              threshold   β”‚    threshold  β”‚   threshold   β”‚ no      β”‚
              met (left)  β”‚    met (up)   β”‚   met (down)  β”‚ thresholdβ”‚
                          β–Ό               β–Ό               β–Ό         β”‚
                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”‚
                   β”‚rolling-L β”‚    β”‚rolling-U β”‚    β”‚rolling-D β”‚ β”€β”€β”€β”€β”˜
                   β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜
                        β”‚               β”‚               β”‚
                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                        β”‚ rollDuration ms
                                        β–Ό
                              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                              β”‚  repositioning  β”‚
                              β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                       β”‚ 16ms
                                       β–Ό
                              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                              β”‚    entering     β”‚
                              β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                       β”‚ enterDuration ms
                                       β–Ό
                                    (back to idle)

3D Rotation Explained

When rolling, the card rotates 180Β° on the appropriate axis:

Horizontal Swipe (Left/Right)

rotateY: -180Β° (left) or +180Β° (right)

      β”Œβ”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”
      β”‚ A  β”‚   β†’     β”‚    β”‚   β†’     β”‚  A β”‚  (flipped!)
      β””β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”˜
     0Β° rotateY    90Β° rotateY   180Β° rotateY

Vertical Swipe (Up/Down)

rotateX: -180Β° (up) or +180Β° (down)

      β”Œβ”€β”€β”€β”€β”
      β”‚ A  β”‚         ← Card flips top-over-bottom (or vice versa)
      β””β”€β”€β”€β”€β”˜

Card Order Management

The deck maintains a cardOrder array that tracks display positions:

// Initial: [0, 1, 2, 3]
// Card 0 is at position 0 (front), Card 3 is at position 3 (back)

// After rolling the top card:
cardOrder = [...cardOrder.slice(1), cardOrder[0]];
// Result: [1, 2, 3, 0]
// Card 1 is now at front, Card 0 is at back

This lets us reorder the visual display without mutating the original cards prop.


Pointer Events API

We use Pointer Events instead of separate mouse/touch handlers:

// One handler works for mouse, touch, and stylus!
onpointerdown={(e) => handlePointerDown(e, displayIndex)}
onpointermove={handlePointerMove}
onpointerup={handlePointerUp}
onpointercancel={handlePointerUp}  // Handle interruptions

Key technique: setPointerCapture() keeps receiving events even if the pointer leaves the element:

(event.currentTarget as HTMLElement)?.setPointerCapture(event.pointerId);

Scroll Locking

When dragging on mobile, we need to prevent the page from scrolling:

import { lockScroll } from '$lib/scrollLock';

function handlePointerDown() {
  unlockScroll = lockScroll();  // Returns cleanup function
}

function handlePointerUp() {
  unlockScroll?.();  // Restore scrolling
  unlockScroll = null;
}

The scrollLock utility coordinates with other components (modals, drawers) to prevent conflicts.


Props Reference

Prop Type Default Description
cards Card[] [] Array of cards with image, title, content
cardWidth number 300 Width of each card in pixels
cardHeight number 400 Height of each card in pixels
cardGap number 50 Horizontal offset between stacked cards
swipeThreshold number 80 Minimum drag distance to trigger roll
rollDuration number 400 Roll animation duration (ms)
enterDuration number 200 Fade-in animation duration (ms)
enable3D boolean true Enable 3D rotation (false for 2D)

Keyboard Controls

Key Action
← Left Arrow Roll card left
β†’ Right Arrow Roll card right
↑ Up Arrow Roll card up
↓ Down Arrow Roll card down

CSS 3D Setup

The container establishes a 3D perspective:

.card-deck {
  perspective: 1000px;          /* Distance from viewer */
  perspective-origin: center;   /* Vanishing point */
}

.card-wrapper {
  transform-style: preserve-3d; /* Enable 3D for children */
  backface-visibility: hidden;  /* Hide back of card */
}

Performance Considerations

  • will-change: transform, opacity on animating cards (removed after animation)
  • Pointer capture prevents event spam
  • touch-action: none on top card prevents browser gesture interference
  • Reduced motion support - animations shortened to 150ms

Edge Cases

Situation Behaviour
Drag cancelled (e.g., phone call) pointercancel handler restores state
Rapid drags State machine prevents mid-animation drags
Zero cards Empty deck, no errors
Single card Card rolls and reappears at same position
Keyboard during animation Ignored until idle

Known Warnings

Warning Reason
state_referenced_locally cardOrder initialised from cards.length

Dependencies

  • $lib/types: CardStackMotionFlipProps
  • $lib/scrollLock: Coordinated scroll lock utility
  • Zero animation libraries: Pure CSS 3D transforms

File Structure

CardStackMotionFlip.svelte      # The component
CardStackMotionFlip.test.ts     # Unit tests
CardStackMotionFlip.md          # This explainer

Last updated: 26 December 2025

API

04
PropTypeDefaultDescription
cardsCard[][]Array of cards rendered in the deck.
cardWidthnumber300Card width in pixels.
cardHeightnumber400Card height in pixels.
cardGapnumber50Vertical gap between stacked cards.
swipeThresholdnumber80Pixel distance before a swipe commits.
enable3DbooleantrueToggle full 3D roll rotation.