スキル一覧に戻る
mgd34msu

view-transitions

by mgd34msu

Plug in, Receive good vibes.

2🍴 0📅 2026年1月25日
GitHubで見るManusで実行

SKILL.md


name: view-transitions description: Creates smooth animated transitions between views using the native View Transitions API. Use when building page transitions, state changes, or morphing elements between different DOM states in SPAs and MPAs.

View Transitions API

Native browser API for smooth animated transitions between different views. Works in SPAs (JavaScript-triggered) and MPAs (navigation-based).

Browser Support

  • Same-document (SPA): Chrome 111+, Edge 111+, Safari 18+, Firefox 144+
  • Cross-document (MPA): Chrome 126+, Edge 126+, Safari 18.2+

Quick Start - SPA

// Check for support
if (!document.startViewTransition) {
  updateDOM();
  return;
}

// Trigger transition
document.startViewTransition(() => {
  updateDOM();
});

The API:

  1. Captures snapshots of current state
  2. Runs your callback to update DOM
  3. Captures snapshots of new state
  4. Animates between old and new states

Quick Start - MPA

No JavaScript needed. Opt in with CSS on both pages:

@view-transition {
  navigation: auto;
}

Transitions happen automatically on same-origin navigations.

Basic CSS Customization

/* Customize the default cross-fade */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.5s;
}

/* Fade out old view */
::view-transition-old(root) {
  animation: fade-out 0.3s ease-out both;
}

/* Fade in new view */
::view-transition-new(root) {
  animation: fade-in 0.3s ease-in both;
}

@keyframes fade-out {
  to { opacity: 0; }
}

@keyframes fade-in {
  from { opacity: 0; }
}

Named Transitions

Give specific elements their own transition behavior.

/* Assign a name to an element */
.header {
  view-transition-name: header;
}

.main-image {
  view-transition-name: main-image;
}

/* Names must be unique! */
/* Style transitions for named elements */
::view-transition-old(main-image),
::view-transition-new(main-image) {
  animation-duration: 0.4s;
}

/* Keep header fixed during transition */
::view-transition-group(header) {
  animation: none;
}

Pseudo-Element Tree

When a view transition runs, this pseudo-element tree is created:

::view-transition
└─ ::view-transition-group(name)
   └─ ::view-transition-image-pair(name)
      ├─ ::view-transition-old(name)
      └─ ::view-transition-new(name)
PseudoPurpose
::view-transitionOverlay containing all transitions
::view-transition-group(name)Container for size/position animation
::view-transition-image-pair(name)Contains old and new snapshots
::view-transition-old(name)Screenshot of old state
::view-transition-new(name)Live representation of new state

Transition Types

Categorize transitions for different styling:

document.startViewTransition({
  update: () => updateDOM(),
  types: ['slide-left']  // Add type identifiers
});
/* Style based on transition type */
html:active-view-transition-type(slide-left) {
  &::view-transition-old(root) {
    animation: slide-out-left 0.3s ease;
  }
  &::view-transition-new(root) {
    animation: slide-in-right 0.3s ease;
  }
}

@keyframes slide-out-left {
  to { transform: translateX(-100%); }
}

@keyframes slide-in-right {
  from { transform: translateX(100%); }
}

JavaScript API

ViewTransition Object

const transition = document.startViewTransition(() => {
  // Update DOM here
});

// Promises for timing
transition.ready.then(() => {
  // Pseudo-elements created, animation about to start
});

transition.updateCallbackDone.then(() => {
  // DOM update callback finished
});

transition.finished.then(() => {
  // Animation complete
});

// Skip the animation
transition.skipTransition();

Custom JavaScript Animation

const transition = document.startViewTransition(() => updateDOM());

transition.ready.then(() => {
  // Use Web Animations API on pseudo-elements
  document.documentElement.animate(
    [
      { clipPath: 'circle(0% at 50% 50%)' },
      { clipPath: 'circle(100% at 50% 50%)' }
    ],
    {
      duration: 500,
      easing: 'ease-out',
      pseudoElement: '::view-transition-new(root)'
    }
  );
});

React Integration

Basic Hook

function useViewTransition() {
  const startTransition = (callback) => {
    if (!document.startViewTransition) {
      callback();
      return;
    }
    document.startViewTransition(callback);
  };

  return { startTransition };
}

// Usage
function Component() {
  const { startTransition } = useViewTransition();
  const [page, setPage] = useState('home');

  const navigate = (newPage) => {
    startTransition(() => setPage(newPage));
  };

  return (
    <div>
      <nav>
        <button onClick={() => navigate('home')}>Home</button>
        <button onClick={() => navigate('about')}>About</button>
      </nav>
      {page === 'home' && <HomePage />}
      {page === 'about' && <AboutPage />}
    </div>
  );
}

With React Router

import { useNavigate } from 'react-router-dom';

function NavLink({ to, children }) {
  const navigate = useNavigate();

  const handleClick = (e) => {
    e.preventDefault();

    if (!document.startViewTransition) {
      navigate(to);
      return;
    }

    document.startViewTransition(() => {
      navigate(to);
    });
  };

  return <a href={to} onClick={handleClick}>{children}</a>;
}

React 19+ (Experimental)

import { unstable_ViewTransition as ViewTransition } from 'react';

function App() {
  const [page, setPage] = useState('home');

  return (
    <ViewTransition>
      {page === 'home' ? <Home /> : <About />}
    </ViewTransition>
  );
}

Common Patterns

Hero Image Morph

/* List page */
.thumbnail {
  view-transition-name: hero-image;
}

/* Detail page */
.hero-image {
  view-transition-name: hero-image;
}

/* The image will smoothly morph between pages */
::view-transition-group(hero-image) {
  animation-duration: 0.4s;
}

Shared Header

.header {
  view-transition-name: header;
}

/* Keep header in place */
::view-transition-old(header),
::view-transition-new(header) {
  animation: none;
  mix-blend-mode: normal;
}

Slide Transitions

@keyframes slide-from-right {
  from { transform: translateX(100%); }
}

@keyframes slide-to-left {
  to { transform: translateX(-100%); }
}

/* Forward navigation */
html:active-view-transition-type(forward) {
  &::view-transition-old(root) {
    animation: slide-to-left 0.3s ease forwards;
  }
  &::view-transition-new(root) {
    animation: slide-from-right 0.3s ease forwards;
  }
}

/* Back navigation */
html:active-view-transition-type(back) {
  &::view-transition-old(root) {
    animation: slide-from-right 0.3s ease reverse forwards;
  }
  &::view-transition-new(root) {
    animation: slide-to-left 0.3s ease reverse forwards;
  }
}

Circle Reveal

function circleReveal(x, y) {
  const transition = document.startViewTransition(() => updateContent());

  transition.ready.then(() => {
    const maxRadius = Math.hypot(
      Math.max(x, window.innerWidth - x),
      Math.max(y, window.innerHeight - y)
    );

    document.documentElement.animate(
      {
        clipPath: [
          `circle(0px at ${x}px ${y}px)`,
          `circle(${maxRadius}px at ${x}px ${y}px)`
        ]
      },
      {
        duration: 500,
        easing: 'ease-in-out',
        pseudoElement: '::view-transition-new(root)'
      }
    );
  });
}

// Trigger from click position
button.addEventListener('click', (e) => {
  circleReveal(e.clientX, e.clientY);
});

Grid Item Expansion

/* In grid */
.card {
  view-transition-name: var(--card-id);
}

/* When expanded */
.card-detail {
  view-transition-name: var(--card-id);
}
function Card({ id, onClick }) {
  return (
    <div
      className="card"
      style={{ '--card-id': `card-${id}` }}
      onClick={onClick}
    />
  );
}

MPA Cross-Document Transitions

Opt In

/* Add to both pages */
@view-transition {
  navigation: auto;
}

Customize by Page

<!-- page1.html -->
<html class="page-home">

<!-- page2.html -->
<html class="page-about">
/* Styles apply based on destination */
.page-about::view-transition-old(root) {
  animation: slide-out-left 0.3s;
}

Match Elements Across Pages

Use the new match-element value (Chrome 126+):

/* Automatically generate view-transition-name based on element identity */
.product-card {
  view-transition-name: match-element;
}

Or manually assign matching names:

/* page1.html */
.product-thumbnail[data-id="123"] {
  view-transition-name: product-123;
}

/* page2.html */
.product-hero[data-id="123"] {
  view-transition-name: product-123;
}

Accessibility

Respect Reduced Motion

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

Skip for Screen Readers

View transitions are purely visual; screen readers see the DOM update immediately.

Debugging

Chrome DevTools:

  1. Open Elements panel
  2. Check "Show view transition pseudo-elements"
  3. Inspect ::view-transition-* elements

Add slow-mo for development:

::view-transition-old(*),
::view-transition-new(*) {
  animation-duration: 3s !important;
}

Best Practices

  1. Keep transitions short - 200-400ms for most cases
  2. Use view-transition-name sparingly - Too many can hurt performance
  3. Avoid layout shifts - Elements should have stable sizes
  4. Test without transitions - Ensure DOM updates work without the API
  5. Handle edge cases - Elements only on one page need custom enter/exit
  6. Use types for direction - Forward vs back navigation should feel different

Fallback Strategy

async function navigate(callback) {
  // Feature detection
  if (!document.startViewTransition) {
    callback();
    return;
  }

  try {
    await document.startViewTransition(callback).finished;
  } catch (e) {
    // Transition was skipped or errored
    // DOM is still updated, just not animated
  }
}

スコア

総合スコア

60/100

リポジトリの品質指標に基づく評価

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

レビュー

💬

レビュー機能は近日公開予定です