Expandable Card <mv-expandable>

Cuadrícula o lista de tarjetas en la que una tarjeta se amplía hasta una vista de detalle modal accesible: su caja, imagen, título y descripción viajan con un único resorte interrumpible, el texto y las imágenes nunca se deforman, y con movimiento reducido se abre al instante.

CategoríaTransiciones
TipoWeb Component (<mv-expandable>)
Estadoestable
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

Instalación

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

Agente de IA con el servidor MCP de Marvelous UI: install_components({ slugs: ["expandable-card"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Archivos copiados (dependencias incluidas): 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.

Uso

Marcado de referencia: parte de él y personalízalo con atributos, data-* y 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

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

Properties

NameTipoDescription
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.

Esta página se tradujo con IA. Informar de un problema de traducción