{"id":10088,"date":"2026-06-19T10:59:23","date_gmt":"2026-06-19T15:59:23","guid":{"rendered":"https:\/\/master.dev\/blog\/?p=10088"},"modified":"2026-06-19T10:59:24","modified_gmt":"2026-06-19T15:59:24","slug":"in-n-out-animations-view-transitions-part-3-3","status":"publish","type":"post","link":"https:\/\/master.dev\/blog\/in-n-out-animations-view-transitions-part-3-3\/","title":{"rendered":"In-N-Out Animations: View Transitions (Part 3\/3)"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">The point of this series is animating elements in and out of view, particularly on Hard Mode\u2122\ufe0f. That is, when those elements transition from <code>display: none;<\/code> to <code>display: block;<\/code> (or another display time, or visibility changes, etc) that was traditionally territory where animating was <em>hard.<\/em><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Just as hard is animating an element when the element is entering the DOM for the very first time, and even harder, when it&#8217;s <em>leaving<\/em> the DOM. But now we&#8217;ve got View Transitions in our toolbox and it&#8217;s going to help us do just this.<\/p>\n\n\n<div class=\"box article-series\">\n  <header>\n    <h3 class=\"article-series-header\">Article Series<\/h3>\n  <\/header>\n  <div class=\"box-content\">\n            <ol>\n                      <li>\n              <a href=\"https:\/\/master.dev\/blog\/in-n-out-animations-dialogs-part-1-3\/\">In-N-Out Animations: Dialogs (Part 1\/3)<\/a>\n            <\/li>\n                      <li>\n              <a href=\"https:\/\/master.dev\/blog\/in-n-out-animations-popovers-part-2-3\/\">In-N-Out Animations: Popovers (Part 2\/3)<\/a>\n            <\/li>\n                      <li>\n              <a href=\"https:\/\/master.dev\/blog\/in-n-out-animations-view-transitions-part-3-3\/\">In-N-Out Animations: View Transitions (Part 3\/3)<\/a>\n            <\/li>\n                  <\/ol>\n        <\/div>\n<\/div>\n\n\n\n<h2 class=\"wp-block-heading\">View Transitions<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">We&#8217;re going to look at &#8220;same page&#8221; view transitions which is most relevant here. But there are also &#8220;multi page&#8221; view transitions which are arguably more broadly amazing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The core API we&#8217;re dealing with for same-page view transitions is like this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">addButton.addEventListener(<span class=\"hljs-string\">\"click\"<\/span>, () =&gt; {\n  <span class=\"hljs-built_in\">document<\/span>.startViewTransition(<span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n    addItemToDOM();\n  });\n});\n\nremoveButton.addEventListener(<span class=\"hljs-string\">\"click\"<\/span>, () =&gt; {\n  <span class=\"hljs-built_in\">document<\/span>.startViewTransition(<span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n    removeItemToDOM();\n  });\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">But we probably wanna add a little resiliency there for browsers who may not support them, and give ourselves a little cleanup function if we need it too:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">addButton.addEventListener(<span class=\"hljs-string\">\"click\"<\/span>, () =&gt; {\n  <span class=\"hljs-keyword\">if<\/span> (!<span class=\"hljs-built_in\">document<\/span>.startViewTransition) {\n    addItemToDOM();\n    cleanupIncomingItems();\n    <span class=\"hljs-keyword\">return<\/span>;\n  }\n\n  <span class=\"hljs-keyword\">const<\/span> transition = <span class=\"hljs-built_in\">document<\/span>.startViewTransition(<span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n    addItemToDOM();\n  });\n  transition.finished.then(cleanupIncomingItems);\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Notice that not all these functions are defined yet. Here&#8217;s a pretty bare-bones example:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_019ed79c-71fa-718c-8f42-d24872c3dfc2\" src=\"\/\/codepen.io\/editor\/anon\/embed\/019ed79c-71fa-718c-8f42-d24872c3dfc2?height=450&amp;theme-id=1&amp;slug-hash=019ed79c-71fa-718c-8f42-d24872c3dfc2&amp;default-tab=js,result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed 019ed79c-71fa-718c-8f42-d24872c3dfc2\" title=\"CodePen Embed 019ed79c-71fa-718c-8f42-d24872c3dfc2\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p class=\"learn-more wp-block-paragraph\">These are just empty <code>&lt;div><\/code>s here, but the point is: <strong>they could be anything<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">See that &#8220;fade in\/out&#8221; effect? We wrote zero code to get that. That&#8217;s just the natural automatic thing that happens with a View Transition. Even, like in our case, when these elements are literally entering <em>and leaving<\/em> the DOM entirely. Little \ud83e\udd2fy to me.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So with just this, we already have &#8220;in&#8221; and &#8220;out&#8221; animations, the namesake of this blog article series. But let&#8217;s <em>control<\/em> them as well. <\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The &#8220;In&#8221;<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If we want to specifically animate the incoming items (the point of this series, ha), then we&#8217;ve got <em>options!<\/em><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">One way is to add <code>@keyframes<\/code> animation specifically to any &#8220;new&#8221; elements, that is, <code>::view-transition-new(*)<\/code>. <\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-code-table\"><mark class='shcb-loc'><span><span class=\"hljs-selector-pseudo\">::view-transition-new(<\/span>*) {\n<\/span><\/mark><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">animation-duration<\/span>: <span class=\"hljs-number\">320ms<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">animation-timing-function<\/span>: <span class=\"hljs-built_in\">cubic-bezier<\/span>(<span class=\"hljs-number\">0.2<\/span>, <span class=\"hljs-number\">0.8<\/span>, <span class=\"hljs-number\">0.2<\/span>, <span class=\"hljs-number\">1<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">animation-name<\/span>: incoming-slide-fade;\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-keyword\">@keyframes<\/span> incoming-slide-fade {\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-selector-tag\">from<\/span> {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attribute\">opacity<\/span>: <span class=\"hljs-number\">0<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attribute\">transform<\/span>: <span class=\"hljs-built_in\">translateY<\/span>(<span class=\"hljs-number\">64px<\/span>) <span class=\"hljs-built_in\">scale<\/span>(<span class=\"hljs-number\">0.9<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-selector-tag\">to<\/span> {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attribute\">opacity<\/span>: <span class=\"hljs-number\">1<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attribute\">transform<\/span>: <span class=\"hljs-built_in\">translateX<\/span>(<span class=\"hljs-number\">0<\/span>) <span class=\"hljs-built_in\">scale<\/span>(<span class=\"hljs-number\">1<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_019ee02c-9e83-732d-8a79-3fe181f4ff45\" src=\"\/\/codepen.io\/editor\/anon\/embed\/019ee02c-9e83-732d-8a79-3fe181f4ff45?height=450&amp;theme-id=1&amp;slug-hash=019ee02c-9e83-732d-8a79-3fe181f4ff45&amp;default-tab=js,result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed 019ee02c-9e83-732d-8a79-3fe181f4ff45\" title=\"CodePen Embed 019ee02c-9e83-732d-8a79-3fe181f4ff45\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">See how that <code>@keyframe<\/code> alone defines the &#8220;on the way in&#8221; styles and the &#8220;open&#8221; styles, as it were.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">But we could be a bit more specific with the <code>view-transition-name<\/code> naming, which allows us just to be a bit more targeted and then ultimately apply an &#8220;out&#8221; translation later. If we use a class name to apply the <code>view-transition-name<\/code>, it just makes it a little easier to query and remove the class after the animation is over. <\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-code-table\"><span class='shcb-loc'><span><span class=\"hljs-selector-class\">.incoming<\/span> {\n<\/span><\/span><mark class='shcb-loc'><span>  <span class=\"hljs-attribute\">view-transition-name<\/span>: incoming;\n<\/span><\/mark><span class='shcb-loc'><span>}\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><mark class='shcb-loc'><span><span class=\"hljs-selector-pseudo\">::view-transition-new(incoming)<\/span> {\n<\/span><\/mark><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">animation-duration<\/span>: <span class=\"hljs-number\">320ms<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">animation-timing-function<\/span>: <span class=\"hljs-built_in\">cubic-bezier<\/span>(<span class=\"hljs-number\">0.2<\/span>, <span class=\"hljs-number\">0.8<\/span>, <span class=\"hljs-number\">0.2<\/span>, <span class=\"hljs-number\">1<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">animation-name<\/span>: incoming-slide-fade;\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_019ee029-dfe3-71bc-94f5-eee9561fa3b5\" src=\"\/\/codepen.io\/editor\/anon\/embed\/019ee029-dfe3-71bc-94f5-eee9561fa3b5?height=450&amp;theme-id=1&amp;slug-hash=019ee029-dfe3-71bc-94f5-eee9561fa3b5&amp;default-tab=js,result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed 019ee029-dfe3-71bc-94f5-eee9561fa3b5\" title=\"CodePen Embed 019ee029-dfe3-71bc-94f5-eee9561fa3b5\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">See how I&#8217;m using the <code>transition.finished<\/code> event to fire a callback function and remove the class (and thus <code>view-transition-name<\/code>). <\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The &#8220;Out&#8221;<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The trick with &#8220;on the way out&#8221; styles is to apply a <code>view-transition-name<\/code> to the outgoing item <em>before<\/em> we call <code>startViewTransition<\/code>. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In our basic setup, we just snag the one of the <code>&lt;div><\/code>s and call <code>.remove()<\/code> on it. But because we specifically apply <code>view-transition-name: outgoing;<\/code> to it, we can latch onto that in CSS with <code>::view-transition-old(outgoing)<\/code> and have a unique &#8220;on the way out&#8221; <code>@keyframes<\/code> to apply to it. <\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_019ee047-2494-7386-b3f3-1b3f9851743b\" src=\"\/\/codepen.io\/editor\/anon\/embed\/019ee047-2494-7386-b3f3-1b3f9851743b?height=450&amp;theme-id=1&amp;slug-hash=019ee047-2494-7386-b3f3-1b3f9851743b&amp;default-tab=js,result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed 019ee047-2494-7386-b3f3-1b3f9851743b\" title=\"CodePen Embed 019ee047-2494-7386-b3f3-1b3f9851743b\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p class=\"learn-more wp-block-paragraph\">Note that if every <em>other<\/em> element either has a unique <code>view-transition-name<\/code> or <code>view-transition-name: match-element<\/code>, than we get a cool scooting effect where the element move into their new position automatically.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is all very magical to me. These elements are literally <em>leaving the DOM<\/em> which has long been an impossible-to-animate situation, and we now have a very straightforward way to do it. Huge.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Final Demo<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Here&#8217;s a slightly more fleshed out example of a &#8220;list&#8221;. This has long been one of my favorite demonstrations of animation on the web, full stop, because of it&#8217;s connection to UX. A user adding to this list is able to clearly and obviously understand what was added <em>because of the motion.<\/em> Likewise, it&#8217;s obvious to confirm the removal of an item <em>because of the motion.<\/em><\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_019d175c-474f-7b41-a28f-77396e88ef21\" src=\"\/\/codepen.io\/editor\/anon\/embed\/019d175c-474f-7b41-a28f-77396e88ef21?height=450&amp;theme-id=1&amp;slug-hash=019d175c-474f-7b41-a28f-77396e88ef21&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed 019d175c-474f-7b41-a28f-77396e88ef21\" title=\"CodePen Embed 019d175c-474f-7b41-a28f-77396e88ef21\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">That does it for this series! I hope it can be a useful reference.<\/p>\n\n\n<div class=\"box article-series\">\n  <header>\n    <h3 class=\"article-series-header\">Article Series<\/h3>\n  <\/header>\n  <div class=\"box-content\">\n            <ol>\n                      <li>\n              <a href=\"https:\/\/master.dev\/blog\/in-n-out-animations-dialogs-part-1-3\/\">In-N-Out Animations: Dialogs (Part 1\/3)<\/a>\n            <\/li>\n                      <li>\n              <a href=\"https:\/\/master.dev\/blog\/in-n-out-animations-popovers-part-2-3\/\">In-N-Out Animations: Popovers (Part 2\/3)<\/a>\n            <\/li>\n                      <li>\n              <a href=\"https:\/\/master.dev\/blog\/in-n-out-animations-view-transitions-part-3-3\/\">In-N-Out Animations: View Transitions (Part 3\/3)<\/a>\n            <\/li>\n                  <\/ol>\n        <\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>View Transitions are of unique help in applying an animation to an element even when you are literally removing it from the DOM. <\/p>\n","protected":false},"author":1,"featured_media":10151,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_post_was_ever_published":false},"categories":[1],"tags":[100,7,3,101],"class_list":["post-10088","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-animation","tag-css","tag-javascript","tag-view-transitions"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/master.dev\/blog\/wp-content\/uploads\/2026\/06\/vt-in-out.jpg?fit=2000%2C1200&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/posts\/10088","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/comments?post=10088"}],"version-history":[{"count":18,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/posts\/10088\/revisions"}],"predecessor-version":[{"id":10158,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/posts\/10088\/revisions\/10158"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/media\/10151"}],"wp:attachment":[{"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/media?parent=10088"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/categories?post=10088"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/tags?post=10088"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}