{"id":5917,"date":"2025-05-22T13:27:26","date_gmt":"2025-05-22T18:27:26","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=5917"},"modified":"2025-06-04T06:05:52","modified_gmt":"2025-06-04T11:05:52","slug":"move-modal-in-on-a-shape","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/move-modal-in-on-a-shape\/","title":{"rendered":"Move Modal in on a&#8230; shape()"},"content":{"rendered":"\n<p><a href=\"https:\/\/css-tricks.com\/move-modal-path\/\">Years ago<\/a> I did <a href=\"https:\/\/codepen.io\/chriscoyier\/pen\/NgNymx\">a demo<\/a> where a modal was triggered open and it came flying in on a curved path. I always thought that was kinda cool. Time has chugged on, and I thought I&#8217;d revisit that with a variety of improved web platform technology.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Instead of a <code>&lt;div&gt;<\/code> it&#8217;ll be a proper <code>&lt;dialog&gt;<\/code>.<\/li>\n\n\n\n<li> We&#8217;ll set it up to work with no JavaScript at all. But we&#8217;ll fall back to using the JavaScript methods <code>.showModal()<\/code> and <code>.close()<\/code> to support browsers that don&#8217;t support the <a href=\"https:\/\/www.oddbird.net\/2025\/04\/17\/winging-it-18\/\">invoker<\/a> command stuff.<\/li>\n\n\n\n<li>We&#8217;ll use <code>@starting-style<\/code>, which is arguably more verbose, but allows for opening and closing animations while allowing the <code>&lt;dialog&gt;<\/code> to be <code>display: none;<\/code> when closed which is better than it was before where the dialog was always in the accessibility tree.<\/li>\n\n\n\n<li>Instead of <code>path()<\/code> for the <code>offset-path<\/code>, which forced us into pixels, we&#8217;ll use <code>shape()<\/code> which allows us to use the viewport better. But we&#8217;ll still fall back to <code>path()<\/code>. <\/li>\n\n\n\n<li>We&#8217;ll continue accounting for <code>prefers-reduced-motion<\/code> however we need to.<\/li>\n<\/ol>\n\n\n\n<p>Here&#8217;s where the refactor ends up:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_GggQrQq\" src=\"\/\/codepen.io\/anon\/embed\/GggQrQq?height=450&amp;theme-id=47434&amp;slug-hash=GggQrQq&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed GggQrQq\" title=\"CodePen Embed GggQrQq\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">1. Use a Dialog<\/h2>\n\n\n\n<p>The <code>&lt;dialog&gt;<\/code> element is the correct semantic choice for this kind of UI, generally. But particularly if you are wanting to force the user to interact with the dialog before doing anything else (i.e. a &#8220;modal&#8221;) then <code>&lt;dialog&gt;<\/code> is particularly good as it moves then traps focus within the dialog. <\/p>\n\n\n\n<h2 class=\"wp-block-heading\">2. Progressively Enhanced Dialog Open and Close <\/h2>\n\n\n\n<p><a href=\"https:\/\/frontendmasters.com\/blog\/lessons-learned-from-recreating-a-styled-dialog\/#no-invokers-yes-invokers\">I only just learned<\/a> you can open a modal (in the proper &#8220;modal&#8221; state) without any JavaScript using invokers. <\/p>\n\n\n\n<p>So you can do an &#8220;open&#8221; button like this, where <code>command<\/code> is the literal command you have to call to open the modal and the <code>commandfor<\/code> matches the <code>id<\/code> of the dialog.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" 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\">button<\/span>\n  <span class=\"hljs-attr\">command<\/span>=<span class=\"hljs-string\">\"show-modal\"<\/span>\n  <span class=\"hljs-attr\">commandfor<\/span>=<span class=\"hljs-string\">\"my-dialog\"<\/span>\n&gt;<\/span>\n  Open Modal\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><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<p>You may want to include <code>popovertarget=\"my-dialog\"<\/code> as well, which is a still-no-JS fallback that will open the modal in a non-modal state (no focus trap) in browsers that don&#8217;t support invokers yet. Buttttttttt, we&#8217;re going to need a JavaScript fallback anyway, so let&#8217;s skip it.<\/p>\n\n\n\n<p>Here&#8217;s how a close button can be:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" 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\">button<\/span>\n  <span class=\"hljs-attr\">command<\/span>=<span class=\"hljs-string\">\"close\"<\/span>\n  <span class=\"hljs-attr\">commandfor<\/span>=<span class=\"hljs-string\">\"my-dialog\"<\/span>\n&gt;<\/span>\n  Close\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><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<p>For browsers that don&#8217;t support that, we&#8217;ll use the <code>&lt;dialog&gt;<\/code> element&#8217;s JavaScript API to do the job instead (use whatever selectors you need):<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ For browsers that don't support the command\/invokes\/popup anything yet.<\/span>\n<span class=\"hljs-keyword\">if<\/span> (<span class=\"hljs-built_in\">document<\/span>.createElement(<span class=\"hljs-string\">\"button\"<\/span>).commandForElement === <span class=\"hljs-literal\">undefined<\/span>) {\n  <span class=\"hljs-keyword\">const<\/span> dialog = <span class=\"hljs-built_in\">document<\/span>.querySelector(<span class=\"hljs-string\">\"#my-dialog\"<\/span>);\n  <span class=\"hljs-keyword\">const<\/span> openButton = <span class=\"hljs-built_in\">document<\/span>.querySelector(<span class=\"hljs-string\">\"#open-button\"<\/span>);\n  <span class=\"hljs-keyword\">const<\/span> closeButton = <span class=\"hljs-built_in\">document<\/span>.querySelector(<span class=\"hljs-string\">\"#close-button\"<\/span>);\n\n  openButton.addEventListener(<span class=\"hljs-string\">\"click\"<\/span>, () =&gt; {\n    dialog.showModal();\n  });\n\n  closeButton.addEventListener(<span class=\"hljs-string\">\"click\"<\/span>, () =&gt; {\n    dialog.close();\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\">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>At this point, we&#8217;ve got a proper dialog that opens and closes.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">3. Open &amp; Close Animation while still using <code>display: none;<\/code><\/h2>\n\n\n\n<p>One thing about <code>&lt;dialog&gt;<\/code> is that when it&#8217;s not open, it&#8217;s <code>display: none;<\/code> automatically, without you having to add any additional styles to do that. Then when you open it (via invoker, method, or adding an <code>open<\/code> attribute), it becomes <code>display: block;<\/code> automatically. <\/p>\n\n\n\n<p>For the past forever in CSS, it hasn&#8217;t been possible to run animations on elements between <code>display: none<\/code> and other display values. The element instantly disappears, so when would that animation happen anyway? Well now you can. If you <code>transition<\/code> the <code>display<\/code> property and use the <code>allow-discrete<\/code> keyword, it will ensure that property &#8220;flips&#8221; when appropriate. That is, it will immediately appear when transitioning <em>away<\/em> from being hidden and delay flipping until the end of the transition when transitioning into being hidden. <\/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-selector-tag\">dialog<\/span> {\n  <span class=\"hljs-attribute\">transition<\/span>: display <span class=\"hljs-number\">1.1s<\/span> allow-discrete;\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>But we&#8217;ll be adding to that transition, which is fine! For instance, to animate opacity <strong>on the way both in and out<\/strong>, we can do it like this:<\/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-selector-tag\">dialog<\/span> {\n  <span class=\"hljs-attribute\">transition<\/span>:\n    display <span class=\"hljs-number\">1.1s<\/span> allow-discrete,\n    opacity <span class=\"hljs-number\">1.1s<\/span> ease-out;\n  <span class=\"hljs-attribute\">opacity<\/span>: <span class=\"hljs-number\">0<\/span>;\n\n  &amp;&#91;open] {\n    <span class=\"hljs-attribute\">opacity<\/span>: <span class=\"hljs-number\">1<\/span>;\n    @starting-style {\n      <span class=\"hljs-attribute\">opacity<\/span>: <span class=\"hljs-number\">0<\/span>;\n    }\n  }\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>I find that kinda <a href=\"https:\/\/zellwk.com\/blog\/making-sense-of-starting-style\/#:~:text=Since%20DRY%20(Don\u2019t%20Repeat%20Yourself)%20is%20one%20of%20the%20major%20programming%20principles%2C%20it%20can%20feel%20like%20we\u2019re%20going%20against%20everything%20we%20know%20to%20be%20right%20and%20good\">awkward and repetitive<\/a>, but that&#8217;s what it takes and the effect is worth it.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">4. Using <code>shape()<\/code> for the movement<\/h2>\n\n\n\n<p>The cool curved movement in the original movement was thanks to animating along an <code>offset-path<\/code>. But I used <code>offset-path: path()<\/code> which was the only practical thing available at the time. Now, <code>path()<\/code> is all but replaced by <a href=\"https:\/\/frontendmasters.com\/blog\/shape-a-new-powerful-drawing-syntax-in-css\/\">the way-better-for-CSS <code>shape()<\/code> function<\/a>. There is no way with <code>path()<\/code> to express something like &#8220;animate from the top left corner of the window to the middle&#8221;, because <code>path()<\/code> deals in pixels which just can&#8217;t know how to do that on an arbitrary screen.<\/p>\n\n\n\n<p>I&#8217;ll leave the <code>path()<\/code> stuff in the to accommodate browsers not supporting <code>shape()<\/code> yet, so it&#8217;ll end up like:<\/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\">dialog {\n  ...\n\n  @supports (offset-rotate: <span class=\"hljs-number\">0<\/span>deg) {\n    offset-rotate: <span class=\"hljs-number\">0<\/span>deg;\n    offset-path: path(<span class=\"hljs-string\">\"M 250,100 S -300,500 -700,-200\"<\/span>);\n  }\n  @supports (\n    offset-path: shape(<span class=\"hljs-keyword\">from<\/span> top left, curve to <span class=\"hljs-number\">50<\/span>% <span class=\"hljs-number\">50<\/span>% <span class=\"hljs-keyword\">with<\/span> <span class=\"hljs-number\">25<\/span>% <span class=\"hljs-number\">100<\/span>%)\n  ) {\n    offset-path: shape(<span class=\"hljs-keyword\">from<\/span> top left, curve to <span class=\"hljs-number\">50<\/span>% <span class=\"hljs-number\">50<\/span>% <span class=\"hljs-keyword\">with<\/span> <span class=\"hljs-number\">25<\/span>% <span class=\"hljs-number\">100<\/span>%);\n    offset-distance: <span class=\"hljs-number\">0<\/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>That <code>shape()<\/code> syntax expresses this movement:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"773\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/05\/shape-movement.png?resize=1024%2C773&#038;ssl=1\" alt=\"\" class=\"wp-image-5957\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/05\/shape-movement.png?resize=1024%2C773&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/05\/shape-movement.png?resize=300%2C226&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/05\/shape-movement.png?resize=768%2C579&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/05\/shape-movement.png?resize=1536%2C1159&amp;ssl=1 1536w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/05\/shape-movement.png?w=1800&amp;ssl=1 1800w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<p>Those points flex to whatever is going on in the viewport, unlike the pixel values in <code>path()<\/code>. Fun!<\/p>\n\n\n\n<p>This stuff is so new from a browser support perspective, I&#8217;m finding that Chrome 126, which is the stable version as I write, does support <code>clip-path: shape()<\/code>, but doesn&#8217;t support <code>offset-path: shape()<\/code>. Chrome Canary is at 128, and does support <code>offset-path: shape()<\/code>. But the demo is coded such that it falls back to the original <code>path()<\/code> by using <code>@supports<\/code> tests. <\/p>\n\n\n\n<p>Here&#8217;s a video of it working responsively:<\/p>\n\n\n\n\t\t<figure class=\"wp-block-jetpack-videopress jetpack-videopress-player\" style=\"\" >\n\t\t\t<div class=\"jetpack-videopress-player__wrapper\"> <iframe title=\"VideoPress Video Player\" aria-label='VideoPress Video Player' width='500' height='331' src='https:\/\/videopress.com\/embed\/uujYoeLh?cover=1&amp;autoPlay=0&amp;controls=1&amp;loop=0&amp;muted=0&amp;persistVolume=1&amp;playsinline=0&amp;preloadContent=metadata&amp;useAverageColor=1&amp;hd=0' frameborder='0' allowfullscreen data-resize-to-parent=\"true\" allow='clipboard-write'><\/iframe><script src='https:\/\/v0.wordpress.com\/js\/next\/videopress-iframe.js?m=1739540970'><\/script><\/div>\n\t\t\t\n\t\t\t\n\t\t<\/figure>\n\t\t\n\n\n<h2 class=\"wp-block-heading\">5. Preferring Less Motion<\/h2>\n\n\n\n<p>I think this is kind of a good example of honoring the intention. <\/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-keyword\">@media<\/span> (prefers-reduced-motion) {\n  <span class=\"hljs-selector-tag\">offset-path<\/span>: <span class=\"hljs-selector-tag\">none<\/span>;\n  <span class=\"hljs-selector-tag\">transition<\/span>: <span class=\"hljs-selector-tag\">display<\/span> 0<span class=\"hljs-selector-class\">.25s<\/span> <span class=\"hljs-selector-tag\">allow-discrete<\/span>, <span class=\"hljs-selector-tag\">opacity<\/span> 0<span class=\"hljs-selector-class\">.25s<\/span> <span class=\"hljs-selector-tag\">ease-out<\/span>;\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>With that, there is far less movement. But you still see the modal fade in (a bit quicker) which still might be a helpful animation emphasizing &#8220;this is leaving&#8221; or &#8220;this is entering&#8221;. <\/p>\n\n\n\n\t\t<figure class=\"wp-block-jetpack-videopress jetpack-videopress-player\" style=\"\" >\n\t\t\t<div class=\"jetpack-videopress-player__wrapper\"> <iframe title=\"VideoPress Video Player\" aria-label='VideoPress Video Player' width='500' height='264' src='https:\/\/videopress.com\/embed\/XpXcvYgv?cover=1&amp;autoPlay=0&amp;controls=1&amp;loop=0&amp;muted=0&amp;persistVolume=1&amp;playsinline=0&amp;preloadContent=metadata&amp;useAverageColor=1&amp;hd=0' frameborder='0' allowfullscreen data-resize-to-parent=\"true\" allow='clipboard-write'><\/iframe><script src='https:\/\/v0.wordpress.com\/js\/next\/videopress-iframe.js?m=1739540970'><\/script><\/div>\n\t\t\t\n\t\t\t\n\t\t<\/figure>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Got an old &#8220;modal&#8221; design? Now might be the time to upgrade it to a <dialog>, as we can do both open &#038; close animations now.<\/p>\n","protected":false},"author":1,"featured_media":5968,"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":[229,7,98,353,350,352],"class_list":["post-5917","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-starting-style","tag-css","tag-dialog","tag-offset-path","tag-prefers-reduced-motion","tag-shape"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/05\/Move-Modal-in-on-a.-shape.jpg?fit=1140%2C676&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/5917","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=5917"}],"version-history":[{"count":17,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/5917\/revisions"}],"predecessor-version":[{"id":6001,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/5917\/revisions\/6001"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/5968"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=5917"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=5917"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=5917"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}