Grade ou lista de cards em que um card cresce até uma visualização de detalhe modal acessível: a caixa, a imagem, o título e a descrição viajam numa única mola interrompível, texto e imagens nunca se deformam, e com movimento reduzido ele abre na hora.
Categoria
Transições
Tipo
Web Component (<mv-expandable>)
Status
estável
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
Agente de IA com o servidor MCP do Marvelous UI: install_components({ slugs: ["expandable-card"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Marcação de referência, para usar como ponto de partida e personalizar com atributos, data-* e variáveis 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
Tipo
Default
Description
close-label
string
Close
Accessible name of the close button (overrides strings.close).
Properties
Name
Tipo
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.