<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ view transitions - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ view transitions - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Thu, 04 Jun 2026 05:19:53 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/view-transitions/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Use the View Transition API for Better Web Transitions ]]>
                </title>
                <description>
                    <![CDATA[ If you want to add some amazing and visually appealing animations to your web page, the View Transition API is a great animation tool. It lets you create Cross-Document Transitions when navigating between pages. And not just in classic multi-page app... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-use-the-view-transition-api/</link>
                <guid isPermaLink="false">6864040518da4fac8afe4a5d</guid>
                
                    <category>
                        <![CDATA[ view transitions ]]>
                    </category>
                
                    <category>
                        <![CDATA[ CSS Animation ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Sumit Saha ]]>
                </dc:creator>
                <pubDate>Tue, 01 Jul 2025 15:51:33 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1751324398272/d2f05e29-6925-43da-8c41-14b1c18a4898.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>If you want to add some amazing and visually appealing animations to your web page, the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API"><strong>View Transition API</strong></a> is a great animation tool. It lets you create Cross-Document Transitions when navigating between pages. And not just in classic multi-page apps – you can also use it to build eye-catching transitions in single-page applications.</p>
<p>In this article, you’ll learn how to:</p>
<ul>
<li><p>Enable cross-document transitions with a single line of CSS</p>
</li>
<li><p>Animate individual elements like titles and images</p>
</li>
<li><p>Debug and fine-tune your transitions</p>
</li>
<li><p>Apply the same API to dynamic interactions in single-page apps using JavaScript</p>
</li>
<li><p>Get an idea of how this works in a <a target="_blank" href="https://react.dev/">React</a> or <a target="_blank" href="https://nextjs.org/">Next.js</a> environment.</p>
</li>
</ul>
<h2 id="heading-heres-what-well-cover">Here’s what we’ll cover:</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-example-setup">Example Setup</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-enabling-cross-document-transitions">Enabling Cross-Document Transitions</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-debug-transition">Debug Transition</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-first-view-transition">First View Transition</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-understanding-view-transition-internals">Understanding View Transition Internals</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-animating-images-across-pages">Animating Images Across Pages</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-endless-animation-opportunities">Endless Animation Opportunities</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-single-page-experience">Single-Page Experience</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-set-up-the-event-listener">Set Up the Event Listener</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-the-actual-addstorycard-function">The Actual addStoryCard() Function</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-applying-the-animation">Applying the Animation</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-view-transition-in-reactjs">View Transition in React.js</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-view-transition-in-nextjs">View Transition in Next.js</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-browser-support">Browser Support</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-wrap-up">Wrap-Up</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-summary">Summary</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along and get the most out of this guide, you should have:</p>
<ol>
<li><p><strong>Basic HTML and CSS:</strong> You should understand how to structure a web page using HTML and apply styles using CSS.</p>
</li>
<li><p><strong>JavaScript fundamentals:</strong> Familiarity with JavaScript DOM manipulation, event handling, and basic functions will help you follow along with the dynamic examples.</p>
</li>
<li><p><strong>Modern browser environment:</strong> The View Transition API is currently supported in Chromium-based browsers like Chrome and Edge. Make sure you’re using a compatible browser.</p>
</li>
<li><p><strong>React and Next.js basics (optional):</strong> Toward the end of the article, we explore how to integrate view transitions in React and Next.js. Basic knowledge of component structure and routing in these frameworks will be helpful, though not strictly required for the core concepts.</p>
</li>
</ol>
<p>If you’re new to any of these topics, you can still follow along and revisit the article later with hands-on practice.</p>
<p>I’ve also created a video to go along with this article. If you’re the type who likes to learn from video as well as text, you can check it out here:</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/Fb-RNqiDoiw" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
<p> </p>
<h2 id="heading-example-setup">Example Setup</h2>
<p>For your demo, you have two simple HTML pages – <code>from.html</code> and <code>to.html</code> – that share the same stylesheet (<code>style.css</code>). <code>from.html</code> page displays a grid of story cards. When you click a card on the first page, its image enlarges and moves to the <code>to.html</code> page.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750879534660/6fb3252f-aef2-42f9-8e64-3e106369c0a5.gif" alt="View Transition Demo" class="image--center mx-auto" width="1138" height="640" loading="lazy"></p>
<pre><code class="lang-xml"><span class="hljs-comment">&lt;!-- from.html --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"style.css"</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"stories-container"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"story-card"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">img</span>
            <span class="hljs-attr">src</span>=<span class="hljs-string">"./assets/image-3.jpg"</span>
            <span class="hljs-attr">alt</span>=<span class="hljs-string">"World in the Glass"</span>
            <span class="hljs-attr">id</span>=<span class="hljs-string">"story-image"</span>
        /&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"./to.html"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"story-overlay"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"story-content"</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"story-tag"</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"story-tag"</span>&gt;</span>
                        Sci-Fi
                    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">h2</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"story-title"</span>&gt;</span>World in the Glass<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"story-description"</span>&gt;</span>
                        A cyberpunk adventure in a dystopian future
                        where reality and virtual worlds collide.
                    <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- more story cards… --&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<pre><code class="lang-xml"><span class="hljs-comment">&lt;!-- to.html --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"style.css"</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">section</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"story-hero"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">img</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"hero-image"</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"./assets/image-3.jpg"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"story-hero-overlay"</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"overlay"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"breadcrumb"</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"breadcrumb"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"from.html"</span>&gt;</span>My Stories<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"breadcrumb-separator"</span>&gt;</span>/<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"#"</span>&gt;</span>World in the Glass<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"story-tag"</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"story-tag"</span>&gt;</span>Sci-Fi<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"story-title"</span>&gt;</span>World in the Glass<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"story-meta"</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"story-meta"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"story-meta-item"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>Created: June 1, 2025<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
            <span class="hljs-comment">&lt;!-- additional markup… --&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">section</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">section</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"story-content-wrapper"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"story-content"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"story-main"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"story-chapter"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">h2</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"chapter-title"</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"title"</span>&gt;</span>
                    Chapter 1: The Discovery
                <span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
                <span class="hljs-comment">&lt;!-- additional markup… --&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">section</span>&gt;</span>
</code></pre>
<p>Instead of showing the full HTML markup here, I’ve only included the key snippet to help you understand the idea. You’ll find the complete code in the GitHub repository at the end of the article.</p>
<p>You’ll see that most of our work happens in <code>style.css</code>, because although the View Transition API is a JavaScript API, you control it heavily with CSS.</p>
<h2 id="heading-enabling-cross-document-transitions">Enabling Cross-Document Transitions</h2>
<p>To turn on cross-document transitions, add just one line to your CSS:</p>
<pre><code class="lang-css"><span class="hljs-keyword">@view-transition</span> {
    <span class="hljs-selector-tag">navigation</span>: <span class="hljs-selector-tag">auto</span>;
}
</code></pre>
<p>Now, when you navigate between two pages – even using the browser’s “Back” and “Forward” buttons – you’ll see a smooth cross-fade by default.</p>
<h3 id="heading-debug-transition">Debug Transition</h3>
<p>If the animation feels too fast, you can use the Developer Tools in Google Chrome browser to slow it down. This not only helps you follow the animation more clearly, but also gives you a chance to learn how to debug animations using Chrome’s DevTools. Just follow the steps below:</p>
<ul>
<li><p>Open DevTools in your Chrome Browser</p>
</li>
<li><p>Click the three dot icon on top right corner (you can follow the diagram below)</p>
</li>
<li><p>Click “More Tools” → Animations</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750877041736/c8aa8b70-eb85-490a-9668-fc8791de7194.jpeg" alt="Debugging Animation with Chrome DevTools" class="image--center mx-auto" width="2334" height="1440" loading="lazy"></p>
<ul>
<li>Then slow the animation speed (for example to 10%) so you can watch it in detail.</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750877234236/9fe44c73-ec28-4216-a40f-d13fe3bd5854.jpeg" alt="Animation Speed Control with DevTools" class="image--center mx-auto" width="2264" height="1440" loading="lazy"></p>
<h3 id="heading-first-view-transition">First View Transition</h3>
<p>By default, the entire document cross-fades, which is quite boring. To animate a specific element – like the “page title” – give it a <code>view-transition-name</code>:</p>
<pre><code class="lang-css"><span class="hljs-selector-id">#title</span> {
    <span class="hljs-attribute">view-transition-name</span>: title;
}
</code></pre>
<p>Both pages use the same <code>id="title"</code>, so the API knows to treat them as one element. Now, when you click a card, the title gracefully moves from its position on the first page to its spot on the detail page – forward and backward. Just with these three lines of code, you get a pretty decent morph transition! Isn’t it interesting?</p>
<h3 id="heading-understanding-view-transition-internals">Understanding View Transition Internals</h3>
<p>To see how the API works under the hood:</p>
<ol>
<li><p>Open DevTools and pause the animation.</p>
</li>
<li><p>Navigate between pages. You’ll notice a new overlay in the Elements panel. This overlay is made up with the CSS pseudo-element <code>::view-transition</code></p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750878635416/e58425ae-99a9-4a83-b16b-52b8fdf4f50f.jpeg" alt="Discovering ::view-transition" class="image--center mx-auto" width="2992" height="1440" loading="lazy"></p>
</li>
<li><p>Inside, you’ll find two Pseudo-element groups:</p>
<ul>
<li><p><code>::view-transition-group-root</code> (the default cross-fade)</p>
</li>
<li><p><code>::view-transition-group-title</code> (for the named element <code>title</code>)</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750878743737/c84cd13d-0110-4f72-b635-6a31c08b2a11.jpeg" alt="View Transition Pseudo-element groups" class="image--center mx-auto" width="1958" height="1440" loading="lazy"></p>
</li>
</ul>
</li>
</ol>
<p>You can target these groups in CSS. For example, to control all transitions’ duration:</p>
<pre><code class="lang-css"><span class="hljs-selector-pseudo">::view-transition-group(</span>*) {
    <span class="hljs-attribute">animation-duration</span>: <span class="hljs-number">0.5s</span>;
}
</code></pre>
<p>Or to disable the <code>root</code> default cross-fade while keeping your <code>title</code> animation:</p>
<pre><code class="lang-css"><span class="hljs-selector-pseudo">::view-transition-group(root)</span> {
    <span class="hljs-attribute">animation</span>: none;
}
</code></pre>
<h3 id="heading-animating-images-across-pages">Animating Images Across Pages</h3>
<p>Let’s animate the story image from the gallery into the larger hero image on the detail page. Here, the IDs differ – <code>#story-image</code> on <code>from.html</code> and <code>#hero-image</code> on <code>to.html</code> – so you select both and name the transition <code>picture</code>:</p>
<pre><code class="lang-css"><span class="hljs-selector-id">#story-image</span>,
<span class="hljs-selector-id">#hero-image</span> {
    <span class="hljs-attribute">view-transition-name</span>: picture;
}
</code></pre>
<h4 id="heading-default-animation">Default Animation</h4>
<p>By default, you’ll see two cross-fading snapshots (“old” and “new”). But this animation isn't perfect for us. To understand this you'll go a bit deeper. Open DevTools again and pause the animation. Then, click on the story card in the <code>from.html</code> page. Now, you can scrub the playhead back and forth in the Animations panel to understand the problem and fine-tune the overlap.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750933857022/7edf89cf-9cb3-4692-9f86-d1124700827d.gif" alt="Finding the default animation overlap problem" class="image--center mx-auto" width="1024" height="640" loading="lazy"></p>
<h4 id="heading-digging-into-the-problem-to-understand-it-better">Digging Into the Problem to Understand It Better</h4>
<p>Just by looking at it, you can already see the problem. While the animation is playing, the state of the <code>from.html</code> page (you can think of this state as the snapshot of the old state) overlaps with the incoming state or snapshot of the <code>to.html</code> page. They blend into each other in a way that doesn’t look good visually. You can check the snapshots of the old and new state of the transitions in the elements panel in the DevTools.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750938129914/eb9cb31f-2aaf-49e8-b46d-9b8edeb43cf5.jpeg" alt="Overlapping issue identified" class="image--center mx-auto" width="2394" height="1440" loading="lazy"></p>
<p>There, you’ll notice a new pseudo-element group <code>::view-transition-group(picture)</code>. If you expand it, another group appears: <code>::view-transition-image-pair(picture)</code>.</p>
<p>Inside that, you’ll find two more pseudo-elements: <code>::view-transition-old(picture)</code> and <code>::view-transition-new(picture)</code>. The naming is pretty self-explanatory. The “image pair” reflects my earlier analogy of treating the before-and-after states as snapshots – you have one for the old state and one for the new. Makes sense now?</p>
<h4 id="heading-improving-the-animation">Improving the Animation</h4>
<p>Now that you understand the concept and have identified the issue, let’s adjust the CSS to improve the animation. You noticed that the new snapshot appears on top of the old one. The old snapshot covers the full height of the parent element <code>::view-transition-image-pair(picture)</code>, while the new one is smaller. They’re cross-fading over each other, which doesn’t look great.</p>
<p>To fix this, you can target both the “old” and “new” snapshots and set their <code>height</code> to 100%. Since the default cross-fade feels a bit dull, you’ll also disable the built-in animation and set their <code>mix-blend-mode</code> property to <code>normal</code> so they don’t visually overlap in an odd way. Finally, you’ll make sure both snapshots have the same <code>border-radius</code> so the transition between the two looks smooth and consistent.</p>
<pre><code class="lang-css"><span class="hljs-selector-pseudo">::view-transition-old(picture)</span>,
<span class="hljs-selector-pseudo">::view-transition-new(picture)</span> {
    <span class="hljs-attribute">animation</span>: none;
    <span class="hljs-attribute">mix-blend-mode</span>: normal;
    <span class="hljs-attribute">height</span>: <span class="hljs-number">100%</span>;
    <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">30px</span> <span class="hljs-number">30px</span>;
}
</code></pre>
<h4 id="heading-digging-deeper-to-discover-hidden-issues">Digging Deeper to Discover Hidden Issues</h4>
<p>Now, if you repeat the debugging process and take a closer look, you’ll see that the overlapping issue is resolved. But there’s still one problem: the <code>::view-transition-new(picture)</code> element on top appears distorted. You can fix this by setting its <code>object-fit</code> property to <code>cover</code> and hiding any <code>overflow</code>. This will ensure the image scales properly without stretching and stays neatly within its container.</p>
<pre><code class="lang-css"><span class="hljs-selector-id">#to</span><span class="hljs-selector-pseudo">::view-transition-new(picture)</span> {
    <span class="hljs-attribute">object-fit</span>: cover;
    <span class="hljs-attribute">overflow</span>: hidden;
}
</code></pre>
<p>Here, I’ve specifically targeted the <code>::view-transition-new(picture)</code> pseudo-element of the <code>to</code> page using the <code>#to</code> identifier – because I added unique IDs to the elements of both <code>from.html</code> and <code>to.html</code>.</p>
<pre><code class="lang-xml"><span class="hljs-comment">&lt;!-- from.html --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"from"</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- code goes here --&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>

<span class="hljs-comment">&lt;!-- to.html --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"to"</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- code goes here --&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>Now, if you check the animation closely, you’ll notice that the transition from <code>from.html</code> to <code>to.html</code> looks perfect.</p>
<p>Next, let’s handle the “back” navigation – transitioning from <code>to.html</code> back to <code>from.html</code>. If you debug this reverse transition, you’ll see that the old snapshot <code>::view-transition-new(picture)</code> appears completely distorted during the animation.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750939152019/65b715c6-0e2b-4997-a9d1-795a5daab922.jpeg" alt="Back navigation distortion issue" class="image--center mx-auto" width="2802" height="1440" loading="lazy"></p>
<p>To fix this, you can target the new snapshot on the <code>from</code> page and set its <code>object-fit</code> to <code>cover</code>.</p>
<pre><code class="lang-css"><span class="hljs-selector-id">#from</span><span class="hljs-selector-pseudo">::view-transition-new(picture)</span> {
    <span class="hljs-attribute">object-fit</span>: contain;
}
</code></pre>
<p>Now, if you debug and inspect again, the distortion is gone! But if you carefully follow the animation, you’ll notice another issue – the lower snapshot (which is <code>::view-transition-old(picture)</code> on the <code>from.html</code> page) – is overlapping awkwardly, as illustrated in the diagram below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750939706428/6f084418-c14e-4eb7-af50-aaca93d30d40.jpeg" alt="New snapshot overlapping the old one" class="image--center mx-auto" width="3170" height="1440" loading="lazy"></p>
<p>To fix this final piece, you can target the <code>::view-transition-old(picture)</code> pseudo-element on the <code>from</code> page. Then you apply <code>object-fit: cover</code>, hide any <code>overflow</code>, and match the <code>border-radius</code> to <code>20px</code> – just like the destination snapshot – for a smooth and visually consistent transition.</p>
<pre><code class="lang-css"><span class="hljs-selector-id">#from</span><span class="hljs-selector-pseudo">::view-transition-old(picture)</span> {
    <span class="hljs-attribute">object-fit</span>: cover;
    <span class="hljs-attribute">overflow</span>: hidden;
    <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">20px</span>;
}
</code></pre>
<h4 id="heading-further-fine-tuning-for-perfection">Further Fine-Tuning for Perfection</h4>
<p>After making these changes, the picture animation finally feels perfect! As you can see, the View Transition API is both simple and powerful. All it really takes is targeting the right pseudo-elements and applying the CSS skills you already have to fine-tune the transition.</p>
<p>It might feel a bit tedious at first – but that’s the nature of animation work, whether it’s in web development or video editing.</p>
<p>These small, detailed adjustments are what make your animations smoother and your user experience truly delightful. The more you debug, the more opportunities you uncover for improvement. So let’s dive a bit deeper and see if there’s anything else you can refine.</p>
<p>If you pause the animation and navigate from the <code>from.html</code> page to the <code>to.html</code> page, you’ll notice that the snapshot of the incoming page title overlaps with the old one – as shown in the diagram below.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750961028146/c1946f2d-f659-4ffb-a597-93ec176d6dce.jpeg" alt="Page title overlap issue" class="image--center mx-auto" width="3128" height="1440" loading="lazy"></p>
<p>You can solve this easily. When your titles overlap during the transition, hide the old title at the right moment:</p>
<pre><code class="lang-css"><span class="hljs-selector-pseudo">::view-transition-old(title)</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">0</span>;
}
</code></pre>
<p>Now, if you check again, you’ll see that the title no longer overlaps – and the animation is finally looking perfect!</p>
<h3 id="heading-endless-animation-opportunities"><strong>Endless Animation Opportunities</strong></h3>
<p>The View Transition API isn’t limited to just targeting pseudo-elements or relying on default animations. You can bring in all your CSS animation and transition skills to craft stunning, eye-catching custom animations. Let’s look at one more example to get a better sense of what’s possible.</p>
<h4 id="heading-finding-the-opportunity">Finding the Opportunity</h4>
<p>When you transition from the <code>from.html</code> page to the <code>to.html</code> page, the image animates smoothly. But there’s an issue: a darker overlay suddenly appears on top of the image, along with the text content inside it. Both the overlay and the text pop in abruptly, which doesn’t look great. So let’s fix that.</p>
<p>If you inspect the elements in DevTools, you’ll see I’ve intentionally given the overlay an ID of <code>#overlay</code>. All the text content on the <code>to.html</code> page lives inside this element.</p>
<p>Ideally, when you transition from <code>from.html</code> to <code>to.html</code>, the overlay should also appear with a smooth animation. Notice that the <code>from.html</code> page doesn’t have this overlay at all. Up to this point, everything you’ve done has involved transitioning between elements that exist on both pages – elements that have counterparts. But in this case, you want to transition from “nothing” to “something.” And yes, that’s also possible with the View Transition API.</p>
<h4 id="heading-implement-the-idea">Implement the Idea</h4>
<p>Without saying anything else, let’s go ahead and target the <code>#overlay</code> element first and assign it a custom transition name "overlay". This gives us the flexibility to control its animation separately from the rest of the elements.</p>
<pre><code class="lang-css"><span class="hljs-selector-id">#overlay</span> {
    <span class="hljs-attribute">view-transition-name</span>: overlay;
}
</code></pre>
<p>Now that you’ve set this up, let’s see what’s actually happening. If you pause the animation and debug it, just like before, you’ll notice a new pseudo-element <code>::view-transition-group(overlay)</code>. Inside this group, within the image pair, you’ll find only <code>::view-transition-new(overlay)</code> – there’s no <code>::view-transition-old(overlay)</code>.</p>
<p>Why is that? It’s simple: on the previous page (<code>from.html</code>), there is no element with the ID <code>overlay</code>. Since there’s nothing to take a snapshot of, the browser doesn’t create a <code>::view-transition-old(overlay)</code>.</p>
<p>Likewise, when navigating back from <code>to.html</code> to <code>from.html</code>, there will only be a <code>::view-transition-old(overlay)</code> – and no <code>::view-transition-new(overlay)</code> – because the overlay exists only on the page you’re leaving.</p>
<p>What you want to do now is animate this element in a nice way. Since you’re transitioning from “nothing” to “something”, you can define a custom CSS animation. A simple and elegant effect could be a fade-in from the bottom.</p>
<h4 id="heading-defining-custom-keyframes">Defining Custom Keyframes</h4>
<p>To achieve that, you can define a custom keyframe animation called <code>fade-in</code>. In this animation, you’ll start from <code>opacity: 0</code> and position the element slightly lower – for example, <code>translateY(50px)</code> – and then animate it upwards as it fades in.</p>
<pre><code class="lang-css"><span class="hljs-keyword">@keyframes</span> fade-in {
    <span class="hljs-selector-tag">from</span> {
        <span class="hljs-attribute">opacity</span>: <span class="hljs-number">0</span>;
        <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">translateY</span>(<span class="hljs-number">50px</span>);
    }
}
</code></pre>
<p>For the reverse (fading out) you can simply transition the <code>opacity</code> back to <code>0</code>.</p>
<pre><code class="lang-css"><span class="hljs-keyword">@keyframes</span> fade-out {
    <span class="hljs-selector-tag">to</span> {
        <span class="hljs-attribute">opacity</span>: <span class="hljs-number">0</span>;
    }
}
</code></pre>
<h4 id="heading-using-the-keyframe-animations">Using the Keyframe Animations</h4>
<p>Now that you’ve defined our keyframe animations, you can target the <code>::view-transition-new(overlay)</code> element and apply the <code>fade-in</code> animation to it. You’ll also add a slight animation delay – let’s say <code>0.5</code> seconds. This delay ensures that our custom animation begins after the default cross-fade animation has completed. Since you previously set a <code>0.5</code> second delay for the transition, this timing helps everything flow smoothly, without overlapping animations.</p>
<pre><code class="lang-css"><span class="hljs-selector-pseudo">::view-transition-new(overlay)</span> {
    <span class="hljs-attribute">animation</span>: <span class="hljs-number">250ms</span> <span class="hljs-built_in">cubic-bezier</span>(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0.3</span>, <span class="hljs-number">1</span>) both fade-in;
    <span class="hljs-attribute">animation-delay</span>: <span class="hljs-number">0.5s</span>;
}
</code></pre>
<p>And in the case of the “old” state (meaning when you navigate back), you simply target the <code>::view-transition-old(overlay)</code> element and apply the <code>fade-out</code> animation to it.</p>
<pre><code class="lang-css"><span class="hljs-selector-pseudo">::view-transition-old(overlay)</span> {
    <span class="hljs-attribute">animation</span>: <span class="hljs-number">50ms</span> <span class="hljs-built_in">cubic-bezier</span>(<span class="hljs-number">0.3</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">1</span>) both fade-out;
}
</code></pre>
<h4 id="heading-fine-tuning-for-perfection">Fine-Tuning for Perfection</h4>
<p>Now let’s pause for a moment and check if any fine-tuning is needed. This step is essential when working with View Transitions, which is why I keep emphasizing it.</p>
<p>If you look closely during the fade-in and fade-out animations, you’ll notice an overflow issue: a subtle black area briefly appears underneath the overlay.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750972763490/73f112e5-3fc1-4115-bbe1-0f23eee66577.jpeg" alt="Black overlay underneath issue" class="image--center mx-auto" width="3178" height="1440" loading="lazy"></p>
<p>To fix this, you can simply select the entire <code>::view-transition-group(overlay)</code> and hide its <code>overflow</code>. That should take care of the issue immediately.</p>
<pre><code class="lang-css"><span class="hljs-selector-pseudo">::view-transition-group(overlay)</span> {
    <span class="hljs-attribute">overflow</span>: hidden;
}
</code></pre>
<p>Now, if you check again, you’ll see that the animation looks perfect!</p>
<h2 id="heading-single-page-experience">Single-Page Experience</h2>
<p>Up until now, you’ve explored how the View Transition API works in the context of cross-document or multi-page applications – something that wasn’t natively possible before.</p>
<p>But now, let’s shift our focus to Single Page Applications (SPAs). In most SPAs, animations have always been part of the experience, even before the View Transition API was introduced. Developers have long used various JavaScript tricks to create smooth transitions within SPAs. But with the View Transition API, you can now implement these transitions natively and much more easily. Let’s quickly take a look at how that works.</p>
<p>Let’s talk about the interaction we’re focusing on. When you click the “New Story” button, a new story card should appear. Then, we are going to animate this interaction using the View Transition API.</p>
<p>But first, let me quickly show you how this works under the hood. It’s a simple DOM (Document Object Model) operation. I’ve specifically targeted the button and added an event listener to its <code>onclick</code> event. So what does that listener do? It creates a new card element and injects it directly into the DOM. Let’s break down how this code creates a new story card using JavaScript.</p>
<h3 id="heading-set-up-the-event-listener">Set Up the Event Listener</h3>
<p>You can set up the Event Listener in five simple steps:</p>
<h4 id="heading-step-1-select-the-button">Step 1: Select the Button</h4>
<p>You’ll begin by selecting the button that the user will click to create a new story card. This line uses <code>document.querySelector()</code> to grab the first element on the page with the class name <code>.new-story-btn</code> and stores it in the <code>newStoryButton</code> variable.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> newStoryButton = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">".new-story-btn"</span>);
</code></pre>
<h4 id="heading-step-2-set-up-the-click-event-listener">Step 2: Set up the Click Event Listener</h4>
<p>Next, add a click event listener to that button. This means that when the user clicks the “New Story” button, the function you define inside this event listener will run. The function is marked <code>async</code> in case you later want to use <code>await</code> inside it – for example, if you fetch data or run animations that need to wait.</p>
<pre><code class="lang-javascript">newStoryButton.addEventListener(<span class="hljs-string">"click"</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-comment">// you will write the listener code here</span>
});
</code></pre>
<h4 id="heading-step-3-select-the-container-for-story-cards">Step 3: Select the Container for Story Cards</h4>
<p>Now that the button has been clicked, you grab the container where our story cards are displayed. This is the element with the class <code>.stories-container</code>, and it’s where you’ll append the new card in the next steps.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// select the container for story cards</span>
<span class="hljs-keyword">const</span> container = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">".stories-container"</span>);
</code></pre>
<h4 id="heading-step-4-create-a-new-story-card">Step 4: Create a new Story Card</h4>
<p>You’ll call a helper function named <code>addStoryCard()</code> – presumably a custom function that returns a ready-made DOM element representing a story card. We pass it the story details: tag, title, description, and an image path. This function likely handles the creation of the HTML structure, styling, and maybe even animations for the card.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> newCard = addStoryCard({
    <span class="hljs-attr">tag</span>: <span class="hljs-string">"Fantasy"</span>,
    <span class="hljs-attr">title</span>: <span class="hljs-string">"Sky Kingdoms"</span>,
    <span class="hljs-attr">description</span>:
        <span class="hljs-string">"A tale of floating islands and the heroes who defend them."</span>,
    <span class="hljs-attr">image</span>: <span class="hljs-string">"./assets/image-5.jpg"</span>,
});
</code></pre>
<h4 id="heading-step-5-add-the-card-to-the-page">Step 5: Add the Card to the Page</h4>
<p>Finally, the newly created card is appended to the <code>.stories-container</code>, making it visible on the page. At this point, the user will see the “Sky Kingdoms” story card appear in the list of stories.</p>
<pre><code class="lang-javascript">container.appendChild(newCard);
</code></pre>
<p>That’s it. Here’s the full event listener function:</p>
<pre><code class="lang-javascript">newStoryButton.addEventListener(<span class="hljs-string">"click"</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> container = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">".stories-container"</span>);

    <span class="hljs-keyword">const</span> newCard = addStoryCard({
        <span class="hljs-attr">tag</span>: <span class="hljs-string">"Fantasy"</span>,
        <span class="hljs-attr">title</span>: <span class="hljs-string">"Sky Kingdoms"</span>,
        <span class="hljs-attr">description</span>:
            <span class="hljs-string">"A tale of floating islands and the heroes who defend them."</span>,
        <span class="hljs-attr">image</span>: <span class="hljs-string">"./assets/image-5.jpg"</span>,
    });

    container.appendChild(newCard);
});
</code></pre>
<h3 id="heading-the-actual-addstorycard-function">The Actual <code>addStoryCard()</code> Function</h3>
<p>Let’s take a closer look at the helper function <code>addStoryCard()</code>, which is responsible for generating a brand-new story card using some predefined structure and inserting custom content into it.</p>
<h4 id="heading-step-1-find-the-template-card">Step 1: Find the Template Card</h4>
<p>You begin by selecting the existing <code>.story-card</code> element from the DOM. This element acts as your template – a ready-made design that you can clone to create new cards. You also add a simple safety check: if for some reason the template doesn’t exist on the page, the function exits immediately by returning undefined.</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">addStoryCard</span>(<span class="hljs-params">data</span>) </span>{
    <span class="hljs-keyword">const</span> templateCard = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">".story-card"</span>);
    <span class="hljs-keyword">if</span> (!templateCard) <span class="hljs-keyword">return</span>;
}
</code></pre>
<h4 id="heading-step-2-clone-the-template">Step 2: Clone the Template</h4>
<p>Once you have the template, you’ll create a deep clone of it using <code>cloneNode(true)</code>. This means it copies the element and all of its nested child elements – preserving the full structure of the card. At this point, you have a fresh new card element in memory that looks just like the original.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> newCard = templateCard.cloneNode(<span class="hljs-literal">true</span>);
</code></pre>
<h4 id="heading-step-3-update-the-image-if-any">Step 3: Update the Image (if any)</h4>
<p>If an image is provided in the data object, you find the <code>img</code> tag inside the new card and update its <code>img</code> attribute.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">if</span> (data.image) {
    <span class="hljs-keyword">const</span> currentImage = newCard.querySelector(<span class="hljs-string">"img"</span>);
    currentImage.setAttribute(<span class="hljs-string">"img"</span>, <span class="hljs-string">`url('<span class="hljs-subst">${data.image}</span>')`</span>);
}
</code></pre>
<h4 id="heading-step-4-update-the-text-content">Step 4: Update the Text Content</h4>
<p>Now you customize the card’s text:</p>
<ul>
<li><p>You look for the <code>.story-tag</code>, <code>.story-title</code>, and <code>.story-description</code> elements inside the card.</p>
</li>
<li><p>If they exist, you set their text content based on the data object that was passed in. This is where the story gets its actual content – like the tag (“Fantasy”), title (“Sky Kingdoms”), and description.</p>
</li>
</ul>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> tag = newCard.querySelector(<span class="hljs-string">".story-tag"</span>);
<span class="hljs-keyword">const</span> title = newCard.querySelector(<span class="hljs-string">".story-title"</span>);
<span class="hljs-keyword">const</span> desc = newCard.querySelector(<span class="hljs-string">".story-description"</span>);
<span class="hljs-keyword">if</span> (tag) tag.textContent = data.tag;
<span class="hljs-keyword">if</span> (title) title.textContent = data.title;
<span class="hljs-keyword">if</span> (desc) desc.textContent = data.description;
</code></pre>
<h4 id="heading-step-5-return-the-final-card">Step 5: Return the Final Card</h4>
<p>Finally, you return the fully prepared story card so it can be added to the page wherever needed.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">return</span> newCard;
</code></pre>
<p>So here’s the full <code>addStoryCard</code> function:</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">addStoryCard</span>(<span class="hljs-params">data</span>) </span>{
    <span class="hljs-keyword">const</span> templateCard = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">".story-card"</span>);
    <span class="hljs-keyword">if</span> (!templateCard) <span class="hljs-keyword">return</span>;

    <span class="hljs-comment">// Clone the card</span>
    <span class="hljs-keyword">const</span> newCard = templateCard.cloneNode(<span class="hljs-literal">true</span>);

    <span class="hljs-comment">// Update image if provided</span>
    <span class="hljs-keyword">if</span> (data.image) {
        <span class="hljs-keyword">const</span> currentImage = newCard.querySelector(<span class="hljs-string">"img"</span>);
        currentImage.setAttribute(<span class="hljs-string">"img"</span>, <span class="hljs-string">`url('<span class="hljs-subst">${data.image}</span>')`</span>);
    }

    <span class="hljs-comment">// Update content</span>
    <span class="hljs-keyword">const</span> tag = newCard.querySelector(<span class="hljs-string">".story-tag"</span>);
    <span class="hljs-keyword">const</span> title = newCard.querySelector(<span class="hljs-string">".story-title"</span>);
    <span class="hljs-keyword">const</span> desc = newCard.querySelector(<span class="hljs-string">".story-description"</span>);
    <span class="hljs-keyword">if</span> (tag) tag.textContent = data.tag;
    <span class="hljs-keyword">if</span> (title) title.textContent = data.title;
    <span class="hljs-keyword">if</span> (desc) desc.textContent = data.description;

    <span class="hljs-keyword">return</span> newCard;
}
</code></pre>
<p>Now, if you click on the “New Story” button, a new story card appears dynamically – thanks to the simple DOM operations you’ve already written above. But you can make it more engaging. Instead of the card just popping into place, you want to add a smooth, eye-catching transition when it’s added to the container.</p>
<p>Can you do this with plain CSS? Unfortunately, no – because the card is being added dynamically via JavaScript, CSS alone won’t catch this change and animate it. That’s where the View Transition API in JavaScript comes in. With just a bit of extra code, you can bring this interaction to life with a smooth and polished transition effect.</p>
<h3 id="heading-applying-the-animation">Applying the Animation</h3>
<p>In your event listener function, after creating the card DOM node, you just appended it to the container using the below code:</p>
<pre><code class="lang-javascript">container.appendChild(newCard);
</code></pre>
<p>This line – <code>container.appendChild(newCard)</code> – is the core operation you want to animate.</p>
<p>So how do you make this transition happen smoothly? You can’t use CSS alone here, because the new element is being inserted dynamically using JavaScript. But that’s not a problem, as JavaScript gives you full control over DOM manipulation, including the ability to apply styles on the fly.</p>
<p>To enable the View Transition API for your <code>newCard</code>, you simply need to assign a <code>viewTransitionName</code> to it. You can do this by setting the <code>style.viewTransitionName</code> property on the <code>newCard</code> element. You’ll give the transition a name <code>targeted-card</code>, just like you did in the CSS-based example earlier.</p>
<pre><code class="lang-javascript">newCard.style.viewTransitionName = <span class="hljs-string">"targeted-card"</span>;
</code></pre>
<p>This tells the browser: “Track this element during the transition and animate it.” And with that single line, your dynamically added element becomes part of a smooth, native-feeling UI animation.</p>
<p>And now you can start the transition using the View Transition JavaScript API like below:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> transition = <span class="hljs-built_in">document</span>.startViewTransition(<span class="hljs-keyword">async</span> () =&gt; {
    container.appendChild(newCard);
});
</code></pre>
<p>Here, you use the <code>startViewTransition()</code> method provided by the browser. This is a modern API that helps you animate changes between two visual states of the page – before and after the DOM updates. Inside <code>startViewTransition()</code>, you pass an asynchronous callback function, in this case:</p>
<pre><code class="lang-javascript">() =&gt; {
    container.appendChild(newCard);
}
</code></pre>
<p>This is the DOM change you want to animate: adding the <code>newCard</code> into the <code>.stories-container</code>. Normally, adding a new DOM element would just appear instantly on the page. But with this API, you’re telling the browser:</p>
<blockquote>
<p>Hey, I’m about to change the DOM. Please capture the visual state before the change, apply my DOM update, then animate the transition between the old and new state.</p>
</blockquote>
<p>Now you need to pause here and wait until the animation is fully complete as this is an asynchronous task. You can do this like below:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">await</span> transition.finished;
</code></pre>
<p>Now that the animation is finished, you remove that name by setting it to <code>null</code>. This step is important to avoid unintended animations if the card is later updated or moved again. Think of it as cleaning up after the animation is done.</p>
<pre><code class="lang-javascript">newCard.style.viewTransitionName = <span class="hljs-literal">null</span>;
</code></pre>
<p>So here’s the full code all in one go, combining everything we just discussed.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// name the transition</span>
newCard.style.viewTransitionName = <span class="hljs-string">"targeted-card"</span>;

<span class="hljs-comment">// start the transition</span>
<span class="hljs-keyword">const</span> transition = <span class="hljs-built_in">document</span>.startViewTransition(<span class="hljs-keyword">async</span> () =&gt; {
    container.appendChild(newCard);
});

<span class="hljs-comment">// wait for the transition to finish</span>
<span class="hljs-keyword">await</span> transition.finished;

<span class="hljs-comment">// finally cleanup the transition when finished</span>
newCard.style.viewTransitionName = <span class="hljs-literal">null</span>;
</code></pre>
<p>Now, if you reload the page and try it out, you’ll see a smooth, beautiful transition when a new card is created. It’s a subtle touch, but it makes the interaction feel much more polished and dynamic.</p>
<p>And that’s how you can harness the power of JavaScript to add any animation you want – just like you did with CSS, but by setting style properties dynamically. The possibilities are endless when you combine your CSS skills with the flexibility of JavaScript and the View Transition API.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751052383696/5f166113-6ca7-4f4c-b5dd-8fde64972523.gif" alt="View Transition in SPA" class="image--center mx-auto" width="1138" height="640" loading="lazy"></p>
<h2 id="heading-view-transition-in-reactjs">View Transition in React.js</h2>
<p>If you are a React Developer, you can play with the experimental <code>&lt;ViewTransition&gt;</code> React component to play with this API.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> {unstable_ViewTransition <span class="hljs-keyword">as</span> ViewTransition} <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">ViewTransition</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>...<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">ViewTransition</span>&gt;</span></span>
</code></pre>
<p>Please note that this API is experimental and is not available in a stable version of React yet. You can try it by upgrading React packages to the most recent experimental version.</p>
<ul>
<li><p>react@experimental</p>
</li>
<li><p>react-dom@experimental</p>
</li>
<li><p>eslint-plugin-react-hooks@experimental</p>
</li>
</ul>
<p>You can check details from the <a target="_blank" href="https://react.dev/reference/react/ViewTransition">React.js official Documentation</a>.</p>
<h2 id="heading-view-transition-in-nextjs">View Transition in Next.js</h2>
<p>If you are a Next.js Developer, just like vanilla React, you can try the View Transition API</p>
<p>To enable this feature, you need to set the <code>viewTransition</code> property to true in your <code>next.config.js</code> file.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">/** <span class="hljs-doctag">@type <span class="hljs-type">{import('next').NextConfig}</span> </span>*/</span>
<span class="hljs-keyword">const</span> nextConfig = {
  <span class="hljs-attr">experimental</span>: {
    <span class="hljs-attr">viewTransition</span>: <span class="hljs-literal">true</span>,
  },
}

<span class="hljs-built_in">module</span>.exports = nextConfig
</code></pre>
<p>Please note that <code>viewTransition</code> is an experimental flag that enables the new experimental View Transitions API in React. Please check details from the <a target="_blank" href="https://nextjs.org/docs/app/api-reference/config/next-config-js/viewTransition">Next.js official Documentation.</a></p>
<h2 id="heading-browser-support">Browser Support</h2>
<p>Browser Support varies (Firefox doesn’t yet support it), so be sure to review the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API#browser_compatibility">compatibility table</a> before shipping to production.</p>
<h2 id="heading-wrap-up">Wrap-Up</h2>
<p>The View Transition API lets you:</p>
<ul>
<li><p>Enable cross-document transitions with one line of CSS</p>
</li>
<li><p>Animate individual elements by naming them</p>
</li>
<li><p>Debug transitions in DevTools and fine-tune timing and easing</p>
</li>
<li><p>Apply the same approach to single-page apps using JavaScript</p>
</li>
</ul>
<p>For more details, check out the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API">MDN documentation</a> on View Transitions. Enjoy creating seamless, native animations in your web projects!</p>
<p>You can find all the source code from this guide in <a target="_blank" href="https://github.com/logicbaselabs/view-transition-api-tutorial">this GitHub repository</a>. If it helped you in any way, consider giving it a star to show your support!</p>
<p>Also, if you found the guide valuable, feel free to share it with others who might benefit from it. I’d really appreciate your thoughts – mention me on X <a target="_blank" href="https://x.com/sumit_analyzen">@sumit_analyzen</a>, watch my <a target="_blank" href="https://youtube.com/@logicBaseLabs">coding tutorials</a>, or simply <a target="_blank" href="https://www.linkedin.com/in/sumitanalyzen/">connect with me on LinkedIn</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
