Flip Card to Modal — Free GSAP Flip Plugin Snippet

Flip Card to Modal · Modals · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

True shared element
One DOM node plays card and modal.
Class-swap layouts
is-open teleports; Flip animates the gap.
No clones to sync
Listeners and state ride the transition.
Tracked radius
props option tweens style changes too.
Two-phase reveal
Container morphs, then content pops in.
Mid-flight guard
An animating flag blocks double-clicks.
Full close paths
Backdrop click and Escape both reverse.
Layered correctly
The flight stays above the backdrop.

About this UI Snippet

Flip Card to Modal — Shared-Element Expansion With One DOM Node

Screenshot of the Flip Card to Modal snippet rendered live

The card-to-modal morph is the shared-element transition every app store and gallery uses: tap a card and it *becomes* the detail view, growing from its grid slot to screen center with perfect continuity. Most implementations fake it with a cloned overlay; this snippet does it honestly — the same DOM node plays both roles — using GSAP's Flip plugin to animate between two CSS layouts.

One element, two layouts, zero coordinates

The card's modal state is just a class: .is-open switches it to position: fixed, centers it with a translate, widens it, and reveals the hidden detail block. The animation is the standard FLIP sandwich — Flip.getState(card) before the class flip, Flip.from(state) after — so GSAP measures where the card was in the grid, where CSS just teleported it, and animates the difference. Neither position is ever computed in JavaScript, which means redesigning the modal (or the grid) requires touching only CSS.

Why the real node beats a clone

Clone-based modals must copy content, sync state between twins, and hide the original — three sources of bugs. Because this is one node, its event listeners, form state, and any live content ride along through the transition. The tradeoff is that the grid keeps a gap where the card left (its slot is preserved since absolute: true handles flight, and the fixed positioning removes it from flow) — visually fine here since the backdrop dims the grid anyway; a placeholder element can hold the slot if your layout collapses.

props: 'borderRadius' keeps corners honest

The grid card and modal share a 16px radius here, but the props option demonstrates the pattern: any style the class flip changes (radius, background, padding-driven visuals) can be recorded and tweened rather than snapping. Without it, Flip animates only position and size.

Content is choreographed, not flipped

The detail paragraph and price are display: none in the grid state — they have no meaningful "before" geometry, so flipping them would stretch text weirdly. Instead they pop in with a separate 0.35s fade-up delayed until the container is ~60% landed. Splitting "container morph" from "content entrance" is the core trick of polished shared-element UIs.

Interaction guards make it production-shaped

An animating flag ignores clicks mid-flight (double-clicking a FLIP is the classic way to strand elements between states), the backdrop click and Escape key both close via the same toggle(), and the open card ignores further clicks so text selection works inside the modal.

The backdrop is CSS, sequenced by class

The blurred backdrop fades via its own CSS transition, toggled in the same frame as the card's class. Layer order (z-index 10 vs 20) puts the traveling card above the backdrop for the entire flight, so the morph never dips behind the dimmer.

Customizing it

Add an image that scales with the card, swap min-height for real content sizing, or FLIP back to a *different* grid position after sorting. Related: reorder animation in flip grid shuffle, the CSS-only expandable card, a classic modal, and product framing like product quick view.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You don't need to reverse-engineer the FLIP sandwich by hand to understand what's happening here. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to walk through exactly what Flip.getState captures before the is-open class toggles, and why props: 'borderRadius' is needed on top of the automatic position and size tracking. The same assistant can help optimize it — ask whether the animating flag is sufficient protection against rapid clicks across multiple cards at once, or whether absolute: true is the right choice if the grid layout itself can reflow while a card is mid-flight. It's also useful for extending the interaction: have it add a hero image that scales with the card during the flip, support returning the card to a different grid position after a sort, or stack multiple simultaneously-open cards into a carousel. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a "card that becomes its own modal" shared-element transition in plain HTML, CSS, and JavaScript using GSAP and its Flip plugin (load both from a CDN) — the modal must be the exact same DOM node as the grid card, never a clone.

Requirements:
- A CSS grid of cards, each with a compact "collapsed" look (icon, title) and a hidden detail block (description and price) that is display: none by default.
- A single .is-open class that, when present on a card, switches it via CSS alone to position: fixed, centered on screen with a translate, given a wider fixed width, and reveals its detail block by changing that block's display — no JavaScript should compute or set any pixel coordinates for the open state.
- On click, capture the card's current geometry with Flip.getState before toggling the is-open class, then call Flip.from with that captured state immediately after the class toggle, so GSAP animates the visual difference between the old and new CSS layouts. Pass borderRadius in the getState props list so a border-radius change between the two states is also tweened rather than snapping.
- The revealed detail content (description and price) must not be part of the Flip animation itself — animate it separately with a short fade-and-slide-up tween delayed until the container flip is mostly complete, since display:none elements have no meaningful starting geometry to flip.
- Use a boolean "is a flip currently animating" guard to ignore new clicks until the current Flip.from completes, since clicking again mid-flight would capture a moving element as a bad starting state.
- Add a backdrop element that fades in via its own CSS opacity transition in the same frame the card opens, sits below the traveling card in z-index for the whole flight, and closes the open card when clicked. Also close the open card on pressing the Escape key.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Add the GSAP CDNsInclude gsap and the Flip plugin from the CDN panel.
  2. 2
    Paste HTML, CSS, and JSThree product cards render in a grid.
  3. 3
    Click a cardIt morphs from its grid slot into a centered modal.
  4. 4
    Watch the contentDetails fade up after the container lands.
  5. 5
    Close itBackdrop click or Escape morphs it back to its slot.
  6. 6
    Restyle either stateBoth layouts are pure CSS — Flip adapts automatically.

Real-world uses

Common Use Cases

Product quick views
Expand catalog cards to detail sheets, like product quick view without the second layer.
Portfolio case studies
Grow a thumbnail into its story; browse with a photo gallery.
App-store style pages
The iOS "card opens into page" feel; compare the CSS-only expandable card.
Dashboard drill-downs
A KPI tile expanding to its full chart, beside a metric card grid.
Team bios
Headshot cards opening into profiles, styled like team card.
Sorted-grid pairing
Combine with flip grid shuffle for a fully FLIP-animated collection.

Got questions?

Frequently Asked Questions

The modal is just another CSS state: .is-open makes the same element position: fixed, centered, and wider. Flip.getState captures the grid geometry before the class flip, and Flip.from animates from that capture to wherever CSS teleported the node. One element, two layouts — GSAP animates the difference, so no clone ever exists.

Clones must duplicate content, mirror any state, and hide the original — and they still desync when live content updates mid-transition. With one node, click handlers, media playback, and form values persist through the morph. The only price is managing the vacated grid slot, which the dimmed backdrop makes a non-issue here.

The description and price are display: none in the card state, so they have no meaningful starting geometry — flipping them would smear text across the resize. Instead a separate tween fades them up 0.3s into the morph, after the container is mostly landed. Splitting container motion from content entrance is what makes shared-element UIs read as polished.

An animating boolean set before Flip.from and cleared in its onComplete. Without it, a second click mid-flight would capture a moving element as the "first" state and stack a conflicting animation — the classic way FLIP UIs strand cards between layouts. Clicks on the open modal are also ignored so users can select text.

Yes — geometry differences are Flip's whole job, and style differences can be tweened by listing them in getState's props (this snippet tracks borderRadius as the template). For images, keep one img whose size is driven by each layout; object-fit: cover makes the crop transition naturally during the morph.

Drive is-open from state, but sequence carefully: capture Flip.getState in the click handler before setting state, then call Flip.from in useLayoutEffect (React), nextTick (Vue), or afterNextRender (Angular) once the class has committed. Register Flip at module scope, guard with an animating ref, and revert a gsap.context on unmount. Both layouts express cleanly as conditional Tailwind classes.