beta

Page Transition .mv-page-transition

Transições de página nativas com a View Transitions API: fade, deslize tipo push, zoom em profundidade e uma revelação circular a partir do clique, cada uma com uma versão de volta, para sites de várias páginas e SPAs, levando o foco para a nova visualização.

CategoriaTransições
TipoUtilitário JavaScript (.mv-page-transition)
Statusbeta
Keywordspage-transition, view-transitions, route, spa, mpa, navigation, reveal, push, focus-management

When to use

  • A multi-page site should animate between pages with a fade, push slide, depth zoom or a reveal from the clicked link
  • A single-page app needs animated route changes that play in reverse on back and move focus to the new view
  • Only the main content area should transition while the header and navigation stay still

Avoid when

  • The transition is a card opening into its detail view on the same page → use Expandable Card instead
  • Only a container changes size between views, not the page → use Morph Panel instead
  • Users must get identical transitions in every browser; unsupported browsers navigate without animation

Instalação

node scripts/add.mjs page-transition --out ./src/marvelous

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

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

Uso

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

<div id="pt-demo">
  <style>
    #pt-demo { display: grid; gap: 1rem; justify-items: center; width: 100%; }
    #pt-demo .pt-window { width: min(40rem, 100%); border: 1px solid var(--mv-border); border-radius: var(--mv-radius-xl); background: var(--mv-surface); box-shadow: var(--mv-shadow-lg); overflow: hidden; }
    #pt-demo .pt-chrome { display: flex; align-items: center; gap: .75rem; padding: .55rem .85rem; border-bottom: 1px solid var(--mv-border); background: var(--mv-bg-subtle); }
    #pt-demo .pt-lights { display: flex; gap: 6px; }
    #pt-demo .pt-lights i { width: 10px; height: 10px; border-radius: 50%; background: var(--mv-border-strong); }
    #pt-demo .pt-address { flex: 1; min-width: 0; padding: .28rem .7rem; border-radius: var(--mv-radius-full); background: var(--mv-bg); border: 1px solid var(--mv-border); font: 500 .75rem var(--mv-font-mono); color: var(--mv-fg-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    #pt-demo nav { display: flex; align-items: center; gap: .25rem; padding: .5rem .75rem; border-bottom: 1px solid var(--mv-border); }
    #pt-demo nav a { padding: .35rem .7rem; border-radius: var(--mv-radius-md); color: var(--mv-fg-muted); font-size: .8125rem; font-weight: 500; text-decoration: none; }
    #pt-demo nav a:hover { color: var(--mv-fg); background: var(--mv-bg-muted); }
    #pt-demo nav a:focus-visible { outline: none; box-shadow: var(--mv-focus-ring); }
    #pt-demo nav a[aria-current="page"] { color: var(--mv-fg); background: var(--mv-bg-muted); }
    #pt-demo .mv-page-transition-scope { height: 15.5rem; padding: 1.5rem 1.75rem; background: var(--mv-surface); outline: none; }
    #pt-demo h3 { width: fit-content; margin: 0 -.25rem .4rem; padding: 0 .25rem; font-size: 1.35rem; letter-spacing: -.02em; border-radius: var(--mv-radius-sm); }
    #pt-demo h3:focus { outline: none; }
    #pt-demo h3:focus-visible { box-shadow: var(--mv-focus-ring); }
    #pt-demo .pt-lead { margin: 0 0 1.1rem; color: var(--mv-fg-muted); font-size: .9rem; max-width: 36ch; }
    #pt-demo .pt-overview { display: grid; grid-template-columns: 1.2fr 1fr; gap: 1.25rem; align-items: center; height: 100%; }
    #pt-demo .pt-spark { display: grid; gap: .45rem; padding: .9rem; border: 1px solid var(--mv-border); border-radius: var(--mv-radius-lg); background: var(--mv-bg-subtle); font-size: .75rem; color: var(--mv-fg-muted); }
    #pt-demo .pt-spark strong { color: var(--mv-fg); font-size: 1.5rem; letter-spacing: -.02em; }
    #pt-demo .pt-bars { display: flex; align-items: end; gap: 4px; height: 3.25rem; }
    #pt-demo .pt-bars i { flex: 1; border-radius: 3px 3px 0 0; background: var(--mv-accent); opacity: .85; }
    #pt-demo .pt-plans { display: grid; grid-template-columns: repeat(3, 1fr); gap: .6rem; }
    #pt-demo .pt-plan { padding: .85rem; border: 1px solid var(--mv-border); border-radius: var(--mv-radius-lg); font-size: .8rem; color: var(--mv-fg-muted); }
    #pt-demo .pt-plan b { display: block; color: var(--mv-fg); font-size: .85rem; }
    #pt-demo .pt-plan strong { display: block; margin: .35rem 0 .15rem; color: var(--mv-fg); font-size: 1.35rem; letter-spacing: -.02em; }
    #pt-demo .pt-plan[data-featured] { border-color: var(--mv-accent); box-shadow: 0 0 0 3px var(--mv-accent-subtle); }
    #pt-demo .pt-list { display: grid; gap: .5rem; margin: 0; padding: 0; list-style: none; }
    #pt-demo .pt-list li { display: flex; align-items: center; gap: .75rem; padding: .5rem .65rem; border: 1px solid var(--mv-border); border-radius: var(--mv-radius-lg); font-size: .8rem; color: var(--mv-fg-muted); }
    #pt-demo .pt-list b { color: var(--mv-fg); font-size: .82rem; font-weight: 600; }
    #pt-demo .pt-list time { margin-inline-start: auto; font-variant-numeric: tabular-nums; }
    #pt-demo .pt-dot { width: .5rem; height: .5rem; border-radius: 50%; background: var(--mv-accent); flex: none; }
    #pt-demo .pt-controls { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: .375rem; }
    #pt-demo .pt-controls .mv-button[aria-pressed="true"] { background: var(--mv-fg); color: var(--mv-bg); }
    @media (max-width: 560px) {
      #pt-demo .pt-plans { grid-template-columns: 1fr 1fr; }
      #pt-demo .pt-overview { grid-template-columns: 1fr; }
      #pt-demo .mv-page-transition-scope { height: auto; min-height: 15.5rem; }
    }
  </style>

  <div class="pt-window">
    <div class="pt-chrome" aria-hidden="true"><span class="pt-lights"><i></i><i></i><i></i></span><span class="pt-address" data-pt-address>northwind.app/</span></div>
    <nav aria-label="Demo app">
      <a href="#overview" data-pt-page="0" aria-current="page">Overview</a>
      <a href="#pricing" data-pt-page="1">Pricing</a>
      <a href="#activity" data-pt-page="2">Activity</a>
    </nav>
    <main class="mv-page-transition-scope" tabindex="-1" data-pt-view></main>
  </div>

  <div class="pt-controls" role="group" aria-label="Transition preset">
    <button class="mv-button" data-variant="secondary" data-size="sm" type="button" data-pt-type="fade" aria-pressed="false">Fade</button>
    <button class="mv-button" data-variant="secondary" data-size="sm" type="button" data-pt-type="slide" aria-pressed="true">Slide</button>
    <button class="mv-button" data-variant="secondary" data-size="sm" type="button" data-pt-type="scale" aria-pressed="false">Zoom</button>
    <button class="mv-button" data-variant="secondary" data-size="sm" type="button" data-pt-type="reveal" aria-pressed="false">Reveal</button>
  </div>

  <template data-pt-tpl="overview">
    <div class="pt-overview">
      <div>
        <h3>Good morning, Aiko</h3>
        <p class="pt-lead">Revenue is up this week. Pick a page above, or go back, to see the transition play in both directions.</p>
      </div>
      <div class="pt-spark">
        <span>Weekly revenue</span>
        <strong>$48,210</strong>
        <div class="pt-bars" aria-hidden="true"><i style="height:38%"></i><i style="height:52%"></i><i style="height:46%"></i><i style="height:64%"></i><i style="height:58%"></i><i style="height:81%"></i><i style="height:100%"></i></div>
      </div>
    </div>
  </template>
  <template data-pt-tpl="pricing">
    <h3>Pricing</h3>
    <p class="pt-lead">Start free, upgrade when your team grows.</p>
    <div class="pt-plans">
      <div class="pt-plan"><b>Starter</b><strong>$0</strong>2 seats</div>
      <div class="pt-plan" data-featured><b>Growth</b><strong>$24</strong>per seat, monthly</div>
      <div class="pt-plan"><b>Scale</b><strong>$59</strong>SSO and audit log</div>
    </div>
  </template>
  <template data-pt-tpl="activity">
    <h3>Activity</h3>
    <p class="pt-lead">What changed across your workspace today.</p>
    <ul class="pt-list">
      <li><span class="pt-dot"></span><b>Mateo Silva</b> shipped the checkout redesign<time datetime="09:42">9:42 AM</time></li>
      <li><span class="pt-dot"></span><b>Grace Okafor</b> invited 3 teammates<time datetime="11:05">11:05 AM</time></li>
      <li><span class="pt-dot"></span><b>Lena Fischer</b> closed 12 support tickets<time datetime="14:18">2:18 PM</time></li>
    </ul>
  </template>

  <script type="module">
    // A tiny router: three views inside the frame, direction taken from the nav order.
    const root = document.getElementById("pt-demo");
    const src = document.querySelector('script[src*="page-transition/page-transition.js"]')?.src;
    const { navigateWithTransition } = await import(src ?? "/components/page-transition/page-transition.js");
    const pages = [["overview", "/"], ["pricing", "/pricing"], ["activity", "/activity"]];
    const view = root.querySelector("[data-pt-view]");
    const address = root.querySelector("[data-pt-address]");
    const links = [...root.querySelectorAll("[data-pt-page]")];
    let current = 0;
    let type = "slide";
    const render = (i) => {
      view.replaceChildren(root.querySelector(`[data-pt-tpl="${pages[i][0]}"]`).content.cloneNode(true));
      address.textContent = `northwind.app${pages[i][1]}`;
      links.forEach((a, j) => (j === i ? a.setAttribute("aria-current", "page") : a.removeAttribute("aria-current")));
      current = i;
    };
    render(0);
    root.addEventListener("click", (e) => {
      const link = e.target.closest("[data-pt-page]");
      if (link) {
        e.preventDefault();
        const i = Number(link.dataset.ptPage);
        if (i === current) return;
        navigateWithTransition(() => render(i), { type, direction: i < current ? "back" : "forward", origin: e, scope: view });
        return;
      }
      const preset = e.target.closest("[data-pt-type]");
      if (preset) {
        type = preset.dataset.ptType;
        for (const b of root.querySelectorAll("[data-pt-type]")) b.setAttribute("aria-pressed", String(b === preset));
      }
    });
  </script>
</div>

API

Attributes

NameTipoDefaultDescription
class="mv-page-transition" (on <html>)Animates the whole page (root group) with the chosen preset. Including the CSS also enables @view-transition { navigation: auto } for same-origin multi-page navigations, except with reduced motion.
data-transition (on <html>)fade | slide | scale | revealfadePreset. Set for the duration of a transition by navigateWithTransition().
data-transition-direction (on <html>)forward | backPlays the preset in reverse on back. Filled in automatically (SPA: direction option; multi-page: Navigation API history moves). Slide also follows dir="rtl".
data-page-focusOn an element of the new view: receives focus after navigation instead of the first h1-h3.

Methods

NameDescription
navigateWithTransition(update, { type, direction, origin, scope, focus, announce })Runs update() inside a same-document View Transition (with transition types when supported). origin = the click event or {x, y}: center of “reveal”, whose circle is sized to the farthest corner; keyboard clicks use the clicked element’s center. focus = element, selector or false (default: the scope, else <main>; its first [data-page-focus] or h1-h3 is focused). announce = "auto" (the new title is spoken when focus did not move), a string, or false. Without the API or with reduced motion the update runs at once and focus still moves. Returns the ViewTransition or a settled stand-in.
initPageTransitions({ type, signal })Multi-page sites: sets the class and preset, plays back history moves in reverse and starts “reveal” from the link clicked on the previous page. Skips the transition under data-motion="reduce". Load it in <head> with blocking="render".
PRESETSThe available presets: fade, slide, scale, reveal.

CSS classes

NameDescription
mv-page-transitionOn <html>: full-page transitions.
mv-page-transition-scopeOnly this region animates (e.g. <main>); the rest of the page stays still. One per page (view-transition-name: mv-page).

CSS variables

NameDefaultDescription
--mv-page-transition-duration420msBase duration; every preset’s timings derive from it.
--mv-page-transition-easingcubic-bezier(0.32, 0.72, 0, 1)Curve of the slide and of the group (size/position).
--mv-pt-x / --mv-pt-y / --mv-pt-r50% 50% 150%Center and radius of the “reveal” circle, set from origin.

Accessibility

Focus moves to the new view after every navigation (its first [data-page-focus] or h1-h3, made focusable with tabindex="-1" until it blurs), which screen readers read out; when focus is not moved the new title is announced in a polite live region. Reduced motion (OS setting or data-motion="reduce"): no animation, the update is instant, focus still moves. Without View Transitions support navigation works normally (progressive enhancement).

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