Exclusivobeta

Blur Up <mv-blur-up>

Envolve um <img> nativo (lazy loading, srcset, sizes, alt e <picture> intactos): mantém a proporção para que nada se desloque, mostra uma imagem minúscula desfocada, a cor dominante ou um gradiente, e faz uma transição cruzada para a foto assim que ela é decodificada. Onde os outros erram: uma imagem em cache aparece na hora, sem fade nem piscada, uma que falha continua como uma caixa discreta com seu texto alt e nunca como um ícone quebrado, e imagens adicionadas depois ou trocadas mantêm o tratamento.

CategoriaMídia
TipoWeb Component (<mv-blur-up>)
Statusbeta
Keywordsexclusive, light, image, lazy-loading, placeholder, blur-up, lqip, progressive, layout-shift, cls

When to use

  • Product grids and listing cards show many photos that should hold their space and appear smoothly as they load
  • A blog or magazine article has large inline photos and the page must not jump while they download
  • User-uploaded or CDN images may fail and the card should degrade to a quiet box that still shows the alt text
  • Images arrive later or change (infinite scroll, color swatches, client-side routing) and need the same treatment

Avoid when

  • The whole card or section is loading, text included, not only its image → use Skeleton instead
  • The image is the page's main hero (LCP): load it eagerly with fetchpriority="high" and skip the fade

Instalação

node scripts/add.mjs blur-up --out ./src/marvelous

Agente de IA com o servidor MCP do Marvelous UI: install_components({ slugs: ["blur-up"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Arquivos copiados (dependências incluídas): tokens/tokens.css, core/base.css, core/element.js, core/motion.js, components/blur-up/blur-up.js, components/blur-up/blur-up.css.

Uso

Início rápido, a menor marcação que funciona:

<mv-blur-up color="#c9b8a6"><img src="photo.jpg" width="1200" height="800" alt="…" loading="lazy"></mv-blur-up>

Marcação de referência, para usar como ponto de partida e personalizar com atributos, data-* e variáveis CSS:

<div id="mv-blur-up-demo" style="display:grid;gap:1rem;width:100%;max-width:1040px;margin-inline:auto">
  <div style="display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:.75rem">
    <div>
      <h3 style="margin:0;font-size:1.05rem">Stays for your fall trip</h3>
      <p style="margin:.15rem 0 0;font-size:.8rem;color:var(--mv-fg-muted)">Cached images appear at once. Replay to watch them arrive over a slow connection.</p>
    </div>
    <button class="mv-button" data-variant="outline" data-size="sm" type="button" id="mv-blur-up-demo-replay">Replay slow connection</button>
  </div>
  <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,210px),1fr));gap:1.25rem">
    <article style="display:grid;gap:.35rem;align-content:start">
      <mv-blur-up placeholder="data:image/webp;base64,UklGRlgAAABXRUJQVlA4IEwAAADwAQCdASoQAAwAAsBMJbACdADwHvFFAAAA/uq9FTlhAYVr9CpNv+3fa637vG69meSpV8Pb+fWc/9jv4Q4sVfmFSB68LDC/ejCofoAA" style="border-radius:var(--mv-radius-lg);margin-bottom:.3rem">
        <img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 800 600' preserveAspectRatio='xMidYMid slice'%3E%3Cdefs%3E%3ClinearGradient id='s' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%23f6c56b'/%3E%3Cstop offset='.55' stop-color='%23f39a4b'/%3E%3Cstop offset='1' stop-color='%23e3643a'/%3E%3C/linearGradient%3E%3CradialGradient id='g'%3E%3Cstop offset='0' stop-color='%23fff1c9' stop-opacity='.9'/%3E%3Cstop offset='1' stop-color='%23fff1c9' stop-opacity='0'/%3E%3C/radialGradient%3E%3C/defs%3E%3Crect width='800' height='600' fill='url(%23s)'/%3E%3Ccircle cx='400' cy='250' r='270' fill='url(%23g)'/%3E%3Ccircle cx='400' cy='250' r='90' fill='%23fff1c9'/%3E%3Cpath d='M0 600L0 360L0 397L50 366L100 370L150 354L200 351L250 370L300 347L350 329L400 325L450 298L500 308L550 344L600 355L650 388L700 408L750 386L800 387L800 600Z' fill='%23c4552f' opacity='.92'/%3E%3Cpath d='M0 600L0 420L0 469L50 470L100 453L150 449L200 418L250 398L300 408L350 400L400 403L450 414L500 392L550 384L600 392L650 387L700 414L750 447L800 450L800 600Z' fill='%238f3423'/%3E%3Cpath d='M0 600L0 490L0 464L50 476L100 474L150 473L200 484L250 469L300 462L350 475L400 475L450 495L500 523L550 521L600 524L650 520L700 495L750 492L800 494L800 600Z' fill='%235a1f18'/%3E%3C/svg%3E" width="800" height="600" alt="Terracotta rooftops of Alfama glowing under a low sun" loading="lazy">
      </mv-blur-up>
      <div style="display:flex;justify-content:space-between;gap:.5rem;font-size:.9rem"><strong>Casa Alfama</strong><span>$184 <span style="color:var(--mv-fg-muted)">night</span></span></div>
      <div style="color:var(--mv-fg-muted);font-size:.8rem">Lisbon, Portugal · Oct 2-6</div>
      <div style="font:.7rem/1.4 var(--mv-font-mono);color:var(--mv-fg-subtle)">placeholder · 96-byte WebP</div>
    </article>
    <article style="display:grid;gap:.35rem;align-content:start">
      <mv-blur-up color="#ba7083" style="border-radius:var(--mv-radius-lg);margin-bottom:.3rem">
        <img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 800 600' preserveAspectRatio='xMidYMid slice'%3E%3Cdefs%3E%3ClinearGradient id='s' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%232a1b3d'/%3E%3Cstop offset='.55' stop-color='%23b85c7a'/%3E%3Cstop offset='1' stop-color='%23f2a88a'/%3E%3C/linearGradient%3E%3CradialGradient id='g'%3E%3Cstop offset='0' stop-color='%23ffe2c2' stop-opacity='.9'/%3E%3Cstop offset='1' stop-color='%23ffe2c2' stop-opacity='0'/%3E%3C/radialGradient%3E%3C/defs%3E%3Crect width='800' height='600' fill='url(%23s)'/%3E%3Ccircle cx='560' cy='330' r='210' fill='url(%23g)'/%3E%3Ccircle cx='560' cy='330' r='70' fill='%23ffe2c2'/%3E%3Cpath d='M0 600L0 360L0 396L50 400L100 376L150 385L200 382L250 375L300 402L350 394L400 369L450 361L500 322L550 298L600 315L650 316L700 335L750 369L800 358L800 600Z' fill='%235b2e55' opacity='.92'/%3E%3Cpath d='M0 600L0 420L0 456L50 423L100 412L150 420L200 407L250 414L300 417L350 388L400 384L450 383L500 376L550 408L600 434L650 440L700 462L750 455L800 433L800 600Z' fill='%233a1f40'/%3E%3Cpath d='M0 600L0 490L0 446L50 461L100 466L150 481L200 504L250 498L300 498L350 505L400 495L450 506L500 523L550 515L600 517L650 507L700 475L750 467L800 462L800 600Z' fill='%231c1026'/%3E%3Crect y='520' width='800' height='80' fill='%23241430'/%3E%3Crect x='500' y='528' width='120' height='3' fill='%23ffe2c2' opacity='.5'/%3E%3Crect x='526' y='542' width='68' height='2' fill='%23ffe2c2' opacity='.35'/%3E%3C/svg%3E" width="800" height="600" alt="Hills above Kyoto at dusk, seen from a ryokan terrace" loading="lazy">
      </mv-blur-up>
      <div style="display:flex;justify-content:space-between;gap:.5rem;font-size:.9rem"><strong>Ryokan Hanaya</strong><span>$236 <span style="color:var(--mv-fg-muted)">night</span></span></div>
      <div style="color:var(--mv-fg-muted);font-size:.8rem">Kyoto, Japan · Oct 14-18</div>
      <div style="font:.7rem/1.4 var(--mv-font-mono);color:var(--mv-fg-subtle)">color · dominant color</div>
    </article>
    <article style="display:grid;gap:.35rem;align-content:start">
      <mv-blur-up gradient="linear-gradient(#123a60, #3f7fb0 55%, #1d3c57)" style="border-radius:var(--mv-radius-lg);margin-bottom:.3rem">
        <img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 800 600' preserveAspectRatio='xMidYMid slice'%3E%3Cdefs%3E%3ClinearGradient id='s' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%230f2a4a'/%3E%3Cstop offset='.55' stop-color='%233f7fb0'/%3E%3Cstop offset='1' stop-color='%23bfe0ee'/%3E%3C/linearGradient%3E%3CradialGradient id='g'%3E%3Cstop offset='0' stop-color='%23f4fbff' stop-opacity='.9'/%3E%3Cstop offset='1' stop-color='%23f4fbff' stop-opacity='0'/%3E%3C/radialGradient%3E%3C/defs%3E%3Crect width='800' height='600' fill='url(%23s)'/%3E%3Ccircle cx='220' cy='180' r='120' fill='url(%23g)'/%3E%3Ccircle cx='220' cy='180' r='40' fill='%23f4fbff'/%3E%3Cpath d='M0 600L0 360L0 369L50 360L100 358L150 340L200 357L250 357L300 328L350 327L400 313L450 301L500 338L550 361L600 378L650 412L700 401L750 383L800 390L800 600Z' fill='%232e5877' opacity='.92'/%3E%3Cpath d='M0 600L0 352L0 320L67 328L133 342L200 371L267 369L333 375L400 356L467 369L533 364L600 371L667 348L733 332L800 318L800 600Z' fill='%23fff' opacity='.18'/%3E%3Cpath d='M0 600L0 420L0 439L50 426L100 430L150 417L200 382L250 379L300 375L350 374L400 410L450 427L500 429L550 446L600 435L650 425L700 444L750 444L800 450L800 600Z' fill='%231d3c57'/%3E%3Cpath d='M0 600L0 490L0 496L50 513L100 516L150 531L200 526L250 500L300 493L350 476L400 463L450 478L500 477L550 473L600 481L650 466L700 460L750 478L800 484L800 600Z' fill='%230f2233'/%3E%3Crect y='520' width='800' height='80' fill='%23123049'/%3E%3Crect x='160' y='528' width='120' height='3' fill='%23f4fbff' opacity='.5'/%3E%3Crect x='186' y='542' width='68' height='2' fill='%23f4fbff' opacity='.35'/%3E%3C/svg%3E" width="800" height="600" alt="Fjord and snowy ridges outside the Solberg cabin" loading="lazy">
      </mv-blur-up>
      <div style="display:flex;justify-content:space-between;gap:.5rem;font-size:.9rem"><strong>Solberg Cabin</strong><span>$158 <span style="color:var(--mv-fg-muted)">night</span></span></div>
      <div style="color:var(--mv-fg-muted);font-size:.8rem">Tromsø, Norway · Nov 3-7</div>
      <div style="font:.7rem/1.4 var(--mv-font-mono);color:var(--mv-fg-subtle)">gradient · CSS gradient</div>
    </article>
    <article style="display:grid;gap:.35rem;align-content:start">
      <mv-blur-up placeholder="data:image/webp;base64,UklGRlAAAABXRUJQVlA4IEQAAAAQAgCdASoQAAwAAsBMJYgCdAEO9KZcXd/AAP6jaiAzWAc7aWxMBso6uj17/L9mJvMdTAvEWsYDrxGfVvigvzPpHAAAAA==" style="border-radius:var(--mv-radius-lg);margin-bottom:.3rem">
        <img src="data:image/jpeg;base64,AAAA" width="800" height="600" alt="Tiled courtyard of Riad Zitoun with an orange tree" loading="lazy">
      </mv-blur-up>
      <div style="display:flex;justify-content:space-between;gap:.5rem;font-size:.9rem"><strong>Riad Zitoun</strong><span>$129 <span style="color:var(--mv-fg-muted)">night</span></span></div>
      <div style="color:var(--mv-fg-muted);font-size:.8rem">Marrakesh, Morocco · Nov 20-24</div>
      <div style="font:.7rem/1.4 var(--mv-font-mono);color:var(--mv-fg-subtle)">failed load · alt text, no broken icon</div>
    </article>
  </div>
</div>
<script type="module">
  const root = document.getElementById("mv-blur-up-demo");
  let run = 0;
  root.querySelector("#mv-blur-up-demo-replay").addEventListener("click", () => {
    const id = ++run;
    root.querySelectorAll("mv-blur-up img").forEach((img, i) => {
      const src = (img.dataset.src ??= img.getAttribute("src"));
      img.removeAttribute("src");
      // A new URL each time, so the browser cannot answer from its cache.
      setTimeout(() => { if (id === run) img.src = `${src}#replay-${id}`; }, 900 + i * 450);
    });
  });
</script>

API

Attributes

NameTipoDefaultDescription
placeholderURLTiny preview image shown blurred (a data: URI of ~100 bytes is ideal). Build-time recipe, no runtime tool: Node + sharp "data:image/webp;base64," + (await sharp(file).resize(16).webp({ quality: 40 }).toBuffer()).toString("base64"), or ImageMagick magick photo.jpg -resize 16x -strip -quality 40 webp:- | base64 -w0.
colorCSS colorvar(--mv-bg-muted)Solid dominant color, shown under the placeholder or alone. Recipe: magick photo.jpg -resize 1x1! -format "#%[hex:p{0,0}]" info: (or sharp's stats().dominant).
gradientCSS gradientTiny CSS placeholder with zero extra bytes to fetch, e.g. linear-gradient(#123a60, #3f7fb0) from two sampled colors. Drawn under placeholder when both are set.
data-stateloading | loaded | errorSet by the component (for styling). Absent until the component runs: without JS the image simply shows.
data-instantbooleanSet by the component when the image was shown without a fade (cached, loaded before the placeholder was painted, or reduced motion).

Properties

NameTipoDescription
state"loading" | "loaded" | "error"Current state (read-only).
imageHTMLImageElement | nullThe wrapped <img> (read-only).

Events

NameDescription
mv-loadThe real image replaced the placeholder; detail = { src, instant } (instant = no fade was played).
mv-errorThe image failed and the fallback box is shown; detail = { src }.

Content structure

NameDescription
(default)One <img> with width and height attributes (they reserve the ratio), or a <picture> around it. It may be added, replaced, or have its src/srcset changed at any time.

CSS classes

NameDescription
mv-blur-up-fallbackGenerated in the error state only: icon and alt text over the dimmed placeholder (aria-hidden).
mv-blur-up-alt / mv-blur-up-iconParts of the fallback.

CSS variables

NameDefaultDescription
--mv-blur-up-blur20pxBlur radius of the placeholder.
--mv-blur-up-durationvar(--mv-duration-slower)Cross-fade duration.
--mv-blur-up-fitcoverobject-fit of the image when the box has its own height or aspect-ratio.
--mv-blur-up-placeholdernonePlaceholder image(s) from CSS, e.g. url(…) set server-side in a style attribute so it shows even before or without JS (the attributes win).
--mv-blur-up-colorvar(--mv-bg-muted)Placeholder color from CSS (the color attribute wins).

Accessibility

The native <img> stays the only thing assistive tech meets: its alt, srcset, sizes and loading are never touched, and the placeholder is a CSS pseudo-element with no accessible name. Nothing is announced (image loading is not news). On failure the <img> is kept, only made transparent, so screen readers still read its alt; the visible fallback repeats that alt next to a small icon and is aria-hidden, so it is never read twice, and an empty alt (decorative image) shows only the icon. The box keeps its ratio from the width/height attributes in every state, so nothing shifts on load or on error. Not focusable by itself: a link or button around it keeps its own focus ring. Reduced motion (system or data-motion="reduce"): no fade, the image replaces the placeholder at once. Cached images, and images that load before the placeholder was ever painted, never fade either. Forced colors: the placeholder keeps its colors like an image would, the fallback uses Canvas and CanvasText. Print: the real image at full opacity. Without JavaScript the <img> behaves exactly as native (with --mv-blur-up-placeholder, the placeholder still shows behind it).

Esta página foi traduzida com IA. Informar um problema de tradução