{"id":8424,"date":"2026-01-30T12:47:10","date_gmt":"2026-01-30T17:47:10","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=8424"},"modified":"2026-02-05T14:38:30","modified_gmt":"2026-02-05T19:38:30","slug":"reacts-viewtransition-element","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/reacts-viewtransition-element\/","title":{"rendered":"React&#8217;s ViewTransition Element"},"content":{"rendered":"\n<p>As a bit of a connoisseur of View Transitions and user of React, I&#8217;m naturally interested in the fact that React now <a href=\"https:\/\/react.dev\/reference\/react\/ViewTransition#my-viewtransition-is-not-activating\">has a <code>&lt;ViewTransition&gt;<\/code> element<\/a> it ships directly (in a &#8220;Canary&#8221; pre-release). <\/p>\n\n\n\n<p>I wanna take a look at it, but to start, let&#8217;s&#8230; <em>not<\/em> use it. View Transitions are a feature of the web platform itself, not specific to any framework. So React can&#8217;t really stop us from using them. And it&#8217;s not entirely weird just do it.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Using View Transitions in React (Classic Style?)<\/h2>\n\n\n\n<p>The same-page View Transitions API (the one most relevant for React, as opposed to multi-page View Transitions), is largely 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\"><span class=\"hljs-built_in\">document<\/span>.startViewTransition(<span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n  <span class=\"hljs-comment\">\/\/ change DOM here<\/span>\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>But changing the DOM is&#8230; React&#8217;s job. It doesn&#8217;t really love it when you do it yourself. So instead of doing any DOM manipulation directly ourselves, we&#8217;ll do something React-y instead like update state. <\/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\"><span class=\"hljs-keyword\">import<\/span> React, { useState } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"react\"<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">DemoOne<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> &#91;buttonExpanded, setButtonExpanded] = useState(<span class=\"hljs-literal\">false<\/span>);\n\n  <span class=\"hljs-keyword\">const<\/span> toggleButton = <span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n    <span class=\"hljs-built_in\">document<\/span>.startViewTransition(<span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n      setButtonExpanded(!buttonExpanded);\n    });\n  };\n\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">button<\/span>\n      <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">{<\/span>`<span class=\"hljs-attr\">button<\/span> ${<span class=\"hljs-attr\">buttonExpanded<\/span> ? \"<span class=\"hljs-attr\">expanded<\/span>\" <span class=\"hljs-attr\">:<\/span> \"\"}`}\n      <span class=\"hljs-attr\">onClick<\/span>=<span class=\"hljs-string\">{toggleButton}<\/span>\n    &gt;<\/span>\n      Button\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span><\/span>\n  );\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>The visual part will be handled by CSS. The state change changes as a class, and the classes change the look.<\/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-selector-class\">.button<\/span> {\n   <span class=\"hljs-comment\">\/* button styles *\/<\/span>\n\n   &amp;.expanded {\n     <span class=\"hljs-attribute\">scale<\/span>: <span class=\"hljs-number\">1.4<\/span>;\n     <span class=\"hljs-attribute\">rotate<\/span>: -<span class=\"hljs-number\">6deg<\/span>;\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<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_019c0cba-8a92-7080-ba1e-b06f6cc5d3e6\" src=\"\/\/codepen.io\/editor\/anon\/embed\/019c0cba-8a92-7080-ba1e-b06f6cc5d3e6?height=450&amp;theme-id=1&amp;slug-hash=019c0cba-8a92-7080-ba1e-b06f6cc5d3e6&amp;default-tab=js,result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed 019c0cba-8a92-7080-ba1e-b06f6cc5d3e6\" title=\"CodePen Embed 019c0cba-8a92-7080-ba1e-b06f6cc5d3e6\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">Getting Ready to use <code>&lt;ViewTransition&gt;<\/code><\/h2>\n\n\n\n<p>The element is only in the &#8220;Canary&#8221; build of React at the time of this writing, meaning you&#8217;d have to install that specifically like:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">npm install react@canary<\/pre>\n\n\n\n<p>So your <code>package.json<\/code> would list <code>canary<\/code> as the version.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\">{\n  <span class=\"hljs-attr\">\"dependencies\"<\/span>: {\n    <span class=\"hljs-attr\">\"react\"<\/span>: <span class=\"hljs-string\">\"canary\"<\/span>,\n    <span class=\"hljs-attr\">\"react-dom\"<\/span>: <span class=\"hljs-string\">\"canary\"<\/span>\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JSON \/ JSON with Comments<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">json<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Or if you&#8217;re using React client-side you could have your imports mapped to a CDN URL. Like this if you&#8217;re using an import map.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">script<\/span> <span class=\"hljs-attr\">type<\/span>=<span class=\"hljs-string\">\"importmap\"<\/span>&gt;<\/span><span class=\"actionscript\">\n{\n  <span class=\"hljs-string\">\"imports\"<\/span>: {\n    <span class=\"hljs-string\">\"react\"<\/span>: <span class=\"hljs-string\">\"https:\/\/esm.sh\/react@canary\"<\/span>,\n    <span class=\"hljs-string\">\"react-dom\"<\/span>: <span class=\"hljs-string\">\"https:\/\/esm.sh\/react-dom@canary\"<\/span>\n  }\n}\n<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">script<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h2 class=\"wp-block-heading\">Using <code>&lt;ViewTransition&gt;<\/code> in React<\/h2>\n\n\n\n<p>Now we can import <code>ViewTransition<\/code> itself and use it as a JSX element, along with it&#8217;s buddy <code>startTransition<\/code>. <\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">import<\/span> React, { startTransition, ViewTransition } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"react\"<\/span>;\n\n<span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">App<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> &#91;buttonExpanded, setButtonExpanded] = useState(<span class=\"hljs-literal\">false<\/span>);\n\n  <span class=\"hljs-keyword\">const<\/span> toggleButton = <span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n    startTransition(<span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n      <span class=\"hljs-comment\">\/\/ do something that changes the DOM but, like, in a React-y way. <\/span>\n      setButtonExpanded(!buttonExpanded);\n    });\n  };\n\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">main<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">ViewTransition<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">button<\/span> \n          <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">{<\/span>`<span class=\"hljs-attr\">button<\/span> ${<span class=\"hljs-attr\">buttonExpanded<\/span> ? \"<span class=\"hljs-attr\">expanded<\/span>\" <span class=\"hljs-attr\">:<\/span> \"\"}`}\n          <span class=\"hljs-attr\">onClick<\/span>=<span class=\"hljs-string\">{toggleButton}<\/span>\n        &gt;<\/span> \n          Button\n        <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">ViewTransition<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">main<\/span>&gt;<\/span><\/span>\n  );\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><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>The same CSS as above would apply, as all we&#8217;re doing is toggling a class on a button. But note we&#8217;re not using like <code>.classList.toggle(\"expanded\")<\/code> as that&#8217;s a direct DOM method, we&#8217;re letting React go through a re-render cycle (or however you say it) and handling that itself. <\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_019c0f9c-c03d-7439-9eef-1e40b832a343\" src=\"\/\/codepen.io\/editor\/anon\/embed\/019c0f9c-c03d-7439-9eef-1e40b832a343?height=450&amp;theme-id=1&amp;slug-hash=019c0f9c-c03d-7439-9eef-1e40b832a343&amp;default-tab=js,result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed 019c0f9c-c03d-7439-9eef-1e40b832a343\" title=\"CodePen Embed 019c0f9c-c03d-7439-9eef-1e40b832a343\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">So&#8230; They Both Work Fine?<\/h2>\n\n\n\n<p>Not really. It&#8217;s just good luck when native View Transitions work at all. <\/p>\n\n\n\n<p class=\"learn-more\">As I write, if you try the <a href=\"https:\/\/codepen.io\/editor\/chriscoyier\/pen\/019c0cba-8a92-7080-ba1e-b06f6cc5d3e6\">native View Transitions demo<\/a> in Firefox, it does not to any transitioning. It&#8217;s just broken. <a href=\"https:\/\/bsky.app\/profile\/nmn.sh\/post\/3me2nfkwvv22v\">The short answer<\/a> is essentially that we just can&#8217;t easily know when React is going to do (&#8220;schedule&#8221;) DOM stuff so it may or may not happen within the callback timing for the View Transition. Somehow it &#8220;works&#8221; in Safari and Chrome, but there is no guarantee of that.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_019c0fae-8e0a-764c-97b6-8953c00c7947\" src=\"\/\/codepen.io\/editor\/anon\/embed\/019c0fae-8e0a-764c-97b6-8953c00c7947?height=450&amp;theme-id=1&amp;slug-hash=019c0fae-8e0a-764c-97b6-8953c00c7947&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed 019c0fae-8e0a-764c-97b6-8953c00c7947\" title=\"CodePen Embed 019c0fae-8e0a-764c-97b6-8953c00c7947\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>One pretty minor thing is that you&#8217;ll need to apply a CSS <code>view-transition-name<\/code> yourself on anything you&#8217;re using <code>document.startViewTransition<\/code> with, while <code>&lt;ViewTransition&gt;<\/code> applies a <code>view-transition-name<\/code> automatically for you. That&#8217;s a little tiny bonus for <code>&lt;ViewTransition&gt;<\/code>. <\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The &#8220;I Hate This&#8221; Part of Me<\/h3>\n\n\n\n<p>Part of me doesn&#8217;t like this at all. React isn&#8217;t really giving us all that much. It&#8217;s not making this stuff any easier, it&#8217;s just making us do it in a way that doesn&#8217;t disrupt how the framework works. If we spend a lot of time learning this (<a href=\"https:\/\/react.dev\/reference\/react\/ViewTransition#my-viewtransition-is-not-activating\">and there is plenty to learn!<\/a>) it&#8217;s not particularly transferrable knowledge to anywhere else. <\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The &#8220;OK, Fine&#8221; Part of Me<\/h3>\n\n\n\n<p>React wants to handle the DOM for you, and it always has since Day One. That&#8217;s what you&#8217;re buying, and because of that, you have to buy into letting it do certain things for you. This means using <code>&lt;ViewTransition&gt;<\/code>, presumably, is going to do things like &#8220;automatically coordinate the transition with its rendering lifecycle, Suspense boundaries, and concurrent features&#8221; and do things like batch updates, prevent conflicts, mange nesting, and whatever complicated crap you and I don&#8217;t want to think about.<\/p>\n\n\n\n<p>Also, things are <em>a little bit<\/em> more &#8220;declarative&#8221; in that you&#8217;re being very specific about where you are applying the wrapping <code>&lt;ViewTransition&gt;<\/code> element, which may jive with people&#8217;s mental model better. But you still need to call <code>startTransition<\/code> so it&#8217;s still fairly imperative too, and I can imagine in more complex nested UIs, it&#8217;ll be a bit confusing to figure out where best to orchestrate all this. <\/p>\n\n\n\n<p>I admit I kinda like the very specific attributes like <code>enter<\/code> and <code>exit<\/code> on the <code>&lt;ViewTransition&gt;<\/code> element, which maps to &#8220;bring you own&#8221; CSS view transition classes. This is more straightforward to me than <a href=\"https:\/\/cydstumpel.nl\/being-lazy-with-view-transition-old-and-new\/\">the <code>:only-child<\/code> technique<\/a> of figuring it out for yourself. <\/p>\n\n\n\n<p>So, I&#8217;ll leave you with a demo like that:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_019c0fc5-5302-71ad-87a1-32e9db8eb978\" src=\"\/\/codepen.io\/editor\/anon\/embed\/019c0fc5-5302-71ad-87a1-32e9db8eb978?height=450&amp;theme-id=1&amp;slug-hash=019c0fc5-5302-71ad-87a1-32e9db8eb978&amp;default-tab=js,result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed 019c0fc5-5302-71ad-87a1-32e9db8eb978\" title=\"CodePen Embed 019c0fc5-5302-71ad-87a1-32e9db8eb978\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n","protected":false},"excerpt":{"rendered":"<p>The Canary version of React has a special component for ViewTransitions. Does it help?<\/p>\n","protected":false},"author":1,"featured_media":8437,"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":[],"class_list":["post-8424","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/view-trans.jpg?fit=2000%2C1200&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/8424","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=8424"}],"version-history":[{"count":6,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/8424\/revisions"}],"predecessor-version":[{"id":8497,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/8424\/revisions\/8497"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/8437"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=8424"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=8424"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=8424"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}