{"id":9974,"date":"2026-06-12T10:30:40","date_gmt":"2026-06-12T15:30:40","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=9974"},"modified":"2026-06-12T10:32:01","modified_gmt":"2026-06-12T15:32:01","slug":"in-n-out-animations-popovers-part-2-3","status":"publish","type":"post","link":"https:\/\/master.dev\/blog\/in-n-out-animations-popovers-part-2-3\/","title":{"rendered":"In-N-Out Animations: Popovers (Part 2\/3)"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">We kicked this series off looking a animating in and out the <code>&lt;dialog&gt;<\/code> element. This time we&#8217;re going to look at popovers. That is, this modern beauty:<\/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> <span class=\"hljs-attr\">popovertarget<\/span>=<span class=\"hljs-string\">\"my-popover\"<\/span>&gt;<\/span>\n  Toggle Popover\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span>\n\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">aside<\/span> <span class=\"hljs-attr\">popover<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">\"my-popover\"<\/span>&gt;<\/span>\n  Content of popover\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">aside<\/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 class=\"wp-block-paragraph\">That &#8220;popover&#8221; will open and close (i.e. change from <code>display: none;<\/code> to <code>display: block;<\/code>) automatically with the button press. But we want to animate that! In and out! <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Admittedly, popovers are fairly similar to dialogs (but certainly <a href=\"https:\/\/frontendmasters.com\/blog\/whats-the-difference-between-htmls-dialog-element-and-popovers\/\">have important differences<\/a>), and that is part of the point. We&#8217;re going to show off that our three-phase system we established in the first article transfers over to something else just fine.<\/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                  <\/ol>\n        <\/div>\n<\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Demo so you an see what we&#8217;re doing:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_019cfbce-0b8b-7fde-a8ee-14ca580475ff\" src=\"\/\/codepen.io\/editor\/anon\/embed\/019cfbce-0b8b-7fde-a8ee-14ca580475ff?height=450&amp;theme-id=1&amp;slug-hash=019cfbce-0b8b-7fde-a8ee-14ca580475ff&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed 019cfbce-0b8b-7fde-a8ee-14ca580475ff\" title=\"CodePen Embed 019cfbce-0b8b-7fde-a8ee-14ca580475ff\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">Reminder: The 3-2-1 System<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The important thing to remember about styling both the <strong>in<\/strong> and <strong>out<\/strong> of an element was styling all three states and in order such that the &#8220;on the way in&#8221; styles have enough specificity to win over the open styles.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It&#8217;s essentially like this<\/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-selector-class\">.element<\/span> {\n  <span class=\"hljs-attribute\">transition<\/span>: ...;\n\n  <span class=\"hljs-comment\">\/* 3 *\/<\/span>\n  &amp;:not(.open) {\n\n  }\n\n  <span class=\"hljs-comment\">\/* 2 *\/<\/span>\n  &amp;<span class=\"hljs-selector-class\">.open<\/span> {\n  \n  }\n  \n  <span class=\"hljs-comment\">\/* 1 *\/<\/span>\n  <span class=\"hljs-keyword\">@starting-style<\/span> {\n    &amp;<span class=\"hljs-selector-class\">.open<\/span> {\n\n    }\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\">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<h2 class=\"wp-block-heading\">3-2-1 with Popovers<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Popovers have their own special pseudo-class for determining if they are open or not, so we can apply it like like below. This time, we&#8217;ll transition the <code>opacity<\/code> and <code>rotate<\/code> property. Anything is on the table! That&#8217;s just what we&#8217;re choosing this time.<\/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\">&#91;popover] {\n  --timing: <span class=\"hljs-number\">.66<\/span>s;\n  \n  transition:\n    <span class=\"hljs-keyword\">var<\/span>(--timing) opacity,\n    <span class=\"hljs-keyword\">var<\/span>(--timing) rotate,\n    <span class=\"hljs-keyword\">var<\/span>(--timing) display allow-discrete,\n    <span class=\"hljs-keyword\">var<\/span>(--timing) overlay allow-discrete;\n\n  <span class=\"hljs-comment\">\/* 3 *\/<\/span>\n  &amp;:not(:popover-open) {\n    <span class=\"hljs-attr\">opacity<\/span>: <span class=\"hljs-number\">0<\/span>;\n    rotate: <span class=\"hljs-number\">10<\/span>deg;\n    transform-origin: top left;\n  }\n\n  <span class=\"hljs-comment\">\/* 2 *\/<\/span>\n  &amp;:popover-open {\n    --timing: <span class=\"hljs-number\">0.2<\/span>s;\n    opacity: <span class=\"hljs-number\">1<\/span>;\n    rotate: <span class=\"hljs-number\">3<\/span>deg;\n  }\n\n  <span class=\"hljs-comment\">\/* 1 *\/<\/span>\n  @starting-style {\n    &amp;:popover-open {\n      <span class=\"hljs-attr\">opacity<\/span>: <span class=\"hljs-number\">0<\/span>;\n      rotate: <span class=\"hljs-number\">-3<\/span>deg;\n    }\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<h2 class=\"wp-block-heading\">Handling Reduced Motion<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">We&#8217;re <em>introducing<\/em> movement on the page here. We should take care to <em>not<\/em> do that if the user doesn&#8217;t want it. But the opacity change is fine. So we&#8217;d do it like this.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">@media (prefers-reduced-motion: reduce) {\n  &#91;popover] {  \n    <span class=\"hljs-attr\">transition<\/span>:\n      <span class=\"hljs-keyword\">var<\/span>(--timing) opacity,\n      <span class=\"hljs-comment\">\/* rotate is removed! *\/<\/span>\n      <span class=\"hljs-keyword\">var<\/span>(--timing) display allow-discrete,\n      <span class=\"hljs-keyword\">var<\/span>(--timing) overlay allow-discrete;\n\n    &amp;, &amp;:popover-open {\n      <span class=\"hljs-attr\">rotate<\/span>: <span class=\"hljs-number\">0<\/span>deg;\n    }\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\">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\">Note that we&#8217;ve made the popover have <em>no<\/em> rotation. That&#8217;s not a requirement, you could leave it tilted if you want.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/codepen.io\/editor\/chriscoyier\/pen\/019cfbce-0b8b-7fde-a8ee-14ca580475ff\">Here&#8217;s the final demo.<\/a><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Noting a Safari Quirk<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Positioning anchors for popovers are &#8220;implied&#8221; in most browsers. And that&#8217;s true of Safari as well. You can see when we open the popover, it&#8217;s positioned where the button that opened it is. But &#8220;on the way out&#8221; it loses that anchor for some reason. Bug? Unsure. So in the demo we&#8217;re just being specific about the anchor and that fixes it. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Also, in <a href=\"https:\/\/master.dev\/blog\/in-n-out-animations-dialogs-part-1-3\/\">Part 1<\/a>, the &#8220;on the way out&#8221; styles for the <code>&lt;dialog&gt;<\/code> have an issue with the <code>inset<\/code> property, so we set it explicitly to <code>inset: 0;<\/code> and that fixes Safari.<\/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                  <\/ol>\n        <\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Using our 3, 2, 1 state system, we can make popovers animate on &#8220;the way in&#8221; and &#8220;the way out&#8221; just like we did with dialogs in Part 1.<\/p>\n","protected":false},"author":1,"featured_media":10085,"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,490,100,7,120],"class_list":["post-9974","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-starting-style","tag-allow-discrete","tag-animation","tag-css","tag-popover"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/master.dev\/blog\/wp-content\/uploads\/2026\/06\/popover-movement.jpg?fit=2000%2C1200&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/posts\/9974","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=9974"}],"version-history":[{"count":12,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/posts\/9974\/revisions"}],"predecessor-version":[{"id":10090,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/posts\/9974\/revisions\/10090"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/media\/10085"}],"wp:attachment":[{"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/media?parent=9974"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/categories?post=9974"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/tags?post=9974"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}