{"id":10055,"date":"2026-06-10T10:23:36","date_gmt":"2026-06-10T15:23:36","guid":{"rendered":"https:\/\/master.dev\/blog\/?p=10055"},"modified":"2026-06-10T10:23:37","modified_gmt":"2026-06-10T15:23:37","slug":"demystifying-the-view-transition-pseudo-tree","status":"publish","type":"post","link":"https:\/\/master.dev\/blog\/demystifying-the-view-transition-pseudo-tree\/","title":{"rendered":"Demystifying the View Transition Pseudo Tree"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Every time a view transition is initiated, a pseudo-element is added to the HTML element for the duration of the view transition. This acts as a snapshot overlay across your entire page.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Each pseudo-element plays a distinct role in how the view transition animates. The browser does most of the heavy lifting, which makes it a little hard to see what\u2019s actually happening under the hood. This article breaks down each pseudo-element in the tree so you know exactly what to target when you need to customize things.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The View Transition Pseudo Tree<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">All elements with a <code>view-transition-name<\/code> CSS property will be added to the view transition pseudo tree every time a view transition is <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/View_Transition_API\/Using\">called<\/a>. By default, only the HTML tag gets a <code>view-transition-name<\/code>, the <code>root<\/code> view transition group is the same size as the HTML element and covers all interactive elements. <\/p>\n\n\n\n<p class=\"learn-more wp-block-paragraph\">This is why your website may not be interactive during a view transition!<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">::view-transition<br>\u2514\u2500 ::view-transition-group(root)<br>  \u2514\u2500 ::view-transition-image-pair(root)<br>      \u251c\u2500 ::view-transition-old(root)<br>      \u2514\u2500 ::view-transition-new(root)<\/pre>\n\n\n\n<figure class=\"wp-block-image size-full is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"645\" height=\"290\" src=\"https:\/\/i0.wp.com\/master.dev\/blog\/wp-content\/uploads\/2026\/06\/Screenshot-2026-06-09-at-6.41.24-PM-1.png?resize=645%2C290&#038;ssl=1\" alt=\"\" class=\"wp-image-10071\" style=\"width:459px;height:auto\" srcset=\"https:\/\/i0.wp.com\/master.dev\/blog\/wp-content\/uploads\/2026\/06\/Screenshot-2026-06-09-at-6.41.24-PM-1.png?w=645&amp;ssl=1 645w, https:\/\/i0.wp.com\/master.dev\/blog\/wp-content\/uploads\/2026\/06\/Screenshot-2026-06-09-at-6.41.24-PM-1.png?resize=300%2C135&amp;ssl=1 300w\" sizes=\"auto, (max-width: 645px) 100vw, 645px\" \/><figcaption class=\"wp-element-caption\">You can see the pseudo-element tree in DevTools during a View Transition.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The pseudo tree will get a <code>view-transition-group<\/code> for every named element, those elements are separated from the <code>root<\/code> view transition and put on top of it. The value of the <code>view-transition-name<\/code> will be used in the view transition group and its children. See the screenshot above where the active view transition had <code>view-transition-name: blob;<\/code>. <\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-comment\">\/* \n  Adding a view-transition-name separates the element \n  from the root view transition and puts it on top\n  in the view transition pseudo tree\n*\/<\/span>\n<span class=\"hljs-selector-class\">.header-image<\/span> {\n  <span class=\"hljs-attribute\">view-transition-name<\/span>: header-image;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><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<pre class=\"wp-block-preformatted\">::view-transition<br>\u2514\u2500 ::view-transition-group(root)<br>  \u2514\u2500 ::view-transition-image-pair(root)<br>      \u251c\u2500 ::view-transition-old(root)<br>      \u2514\u2500 ::view-transition-new(root)<br>\u2514\u2500 ::view-transition-group(header-image)<br>  \u2514\u2500 ::view-transition-image-pair(header-image)<br>      \u251c\u2500 ::view-transition-old(header-image)<br>      \u2514\u2500 ::view-transition-new(header-image)<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><code>::view-transition-group<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>view-transition-group<\/code> pseudo-element is responsible for the animation of the position, size, and rotation, this is all generated and applied automatically.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_KwNoKEV\" src=\"\/\/codepen.io\/anon\/embed\/KwNoKEV?height=450&amp;theme-id=1&amp;slug-hash=KwNoKEV&amp;default-tab=js,result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed KwNoKEV\" title=\"CodePen Embed KwNoKEV\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">The browser positions all view transition groups absolutely at the top-left corner.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-comment\">\/* user agent stylesheet *\/<\/span>\n<span class=\"hljs-selector-pseudo\">::view-transition-group(<\/span>*) {\n  <span class=\"hljs-attribute\">position<\/span>: absolute;\n  <span class=\"hljs-attribute\">top<\/span>: <span class=\"hljs-number\">0px<\/span>;\n  <span class=\"hljs-attribute\">left<\/span>: <span class=\"hljs-number\">0px<\/span>;\n  <span class=\"hljs-attribute\">animation-duration<\/span>: <span class=\"hljs-number\">0.25s<\/span>;\n  <span class=\"hljs-attribute\">animation-fill-mode<\/span>: both;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><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<p class=\"wp-block-paragraph\">The keyframe then moves the element to the position where it was when the view transition was called using a matrix animation:<\/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\"><span class=\"hljs-comment\">\/* user agent stylesheet *\/<\/span>\n<span class=\"hljs-keyword\">@keyframes<\/span> -ua-view-transition-group-anim-box {\n  0% {\n    <span class=\"hljs-attribute\">transform<\/span>: <span class=\"hljs-built_in\">matrix<\/span>(<span class=\"hljs-number\">1<\/span>, <span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">1<\/span>, <span class=\"hljs-number\">8<\/span>, <span class=\"hljs-number\">8<\/span>);\n    <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-number\">100px<\/span>;\n    <span class=\"hljs-attribute\">height<\/span>: <span class=\"hljs-number\">100px<\/span>;\n    <span class=\"hljs-attribute\">backdrop-filter<\/span>: none;\n  }\n}<\/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<p class=\"wp-block-paragraph\">Only a start keyframe is added to the animation; it will then animate to the end state that\u2019s set by the browser on the view transition group:<\/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\"><span class=\"hljs-comment\">\/* user agent stylesheet *\/<\/span>\n<span class=\"hljs-selector-pseudo\">::view-transition-group(box)<\/span> {\n  <span class=\"hljs-attribute\">transform<\/span>: <span class=\"hljs-built_in\">matrix<\/span>(<span class=\"hljs-number\">1<\/span>, <span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">1<\/span>, <span class=\"hljs-number\">108<\/span>, <span class=\"hljs-number\">108<\/span>);\n  <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-number\">200px<\/span>;\n  <span class=\"hljs-attribute\">height<\/span>: <span class=\"hljs-number\">200px<\/span>;\n  <span class=\"hljs-comment\">\/* &#91;...] *\/<\/span>\t\n}<\/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<p class=\"wp-block-paragraph\">To simplify: this matrix animation animates the <code>translateX<\/code> and <code>translateY<\/code> from 8px to 108px.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The same thing is applied in reverse if you click the button again:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-comment\">\/* user agent stylesheet *\/<\/span>\n<span class=\"hljs-keyword\">@keyframes<\/span> -ua-view-transition-group-anim-box {\n  0% {\n    <span class=\"hljs-attribute\">transform<\/span>: <span class=\"hljs-built_in\">matrix<\/span>(<span class=\"hljs-number\">1<\/span>, <span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">1<\/span>, <span class=\"hljs-number\">108<\/span>, <span class=\"hljs-number\">108<\/span>);\n    <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-number\">200px<\/span>;\n    <span class=\"hljs-attribute\">height<\/span>: <span class=\"hljs-number\">200px<\/span>;\n    <span class=\"hljs-attribute\">backdrop-filter<\/span>: none;\n  }\n}\n\n<span class=\"hljs-selector-pseudo\">::view-transition-group(box)<\/span> {\n  <span class=\"hljs-attribute\">transform<\/span>: <span class=\"hljs-built_in\">matrix<\/span>(<span class=\"hljs-number\">1<\/span>, <span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">1<\/span>, <span class=\"hljs-number\">8<\/span>, <span class=\"hljs-number\">8<\/span>);\n  <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-number\">100px<\/span>;\n  <span class=\"hljs-attribute\">height<\/span>: <span class=\"hljs-number\">100px<\/span>;\n  <span class=\"hljs-comment\">\/* &#91;...] *\/<\/span>\t\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><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<p class=\"wp-block-paragraph\">All this is handled by the browser, using the user agent stylesheet, often you don\u2019t need to change anything about this animation, you can make it fit your website better using a custom duration, delay and ease. It\u2019s good practice to add all changes to the animation property as individual properties (not using the animation shorthand) so you don\u2019t inadvertently overwrite any of the default values or animations.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It\u2019s also important to understand that many of the animation properties that you set on view transition pseudo-elements are inherited by their children. If you change the duration, fill-mode, delay, timing-function, iteration-count, direction or play-state of the <code>view-transition-group<\/code> , for example, it will also be applied to the <code>view-transition-image-pair<\/code> and <code>view-transition-old<\/code> and <code>view-transition-new<\/code> pseudo-elements.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>::view-transition-image-pair<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The sole purpose of <code>view-transition-image-pair<\/code> is to apply this rule:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-comment\">\/* user agent stylesheet *\/<\/span>\n<span class=\"hljs-selector-pseudo\">::view-transition-image-pair(box)<\/span> {\n  <span class=\"hljs-attribute\">isolation<\/span>: isolate;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><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<p class=\"wp-block-paragraph\"><a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/CSS\/Reference\/Properties\/isolation\">Isolation<\/a> is a CSS property that creates a new stacking context, meaning mix-blend modes set on children remain isolated and won\u2019t mix with anything but themselves.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>::view-transition-old<\/code> and <code>::view-transition-new<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>view-transition-old<\/code> and <code>view-transition-new<\/code> pseudo-elements represent what the element looks like at the start and end of the animation.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">By default, a blend mode animation and opacity change is added by the user agent:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-comment\">\/* user agent stylesheet *\/<\/span>\n<span class=\"hljs-selector-pseudo\">::view-transition-old(box)<\/span> {\n  <span class=\"hljs-attribute\">animation-name<\/span>: -ua-view-transition-fade-out, -ua-mix-blend-mode-plus-lighter;\n}\n<span class=\"hljs-selector-pseudo\">::view-transition-new(box)<\/span> {\n  <span class=\"hljs-attribute\">animation-name<\/span>: -ua-view-transition-fade-in, -ua-mix-blend-mode-plus-lighter;\n}\n\n<span class=\"hljs-keyword\">@keyframes<\/span> -ua-mix-blend-mode-plus-lighter {\n  <span class=\"hljs-selector-tag\">from<\/span> {\n    <span class=\"hljs-attribute\">mix-blend-mode<\/span>: plus-lighter;\n  }\n  <span class=\"hljs-selector-tag\">to<\/span> {\n    <span class=\"hljs-attribute\">mix-blend-mode<\/span>: plus-lighter;\n  }\n}\n\n<span class=\"hljs-keyword\">@keyframes<\/span> -ua-view-transition-fade-in {\n  <span class=\"hljs-selector-tag\">from<\/span> {\n    <span class=\"hljs-attribute\">opacity<\/span>: <span class=\"hljs-number\">0<\/span>;\n  }\n}\n\n<span class=\"hljs-keyword\">@keyframes<\/span> -ua-view-transition-fade-out {\n  <span class=\"hljs-selector-tag\">to<\/span> {\n    <span class=\"hljs-attribute\">opacity<\/span>: <span class=\"hljs-number\">0<\/span>;\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><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<p class=\"wp-block-paragraph\">If all you\u2019re changing is the position, size (without changing the ratio), and\/or rotation of an element during the view transition, then this animation does basically nothing visually.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">When the old and new states aren\u2019t exactly the same, you <strong>will<\/strong> see the animation:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_bNBvwVd\" src=\"\/\/codepen.io\/anon\/embed\/bNBvwVd?height=450&amp;theme-id=1&amp;slug-hash=bNBvwVd&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed bNBvwVd\" title=\"CodePen Embed bNBvwVd\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">However, if you change the aspect ratio or the content of the element you\u2019ve set the view transition on, the animation starts to look a little weird:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_myOxraJ\" src=\"\/\/codepen.io\/anon\/embed\/myOxraJ?height=450&amp;theme-id=1&amp;slug-hash=myOxraJ&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed myOxraJ\" title=\"CodePen Embed myOxraJ\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">That\u2019s because the <code>view-transition-old<\/code> and <code>-new<\/code> aren\u2019t the same size. We can fix this by adding some extra code (see what happens if you check the input), but for a deep dive into aspect ratio changes, you should definitely check out <a href=\"https:\/\/jakearchibald.com\/2024\/view-transitions-handling-aspect-ratio-changes\/\">Jake Archibald&#8217;s writeup<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Understanding the pseudo tree will help you create better view transitions. Once you know which layer controls what, you can better target what you need and start using browser defaults to your advantage.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Each pseudo element plays a distinct role in how the view transition animates. The browser does most of the heavy lifting though, which makes it a little hard to see what\u2019s actually happening under the hood. <\/p>\n","protected":false},"author":52,"featured_media":10078,"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_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[7,101],"class_list":["post-10055","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-css","tag-view-transitions"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/master.dev\/blog\/wp-content\/uploads\/2026\/06\/tree.jpg?fit=2000%2C1200&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/posts\/10055","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\/52"}],"replies":[{"embeddable":true,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/comments?post=10055"}],"version-history":[{"count":10,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/posts\/10055\/revisions"}],"predecessor-version":[{"id":10077,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/posts\/10055\/revisions\/10077"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/media\/10078"}],"wp:attachment":[{"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/media?parent=10055"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/categories?post=10055"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/tags?post=10055"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}