Expandable Card <mv-expandable>

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égorieTransitions
TypeWeb Component (<mv-expandable>)
Statutstable
Keywordsexpandable, 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

Installation

node scripts/add.mjs expandable-card --out ./src/marvelous

Agent IA avec le serveur MCP de Marvelous UI : install_components({ slugs: ["expandable-card"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Fichiers copiés (dépendances comprises) : tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/focus.js, core/motion.js, components/expandable-card/expandable-card.js, components/expandable-card/expandable-card.css.

Utilisation

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

NameTypeDefaultDescription
close-labelstringCloseAccessible name of the close button (overrides strings.close).

Properties

NameTypeDescription
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

NameDescription
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

NameDescription
mv-before-openCancelable; detail.card.
mv-openOpening finished; detail.card.
mv-closeClosing finished; detail.card.

Content structure

NameDescription
.mv-expandable-cardCard (anywhere inside the element). data-label = accessible name of its trigger.
.mv-expandable-media / -title / -descriptionShared parts that morph.
.mv-expandable-contentContent 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-actionDecorative pill (“Listen”…) visible in the card.

CSS classes

NameDescription
mv-expandable-gridAuto-fill grid; data-layout="list" for a thumbnail · text · action list.
mv-expandable-dialog / -body / -header / -closeGenerated detail view (modal <dialog>); carries data-morph while in flight.
mv-expandable-triggerReal generated button, stretched over the card.

CSS variables

NameDefaultDescription
--mv-expandable-width36remWidth of the detail view.
--mv-expandable-duration480msApproximate morph duration (drives the spring; set it on the element or an ancestor).
--mv-expandable-media-ratio / --mv-expandable-panel-ratio4 / 3 · 16 / 9Image ratios in the card and in the view.
--mv-expandable-min13remMinimum column width.
--mv-expandable-gapvar(--mv-space-4)Gap between cards.
--mv-expandable-thumb3.5remThumbnail size in the list layout.
--mv-expandable-radiusvar(--mv-radius-2xl)Radius of the detail view.

Accessibility

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.

Cette page a été traduite par IA. Signaler un problème de traduction