Grille ou liste de cartes où une carte s’agrandit en une vue détaillée modale accessible : sa boîte, son image, son titre et sa description voyagent sur un seul ressort interruptible, le texte et les images ne sont jamais étirés, et le mouvement réduit l’ouvre instantanément.
Catégorie
Transitions
Type
Web Component (<mv-expandable>)
Statut
stable
Keywords
expandable, shared-element, morph, card, dialog, flip, spring
When to use
A grid of cards such as recipes, tracks or projects should open into a full detail view in place
A list with thumbnails needs each row to morph into a modal with more content and actions
Keeping visual continuity between a preview and its detail matters more than navigating to a new page
Avoid when
The detail view is a form or confirmation not tied to a card's image and title → use Dialog instead
The detail needs its own URL, deep linking or back-button support → use Page Transition instead
Agent IA avec le serveur MCP de Marvelous UI : install_components({ slugs: ["expandable-card"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Balisage de référence : partez de celui-ci et personnalisez-le avec les attributs, data-* et les variables CSS :
<div id="exp-demo" style="display:grid;gap:1.5rem;width:100%;max-width:56rem;margin-inline:auto">
<style>
#exp-demo h4, .mv-expandable-dialog .ex-cta { margin: 0; }
#exp-demo .ex-label { margin: 0 0 .25rem .5rem; font-size: .75rem; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--mv-fg-subtle); }
.mv-expandable-dialog .ex-cta { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding-top: .75rem; border-top: 1px solid var(--mv-border); color: var(--mv-fg); }
</style>
<mv-expandable>
<p class="ex-label">Destinations</p>
<ul class="mv-expandable-grid">
<li class="mv-expandable-card">
<img class="mv-expandable-media" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 300' preserveAspectRatio='xMidYMid slice'%3E%3Cdefs%3E%3ClinearGradient id='s' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%23ffd8a8'/%3E%3Cstop offset='1' stop-color='%23ff9f7a'/%3E%3C/linearGradient%3E%3ClinearGradient id='w' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%233f7fb0'/%3E%3Cstop offset='1' stop-color='%231d3f66'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='400' height='300' fill='url%28%23s%29'/%3E%3Ccircle cx='110' cy='120' r='34' fill='%23fff3d6'/%3E%3Cpath d='M0 190h30v-28h16v-20h14v48h22v-36h18v36h14v-58h12v58h26v-30h20v30h30v-44h14v44h22v-24h18v24h26v-36h16v36h52v20H0z' fill='%238a4b5c'/%3E%3Crect y='205' width='400' height='95' fill='url%28%23w%29'/%3E%3Cpath d='M80 230h60M200 250h90M40 270h50M260 222h70' stroke='%23fff' stroke-opacity='.35' stroke-width='3' stroke-linecap='round'/%3E%3C/svg%3E" alt="">
<h3 class="mv-expandable-title">Lisbon</h3>
<p class="mv-expandable-description">Portugal · 3 days</p>
<div class="mv-expandable-content">
<p>Yellow trams, miradouros at sunset and pastéis de nata still warm from the oven: a long weekend between Alfama and Belém.</p>
<p>Best season: April to June, averaging 72°F.</p>
<div class="ex-cta"><strong>From $420</strong><a class="mv-button" href="#exp-demo" data-mv-close>Book now</a></div>
</div>
</li>
<li class="mv-expandable-card">
<img class="mv-expandable-media" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 300' preserveAspectRatio='xMidYMid slice'%3E%3Cdefs%3E%3ClinearGradient id='s' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%23ffe3ef'/%3E%3Cstop offset='1' stop-color='%23ffc2d6'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='400' height='300' fill='url%28%23s%29'/%3E%3Ccircle cx='292' cy='112' r='38' fill='%23ffffff' opacity='.95'/%3E%3Cg fill='%23fff' opacity='.7'%3E%3Ccircle cx='60' cy='70' r='4'/%3E%3Ccircle cx='120' cy='50' r='3'/%3E%3Ccircle cx='180' cy='80' r='3.5'/%3E%3Ccircle cx='340' cy='60' r='3'/%3E%3C/g%3E%3Cpath d='M0 210 60 150l50 38 70-78 90 92 60-50 70 60v88H0z' fill='%23c65a86'/%3E%3Cpath d='M0 250c60-30 110-34 170-14s120 14 160-8 60-8 70 0v72H0z' fill='%236d2a52'/%3E%3C/svg%3E" alt="">
<h3 class="mv-expandable-title">Kyoto in spring</h3>
<p class="mv-expandable-description">Japan · 7 days</p>
<div class="mv-expandable-content">
<p>Cherry blossoms along the Philosopher’s Path, Higashiyama’s temples at dawn and a night in a ryokan with an onsen.</p>
<p>Hanami: late March to mid-April, depending on the year.</p>
<div class="ex-cta"><strong>From $1,890</strong><a class="mv-button" href="#exp-demo" data-mv-close>Book now</a></div>
</div>
</li>
<li class="mv-expandable-card">
<img class="mv-expandable-media" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 300' preserveAspectRatio='xMidYMid slice'%3E%3Cdefs%3E%3ClinearGradient id='s' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%23b9e3ff'/%3E%3Cstop offset='1' stop-color='%23e8f6ff'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='400' height='300' fill='url%28%23s%29'/%3E%3Ccircle cx='292' cy='112' r='38' fill='%23fffbe6' opacity='.95'/%3E%3Cpath d='M0 210 60 150l50 38 70-78 90 92 60-50 70 60v88H0z' fill='%234d7aa3'/%3E%3Cpath d='M0 250c60-30 110-34 170-14s120 14 160-8 60-8 70 0v72H0z' fill='%231f3d5c'/%3E%3C/svg%3E" alt="">
<h3 class="mv-expandable-title">Norwegian fjords</h3>
<p class="mv-expandable-description">Norway · 5 days</p>
<div class="mv-expandable-content">
<p>A cruise through the Geirangerfjord, a hike up to Preikestolen and nights under the midnight sun.</p>
<p>Best from June to August; daily ferries from Bergen.</p>
<div class="ex-cta"><strong>From $1,240</strong><a class="mv-button" href="#exp-demo" data-mv-close>Book now</a></div>
</div>
</li>
<li class="mv-expandable-card">
<img class="mv-expandable-media" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 300' preserveAspectRatio='xMidYMid slice'%3E%3Cdefs%3E%3ClinearGradient id='s' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%23ffcf8a'/%3E%3Cstop offset='1' stop-color='%23ff8a5c'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='400' height='300' fill='url%28%23s%29'/%3E%3Ccircle cx='292' cy='112' r='38' fill='%23fff1c9' opacity='.95'/%3E%3Cg fill='%23fff' opacity='.8'%3E%3Ccircle cx='40' cy='40' r='1.5'/%3E%3Ccircle cx='90' cy='26' r='1.2'/%3E%3Ccircle cx='150' cy='46' r='1.4'/%3E%3C/g%3E%3Cpath d='M0 210 60 150l50 38 70-78 90 92 60-50 70 60v88H0z' fill='%23c8683d'/%3E%3Cpath d='M0 250c60-30 110-34 170-14s120 14 160-8 60-8 70 0v72H0z' fill='%238c3f22'/%3E%3C/svg%3E" alt="">
<h3 class="mv-expandable-title">Agafay Desert</h3>
<p class="mv-expandable-description">Morocco · 4 days</p>
<div class="mv-expandable-content">
<p>Camp facing the Atlas Mountains, a camel ride and dinner under the stars, 40 minutes from Marrakech.</p>
<p>October to April: mild days, cool nights.</p>
<div class="ex-cta"><strong>From $560</strong><a class="mv-button" href="#exp-demo" data-mv-close>Book now</a></div>
</div>
</li>
</ul>
</mv-expandable>
<mv-expandable style="--mv-expandable-width: 30rem; --mv-expandable-panel-ratio: 2 / 1">
<p class="ex-label">Workshops · October</p>
<ul class="mv-expandable-grid" data-layout="list">
<li class="mv-expandable-card">
<img class="mv-expandable-media" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Cdefs%3E%3ClinearGradient id='c' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0' stop-color='oklch%280.8 0.12 250%29'/%3E%3Cstop offset='1' stop-color='oklch%280.5 0.16 290%29'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='120' height='120' fill='url%28%23c%29'/%3E%3Crect x='30' y='34' width='60' height='12' rx='6' fill='%23fff' opacity='.9'/%3E%3Crect x='30' y='54' width='44' height='12' rx='6' fill='%23fff' opacity='.7'/%3E%3Crect x='30' y='74' width='52' height='12' rx='6' fill='%23fff' opacity='.5'/%3E%3C/svg%3E" alt="">
<h3 class="mv-expandable-title">Cascade layers in practice</h3>
<p class="mv-expandable-description">Workshop · Tue, Oct 14 · 90 min</p>
<span class="mv-expandable-action" aria-hidden="true">Details</span>
<div class="mv-expandable-content">
<p>Order third-party styles, tokens and components with @layer so overrides stop being a fight. Bring a real project.</p>
<div class="ex-cta"><span>Hosted by Amara Okafor</span><button class="mv-button" type="button" data-mv-close>Reserve a seat · $49</button></div>
</div>
</li>
<li class="mv-expandable-card">
<img class="mv-expandable-media" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Cdefs%3E%3ClinearGradient id='c' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0' stop-color='oklch%280.8 0.12 160%29'/%3E%3Cstop offset='1' stop-color='oklch%280.5 0.16 200%29'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='120' height='120' fill='url%28%23c%29'/%3E%3Ccircle cx='60' cy='60' r='26' fill='none' stroke='%23fff' stroke-width='8' stroke-dasharray='120 60' opacity='.9'/%3E%3C/svg%3E" alt="">
<h3 class="mv-expandable-title">Motion that respects people</h3>
<p class="mv-expandable-description">Workshop · Thu, Oct 16 · 60 min</p>
<span class="mv-expandable-action" aria-hidden="true">Details</span>
<div class="mv-expandable-content">
<p>Springs, interruptible transitions and reduced-motion fallbacks that keep an interface lively without making anyone dizzy.</p>
<div class="ex-cta"><span>Hosted by Kenji Watanabe</span><button class="mv-button" type="button" data-mv-close>Reserve a seat · $39</button></div>
</div>
</li>
<li class="mv-expandable-card">
<img class="mv-expandable-media" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Cdefs%3E%3ClinearGradient id='c' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0' stop-color='oklch%280.8 0.12 30%29'/%3E%3Cstop offset='1' stop-color='oklch%280.5 0.16 70%29'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='120' height='120' fill='url%28%23c%29'/%3E%3Cpath d='M36 60h48M60 36v48' stroke='%23fff' stroke-width='10' stroke-linecap='round' opacity='.9'/%3E%3C/svg%3E" alt="">
<h3 class="mv-expandable-title">Keyboard-first components</h3>
<p class="mv-expandable-description">Workshop · Mon, Oct 20 · 75 min</p>
<span class="mv-expandable-action" aria-hidden="true">Details</span>
<div class="mv-expandable-content">
<p>Focus order, roving tab index and dialogs that return focus where it came from, tested live with a screen reader.</p>
<div class="ex-cta"><span>Hosted by Lucía Fernández</span><button class="mv-button" type="button" data-mv-close>Reserve a seat · $49</button></div>
</div>
</li>
</ul>
</mv-expandable>
</div>
API
Attributes
Name
Type
Default
Description
close-label
string
Close
Accessible name of the close button (overrides strings.close).
Properties
Name
Type
Description
strings
{ close, open }
Default texts, for translation. open is a template: "Open {title}". A card's data-label and the close-label attribute win. Assignable before the element is defined.
Methods
Name
Description
open(card | index)
Opens a card. Calling it on the card that is closing turns the morph back.
close()
Closes the open card; during the opening it reverses from where it is.
Events
Name
Description
mv-before-open
Cancelable; detail.card.
mv-open
Opening finished; detail.card.
mv-close
Closing finished; detail.card.
Content structure
Name
Description
.mv-expandable-card
Card (anywhere inside the element). data-label = accessible name of its trigger.
.mv-expandable-media / -title / -description
Shared parts that morph.
.mv-expandable-content
Content hidden in the card and moved (not copied) into the detail view, so its listeners and form state survive. [data-mv-close] inside it closes the view.
.mv-expandable-action
Decorative pill (“Listen”…) visible in the card.
CSS classes
Name
Description
mv-expandable-grid
Auto-fill grid; data-layout="list" for a thumbnail · text · action list.
mv-expandable-dialog / -body / -header / -close
Generated detail view (modal <dialog>); carries data-morph while in flight.
mv-expandable-trigger
Real generated button, stretched over the card.
CSS variables
Name
Default
Description
--mv-expandable-width
36rem
Width of the detail view.
--mv-expandable-duration
480ms
Approximate morph duration (drives the spring; set it on the element or an ancestor).
Each card gets a real <button> stretched over it (aria-haspopup=dialog, aria-expanded, aria-controls), named after its title or data-label. The detail view is a native modal <dialog>: focus moves to the Close button, the rest of the page is inert and scroll-locked, Tab stays inside, Escape, the Close button, any [data-mv-close] and a click on the backdrop close it, and focus returns to the card. aria-labelledby points at the heading, aria-describedby at the description. Reduced motion (OS setting or data-motion="reduce"): no morph, the view opens and closes at once. Hover lift only under a fine pointer; forced colors keep the focus ring and the dialog and close-button borders.