Ryan Mulligan Blogging general thoughts and rambles, code snippets, and front-end web dev discoveries 2025-10-11T00:00:00Z https://ryanmulligan.dev Ryan Mulligan hey@ryanmulligan.dev Style Review 2021-11-05T00:00:00Z https://ryanmulligan.dev/blog/style-review/ <h2 id="donut-dessert-lollipop-dragee-pudding-marzipan-jelly">Donut dessert lollipop dragée pudding marzipan jelly</h2> <p>Danish icing cake <em>sugar plum chocolate bar</em> candy canes macaroon pie.</p> <p>Bear claw bear claw biscuit fruitcake icing brownie. Jelly-o pudding tart cake ice cream jelly-o. Danish sugar plum chocolate cake wafer cake pudding sweet roll sesame snaps tiramisu. Carrot cake soufflé chocolate bar biscuit ice cream donut <a href="https://en.wikipedia.org/wiki/Bear_claw" target="_blank" rel="noopener">bear claw</a> muffin marzipan.</p> <h3 id="carrot-cake">Carrot cake?</h3> <p>Toffee wafer bonbon dessert dragée topping. Jelly tiramisu <s>gingerbread pie</s> toffee chocolate <strong>chocolate</strong> cake caramels. Donut gummi bears oat cake sugar plum cake marzipan marzipan. Cake gingerbread fruitcake tart chupa chups.</p> <h3 id="chocolate-cake-danish-toffee">Chocolate cake! Danish toffee!</h3> <p>Danish sugar plum chocolate cake wafer:</p> <figure><picture><source type="image/webp" srcset="https://ryanmulligan.dev/images/Tkx7mmWXl7-100.webp 100w, https://ryanmulligan.dev/images/Tkx7mmWXl7-400.webp 400w, https://ryanmulligan.dev/images/Tkx7mmWXl7-800.webp 800w" sizes="100vw" /><img alt="Stock photo of a fancy chocolate cake" src="https://ryanmulligan.dev/images/Tkx7mmWXl7-100.jpeg" width="800" height="588" srcset="https://ryanmulligan.dev/images/Tkx7mmWXl7-100.jpeg 100w, https://ryanmulligan.dev/images/Tkx7mmWXl7-400.jpeg 400w, https://ryanmulligan.dev/images/Tkx7mmWXl7-800.jpeg 800w" sizes="100vw" /></picture><figcaption>The fanciest of the chocolate cakes</figcaption></figure> <p>Pastry jelly tootsie roll biscuit sesame snaps sesame snaps cotton candy sweet. Muffin tart cupcake jelly marzipan jelly beans liquorice pudding. Croissant powder marshmallow donut candy canes cupcake.</p> <aside class="callout"><p>Cake gummies powder chocolate cake gummi bears bear claw chocolate sugar plum apple pie muffin.</p> </aside><p>Tiramisu apple pie muffin fruitcake wafer powder macaroon muffin caramels.</p> <ol> <li>Pie sugar sweet roll <a href="https://en.wikipedia.org/wiki/Jujube_(confectionery)" target="_blank" rel="noopener">jujubes</a> cake</li> <li>Tart sweet pudding caramels candy candy marzipan carrot cake soufflé chocolate bar biscuit.</li> <li>Dessert toffee donut jelly</li> <li>Powder gummies cheesecake brownie</li> </ol> <h2 id="biscuit-wafer-danish-sweet-roll-wafer">Biscuit wafer danish sweet roll wafer</h2> <p>Toffee jelly beans fruitcake cake candy canes liquorice gingerbread:</p> <ul class="multi-column"> <li>Tart</li> <li>fruitcake</li> <li>shortbread</li> <li>chupa chups</li> <li>chocolate cake</li> <li>cheesecake</li> <li>gingerbread</li> </ul> <p>Jujubes chocolate sesame snaps donut topping pie. Cake gummies powder chocolate cake cookie sesame snaps chocolate cake. Gummi bears bear claw chocolate sugar plum chupa chups.</p> <p>Pudding lollipop cake gummi bears oat cake bear claw muffin powder chupa chups. Tiramisu apple pie muffin fruitcake wafer powder macaroon muffin caramels.</p> <blockquote> <p>Chupa chups bear claw biscuit cookie! Sweet biscuit powder... ice cream cupcake danish? Cake gummies powder chocolate cake cookie.</p> </blockquote> <p>Sweet chocolate cake oat cake dragée candy apple pie. Oat cake soufflé brownie toffee gummi bears marzipan chocolate bar. Try <code>getChocolate('cake')</code> or fruitcake pie chocolate bar shortbread.</p> <p>Chocolate cake sweet roll jelly beans. Cake lollipop apple pie lollipop jelly beans cookie.</p> <pre class="language-js"><code class="language-js"><span class="token keyword">const</span> yum <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">".yum"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">function</span> <span class="token function">handleClick</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// Log something sweet</span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"Chocolate!"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> yum<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">"click"</span><span class="token punctuation">,</span> handleClick<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre> <ol> <li>Chocolate cake sweet roll jelly beans.</li> <li>Cake lollipop apple pie.</li> <li>Lollipop jelly beans cookie!</li> </ol> <p>Jelly-o gummi bears cake. Dragée chocolate cake danish toffee cupcake brownie cheesecake oat cake topping. Carrot cake chupa chups ice cream tart soufflé gummi bears gummies danish.</p> <p>Donut.</p> Migrating to Eleventy 2021-11-08T00:00:00Z https://ryanmulligan.dev/blog/migrating-to-11ty/ <h2 id="hello-world">Hello, world!</h2> <p>The time has come. I've finally decided to explore the wonderful world of <a href="https://www.11ty.dev/">11ty</a>!</p> <p>The migration process for my personal website was dead simple. Not that moving a tiny one-pager over to a static site generator is really a big deal, but getting started was just so easy. <a href="https://egghead.io/courses/build-an-eleventy-11ty-site-from-scratch-bfd3">Build An Eleventy (11ty) Site From Scratch</a> by <a href="https://www.11ty.dev/authors/5t3ph/">Stephanie Eckles</a> was an incredible introduction to helping me understand the basics.</p> <h2 id="why-choose-eleventy">Why choose Eleventy?</h2> <p>Most of my curiosity around 11ty stems from the amount of positive feedback and love I see for this project around the web. It's decoupled from the rest of my tech stack and tooling. It works with multiple template engines (using markdown and nunjucks here). Spinning up dynamic pages from external data is no sweat. There are <em>no</em> client-side javascript dependencies. Instead of me going on about it, check out the <a href="https://www.11ty.dev/docs/">11ty Docs</a> or amazing resources like <a href="https://11ty.rocks/">11ty.rocks</a> and <a href="https://11ty.recipes/">11ty.recipes</a>.</p> <p>On top of all that, the community is made up of really stellar people that I admire. People that care about a performant and inclusive web. I'll always be sold on that.</p> <h2 id="whats-next-then">What's next then?</h2> <p>This only marks the beginning of this website's evolution. It will probably be a slow burn. Which is fine. Getting set up on 11ty gives me the opportunity to spin up content quickly.</p> <p>I have often wanted to write small articles around front-end techniques that have been helpful for me and share them with the community. It will also be nice to have a space where I can look up solutions I've since forgotten. I haven't been compelled to set up that space until now. The desire to mess around with 11ty gave me the push I needed.</p> <p>Be on the lookout for small snippets and chunks of thoughts. Looking forward to sharing with you all and getting feedback on improvements or alternatives.</p> A Horizontal Scroll List and Custom Keyboard Navigation 2021-11-15T00:00:00Z https://ryanmulligan.dev/blog/project-keyboard-navigation/ <h2 id="getting-started">Getting started</h2> <p>It was time for a personal site refresh. I didn't plan much for this next iteration, but I knew I wanted to include a showcase of <a href="https://codepen.io/hexagoncircle">my CodePen projects</a>. With so many to choose from, it was tough deciding on how I'd ultimately like to visibly display project content. To kick things off, a list of linked cards presented in a horizontal scroll container felt worthy of exploring.</p> <p>The argument against a carousel-style UX popped in my head, naturally, and maybe I <a href="https://shouldiuseacarousel.com/">should not use a carousel</a>. However, I wanted to try this aesthetic with the following scope in mind:</p> <ul> <li>An inline overflow of multiple cards provides context for scrolling the x-axis of this section. <em>Although</em>, I do recognize some edge case viewport widths where this may not seem overtly obvious.</li> <li>When hovering this project section, a scroll bar will appear as another indication that content is horizontally scrollable.</li> <li>This layout can be achieved with just HTML and CSS, even utilize <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Scroll_Snap">scroll snap properties</a>.</li> <li>It creates a similar experience across all modern input devices and screen sizes.</li> <li>We can quickly scan down to the next article on the page without scrolling through a list of 40+ cards, e.g. displayed in a layout such as a traditional responsive grid.</li> </ul> <p>Below is a stripped-down CodePen demo focused on layout and keyboard navigation criteria:</p> <p class="codepen" data-height="600" data-preview="false" data-default-tab="result" data-slug-hash="QWMZBve" data-user="hexagoncircle"> <a href="https://codepen.io/hexagoncircle/pen/QWMZBve"> <svg class="icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512" fill="currentcolor"><path d="M502.285 159.704l-234-156c-7.987-4.915-16.511-4.96-24.571 0l-234 156C3.714 163.703 0 170.847 0 177.989v155.999c0 7.143 3.714 14.286 9.715 18.286l234 156.022c7.987 4.915 16.511 4.96 24.571 0l234-156.022c6-3.999 9.715-11.143 9.715-18.286V177.989c-.001-7.142-3.715-14.286-9.716-18.285zM278 63.131l172.286 114.858-76.857 51.429L278 165.703V63.131zm-44 0v102.572l-95.429 63.715-76.857-51.429L234 63.131zM44 219.132l55.143 36.857L44 292.846v-73.714zm190 229.715L61.714 333.989l76.857-51.429L234 346.275v102.572zm22-140.858l-77.715-52 77.715-52 77.715 52-77.715 52zm22 140.858V346.275l95.429-63.715 76.857 51.429L278 448.847zm190-156.001l-55.143-36.857L468 219.132v73.714z"></path></svg> <span>Open CodePen demo</span> </a> </p> <script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script> <p>Overall, I'd consider this a horizontal scroll container. Not <em>really</em> a carousel. If you're shaking your head, disagree, and have feedback already, I'm looking forward to it! For the sake of getting to the real purpose of this article, let's read on.</p> <h2 id="user-flow-on-a-keyboard">User flow on a keyboard</h2> <p>After building out the page structure, I tried navigating <a href="https://ryanmulligan.dev/">the site homepage</a> using my keyboard. I quickly noticed how tedious it was tabbing through every single one of those CodePen project links. Perhaps there's a way to make this interaction and page flow feel more seamless.</p> <aside class="callout"><p>Experts in the accessibility community may have some really helpful feedback and guidance on these ideas. If you're one of them, I'd love to get your thoughts and update this article accordingly! You can <a href="https://twitter.com/hexagoncircle">message me on Twitter</a> or <a href="https://ryanmulligan.dev/blog/project-keyboard-navigation/&#109;a&#105;lto&#58;&#104;%65y&#64;%72%79&#37;61%6E&#37;6D%75&#37;&#54;Clig%61&#110;&#46;&#100;&#101;v?subject=A%20Horizontal%20Scroll%20List%20and%20Custom%20Keyboard%20Navigation" target="_blank" rel="noopener">email me</a>.</p> </aside><p>Let's jump into some solutions. The following is what I had tried with the latter option being the path forward.</p> <h3 id="skip-links">Skip links</h3> <p>My first solution was to introduce a &quot;skip to next section&quot; anchor element that would be focused prior to entering the project list. It's similar to <a href="https://www.a11ymatters.com/pattern/skip-link/">skip navigation links</a>, a common pattern for keyboard navigation and screen readers that allow us to jump directly to the site's main content area.</p> <p>While inactive, this anchor element is visually hidden on the page. Once focused, the link appears on screen. We can then press the <code>enter</code> key and skip over these projects to the next page section containing the <code>id</code> used in the <code>href</code> attribute.</p> <p>Using <code>shift + tab</code> to navigate back up the page will surface the same issue in reverse. At this point, I debated appending a skip link to the end of the project list. Doing so would lead to something like this:</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>section</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>above-section<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token comment">&lt;!-- section content --></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>section</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#below-section<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Skip project section<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>projects<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token comment">&lt;!-- 40+ links --></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#above-section<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Skip project section<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>section</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>below-section<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token comment">&lt;!-- section content --></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>section</span><span class="token punctuation">></span></span></code></pre> <p>Hmm. This seems somewhat restrictive and may be confusing. Let's explore a different way to handle this navigation instead of sandwiching the component with these skip elements.</p> <h3 id="custom-keyboard-control">Custom keyboard control</h3> <p>This iteration explores setting focus on the project list element with <a href="https://www.a11yproject.com/posts/2021-01-28-how-to-use-the-tabindex-attribute/">tabindex</a>. By customizing the <code>tabindex</code> on this component, we now have the choice of interacting with this list of links or jumping to the next focusable element on the page.</p> <p>Here's how it works:</p> <ul> <li>The script is initialized, and <code>tabindex=&quot;0&quot;</code> is applied to the project list. This adds it as a focusable element in the document's source order.</li> <li><code>tabindex=&quot;-1&quot;</code> is set on every project link making them unreachable through sequential keyboard navigation.</li> <li>When the list element is focused, the left and right arrow keys become activated to traverse its links.</li> <li>The right arrow jumps to the next project link in sequence until it reaches the end, then loops back to the beginning of the list.</li> <li>The left arrow focuses the previous project link until it reaches the first item, then it jumps to the end of the list and continues working backwards.</li> </ul> <p>In an effort to better surface this interaction, helper text is inserted into the <abbr title="Document Object Model">DOM</abbr> when the container focus is visible. My screen reader testing has been limited to Voiceover on macOS at the time of writing this article, but it's good to note that with Voiceover enabled, we are given feedback on how to traverse the list using built-in keyboard shortcuts.</p> <figure><picture><source type="image/webp" srcset="https://ryanmulligan.dev/images/vES9H6z2Uz-100.webp 100w, https://ryanmulligan.dev/images/vES9H6z2Uz-400.webp 400w, https://ryanmulligan.dev/images/vES9H6z2Uz-800.webp 800w, https://ryanmulligan.dev/images/vES9H6z2Uz-1280.webp 1280w" sizes="100vw" /><img alt="A screenshot of the projects list focused and the Voiceover notification" src="https://ryanmulligan.dev/images/vES9H6z2Uz-100.jpeg" width="1280" height="925" srcset="https://ryanmulligan.dev/images/vES9H6z2Uz-100.jpeg 100w, https://ryanmulligan.dev/images/vES9H6z2Uz-400.jpeg 400w, https://ryanmulligan.dev/images/vES9H6z2Uz-800.jpeg 800w, https://ryanmulligan.dev/images/vES9H6z2Uz-1280.jpeg 1280w" sizes="100vw" /></picture><figcaption>An example of the voiceover notification that reads, 'You are currently on a list. To move between items in this list, press Control-Option-Right Arrow or Control-Option-Left Arrow.'</figcaption></figure> <p>One final tweak: Elements now scroll completely into view when focused. Without this bit of code, it was possible to focus an element overflowing the boundary of the viewport but it did not pull it all the way on screen. Combining the <code>scrollIntoView</code> method with a programmatic focus improves this flow:</p> <pre class="language-js"><code class="language-js"><span class="token keyword">const</span> reducedMotion <span class="token operator">=</span> window<span class="token punctuation">.</span><span class="token function">matchMedia</span><span class="token punctuation">(</span><span class="token string">"(prefers-reduced-motion: reduce)"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//...</span> selected<span class="token punctuation">.</span><span class="token function">scrollIntoView</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">block</span><span class="token operator">:</span> <span class="token string">"nearest"</span><span class="token punctuation">,</span> <span class="token literal-property property">inline</span><span class="token operator">:</span> <span class="token string">"start"</span><span class="token punctuation">,</span> <span class="token literal-property property">behavior</span><span class="token operator">:</span> reducedMotion<span class="token punctuation">.</span>matches <span class="token operator">?</span> <span class="token string">"auto"</span> <span class="token operator">:</span> <span class="token string">"smooth"</span><span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> selected<span class="token punctuation">.</span><span class="token function">focus</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">preventScroll</span><span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre> <p>Notice that a <code>prefers-reduced-motion</code> conditional is applied to the <code>behavior</code> option. This will respect our reduced motion settings and disable smooth scrolling of the list.</p> <aside class="callout"><p>To review all the code used in setting up these custom keyboard interactions, scroll back up to the embedded CodePen example from earlier in this article and select the JS tab.</p> </aside><h2 id="when-java-script-is-disabled">When JavaScript is disabled</h2> <p>This layout works as intended without JavaScript. The level of control I've added attempts to make it easier to interact with this component, but content is still navigable without it. You can give it a shot by disabling JavaScript in your browser settings. Navigating with your keyboard still works; You'll just have to tab through every project in the list. Mouse and touch scrolling are no different.</p> <h2 id="whats-your-take">What's your take?</h2> <p>I've made quite a few assumptions here. Does this feel intuitive when navigating using a keyboard? Or is it possible that this may diminish the default flow? Your feedback will help me improve this experience or think about this component behavior differently. <a href="https://twitter.com/hexagoncircle">Reach out on Twitter</a> or <a href="https://ryanmulligan.dev/blog/project-keyboard-navigation/&#109;a&#105;lto&#58;&#104;%65y&#64;%72%79&#37;61%6E&#37;6D%75&#37;&#54;Clig%61&#110;&#46;&#100;&#101;v?subject=A%20Horizontal%20Scroll%20List%20and%20Custom%20Keyboard%20Navigation" target="_blank" rel="noopener">send me an email</a>.</p> <h3 id="helpful-resources">Helpful resources</h3> <p>Special thanks to my good friends that gave initial feedback in a draft of this article. Your help is very appreciated! Here are some other supportive resources:</p> <ul> <li><a href="https://www.a11yproject.com/posts/2021-01-28-how-to-use-the-tabindex-attribute/">Use the tabindex attribute - The A11Y Project</a></li> <li><a href="https://www.sarasoueidan.com/blog/keyboard-friendlier-article-listings/">Optimizing keyboard navigation using tabindex and ARIA</a></li> <li><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex">tabindex - HTML: HyperText Markup Language – MDN</a></li> </ul> Animating with the Flip Plugin for GSAP 2022-01-07T00:00:00Z https://ryanmulligan.dev/blog/gsap-flip-cart/ <h2 id="what-the-flip-is-it">What the flip is it?</h2> <p>Every time a new <a href="https://gsap.com/">GSAP</a> plugin is introduced, I'm close to bursting from excitement. The simplicity of the GreenSock API makes learning and applying these tools in projects such a dream. I had the pleasure of beta testing the <a href="https://gsap.com/scrolltrigger/">ScrollTrigger plugin</a> and was blown away by how easily I was able to dive in and start creating.</p> <p>The <a href="https://gsap.com/docs/v3/Plugins/Flip">Flip plugin</a> is no different. And how about this? As of the <a href="https://gsap.com/3-9/">3.9 release</a> (Dec 2021), it's no longer a members-only plugin. T'was a <a href="https://codepen.io/GreenSock/pen/NWadxaR">Merry Christmas</a> indeed!</p> <p>Before I continue, let's take a moment to celebrate the amazing GreenSock team for the incredible animation tools they provide for our web community. 🙏</p> <h2 id="the-technique">The technique</h2> <p>FLIP, coined by <a href="https://aerotwist.com/blog/flip-your-animations/">Paul Lewis</a>, is an acronym for First, Last, Invert, and Play. The Flip plugin harnesses this technique so that web developers can effortlessly and smoothly transition elements between states. Take it straight from <a href="https://gsap.com/docs/v3/Plugins/Flip">the plugin's introduction</a>:</p> <blockquote> <p>Flip records the current position/<wbr />size/<wbr />rotation of your elements, then you make whatever changes you want, and then Flip applies offsets to make them LOOK like they never moved/<wbr />resized/<wbr />rotated and then animates the removal of those offsets! UI transitions become remarkably simple to code. Flip does all the heavy lifting.</p> </blockquote> <p>I recommend reading <a href="https://gsap.com/docs/v3/Plugins/Flip">the docs</a> (always!), and watching that intro tutorial video (or jump straight down to their code examples if that's your fancy) to find out how you, too, can produce super sizzlin' layout animations with a minimal amount of code.</p> <h2 id="the-challenge">The challenge</h2> <p>The final week's prompt for the <a href="https://codepen.io/challenges/2021/december/4">December 2021 CodePen Challenge</a> involved using the FLIP technique. This couldn't have lined up more perfectly. The holidays had arrived. The office was quiet. I filled my coffee mug to its very top and, after a few hours of learning and experimentation, came up with this animation prototype:</p> <p class="codepen" data-height="600" data-preview="false" data-default-tab="result" data-slug-hash="RwLQLop" data-user="hexagoncircle"> <a href="https://codepen.io/hexagoncircle/pen/RwLQLop"> <svg class="icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512" fill="currentcolor"><path d="M502.285 159.704l-234-156c-7.987-4.915-16.511-4.96-24.571 0l-234 156C3.714 163.703 0 170.847 0 177.989v155.999c0 7.143 3.714 14.286 9.715 18.286l234 156.022c7.987 4.915 16.511 4.96 24.571 0l234-156.022c6-3.999 9.715-11.143 9.715-18.286V177.989c-.001-7.142-3.715-14.286-9.716-18.285zM278 63.131l172.286 114.858-76.857 51.429L278 165.703V63.131zm-44 0v102.572l-95.429 63.715-76.857-51.429L234 63.131zM44 219.132l55.143 36.857L44 292.846v-73.714zm190 229.715L61.714 333.989l76.857-51.429L234 346.275v102.572zm22-140.858l-77.715-52 77.715-52 77.715 52-77.715 52zm22 140.858V346.275l95.429-63.715 76.857 51.429L278 448.847zm190-156.001l-55.143-36.857L468 219.132v73.714z"></path></svg> <span>Open CodePen demo</span> </a> </p> <script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script> <p>In the above CodePen embed, click on a product item square and it will magically slingshot towards the cart button. Once the element reaches the end of its transition, it will be inserted into the cart alongside other selected products. Click on the cart button to pull its container into view with those selections. Inside this container, clicking items sends them back to their initial positions in the grid.</p> <p>Building this functionality without the Flip plugin would take quite a bit of time and strategy. GSAP just handles all of that critical code; the rest is left up to our wild imaginations!</p> <p>Let's get into some of the key features that bring this animation to life.</p> <h2 id="how-it-works">How it works</h2> <p>The &quot;Usage&quot; section of the <a href="https://gsap.com/docs/v3/Plugins/Flip">Flip plugin docs</a> breaks this down into three steps that are followed to execute this add-to-cart animation:</p> <ol> <li>Get the current state</li> <li>Make your state changes</li> <li>Call <code>Flip.from(state, options)</code></li> </ol> <h3 id="step-1-capture-the-state">Step 1: Capture the state</h3> <p>When an item is selected, Flip's <code>getState</code> method is called to collect data about the item's current size, position, rotation, and skew. This gets stored in a variable before applying other DOM edits, style changes, and so on.</p> <pre class="language-js"><code class="language-js"><span class="token keyword">const</span> state <span class="token operator">=</span> Flip<span class="token punctuation">.</span><span class="token function">getState</span><span class="token punctuation">(</span>item<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre> <aside class="callout"><p>The Flip plugin by default only records the following CSS properties: transforms (x, y, scaleX, scaleY, rotation, skewX), width, height, and opacity. However, it can be configured to affect others by adding a <code>props</code> property with a comma-delimited list of values in the <code>options</code> object. Learn more under the &quot;Usage&quot; section in <a href="https://gsap.com/docs/v3/Plugins/Flip">the docs</a>!</p> </aside><h3 id="step-2-make-the-changes">Step 2: Make the changes</h3> <p>After capturing the initial state data, the item gets appended as a child of the cart button.</p> <pre class="language-js"><code class="language-js">cartBtnWrapper<span class="token punctuation">.</span><span class="token function">appendChild</span><span class="token punctuation">(</span>item<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre> <h3 id="step-3-flip-it">Step 3: FLIP it!</h3> <p>The selected item is ready to animate from its current grid position over to the cart button. Time for the Flip plugin to dazzle us all with its magic. ✨</p> <pre class="language-js"><code class="language-js">Flip<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span>state<span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">duration</span><span class="token operator">:</span> reducedMotion <span class="token operator">?</span> <span class="token number">0</span> <span class="token operator">:</span> <span class="token number">0.5</span><span class="token punctuation">,</span> <span class="token literal-property property">ease</span><span class="token operator">:</span> <span class="token string">"back.in(0.8)"</span><span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre> <p>Flip checks out the stored <code>state</code> object, compares it to the item's current state data, and immediately sets the position and size so that the item appears to still exist in its grid placement. Then the item transitions to its <em>actual</em> placement inside the button by animating the removal of these position and size offset values.</p> <p>I did nearly nothing here. This is all GSAP Flip sorcery. My goodness it's good.</p> <aside class="callout"><p>You might be wondering about the <code>reducedMotion</code> variable; review its value in the full version of the JavaScript code (click the JS tab in the CodePen embed above). It detects if a user has requested less movement on screen. If true, the item will be instantly added to the cart instead of animating across the page. Learn more about <code>prefers-reduced-motion</code> in <a href="https://web.dev/prefers-reduced-motion/">this web.dev article</a>.</p> </aside><p>In order to get the item to move into the cart once the animation has finished, the <code>onComplete</code> callback is used to append the item as a child.</p> <pre class="language-js"><code class="language-js">Flip<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span>state<span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">duration</span><span class="token operator">:</span> reducedMotion <span class="token operator">?</span> <span class="token number">0</span> <span class="token operator">:</span> <span class="token number">0.5</span><span class="token punctuation">,</span> <span class="token literal-property property">ease</span><span class="token operator">:</span> <span class="token string">"back.in(0.8)"</span><span class="token punctuation">,</span> <span class="token function-variable function">onComplete</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> cartItems<span class="token punctuation">.</span><span class="token function">appendChild</span><span class="token punctuation">(</span>item<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre> <p>After that, other animations are run such as sliding the item into place and the acrobatic front flip of the count badge. This project is <em>all</em> about the flips. Be sure to jump into the <a href="https://ryanmulligan.dev/blog/gsap-flip-cart/#codepen-demo">full JS code</a> for those implementation details.</p> <h2 id="wrapping-up">Wrapping up</h2> <p>This experiment seems like it only just begins to harness the superpower supplied by the GSAP Flip plugin. I'm looking forward to seeing how you all utilize this in projects. As always, with this great power comes a lot of responsibility. Consider folks that prefer reduced motion or how larger layout animations could affect the overall experience.</p> <p>Friendly feedback forever welcome. Share with me on <a href="https://fosstodon.org/@hexagoncircle">Mastodon</a>.</p> <h3 id="helpful-resources">Helpful resources</h3> <ul> <li><a href="https://gsap.com/docs/v3/Plugins/Flip">GSAP Flip plugin docs</a></li> <li><a href="https://codepen.io/collection/AEkJmd">Flip showcase</a></li> <li><a href="https://codepen.io/collection/nqvwmG">Flip how-to demos</a></li> <li><a href="https://web.dev/prefers-reduced-motion/">prefers-reduced-motion: Sometimes less movement is more</a></li> </ul> Website Themes and Color Schemes 2022-02-01T00:00:00Z https://ryanmulligan.dev/blog/themes-and-schemes/ <h2 id="getting-into-the-mode">Getting into the mode</h2> <p>Before we begin, I'd like to preface this article with the following resources that were helpful guides on my theming quest. They explain a lot of the intricacies of setting up dark mode and I recommend reading them before my own.</p> <ul> <li><a href="https://css-tricks.com/a-complete-guide-to-dark-mode-on-the-web/">A Complete Guide to Dark Mode on the Web</a></li> <li><a href="https://css-irl.info/quick-and-easy-dark-mode-with-css-custom-properties/">Quick and Easy Dark Mode with CSS Custom Properties</a></li> <li><a href="https://piccalil.li/tutorial/create-a-user-controlled-dark-or-light-mode/">Create a user controlled dark or light mode</a></li> </ul> <p>Read those already? Skimmed them a fair amount at least? Fantastic. Let's jump into the theming details for this website.</p> <h2 id="range-of-styles">Range of styles</h2> <p>My first go-around with theme switching was handled with an HTML range input (dubbed <em>theme slider</em>). Each input value correlated to a CSS ruleset. Interacting with the theme slider did a couple things:</p> <ol> <li>Set a <code>data-theme</code> attribute on the <code>&lt;html&gt;</code> element.</li> <li>Saved the theme value to the browser's local storage to be referenced on subsequent site visits.</li> </ol> <p>Each theme changed the site colors set in CSS custom properties. I've simplified the code in these examples for the sake of brevity.</p> <pre class="language-css"><code class="language-css"><span class="token selector">[data-theme="1"]</span> <span class="token punctuation">{</span> <span class="token comment">/* dark theme */</span> <span class="token property">--color-text</span><span class="token punctuation">:</span> papayawhip<span class="token punctuation">;</span> <span class="token property">--color-bg</span><span class="token punctuation">:</span> midnightblue<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token comment">/* ...rulesets for themes 2 through 4... */</span> <span class="token selector">[data-theme="5"]</span> <span class="token punctuation">{</span> <span class="token comment">/* light theme */</span> <span class="token property">--color-text</span><span class="token punctuation">:</span> darkslategray<span class="token punctuation">;</span> <span class="token property">--color-bg</span><span class="token punctuation">:</span> lightsalmon<span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>The <code>min</code> and <code>max</code> attributes on the theme slider were set to 1 and 5 respectively, allowing five different themes. If the slider had not yet been moved, a theme was applied to the site based on color scheme preferences in a user's system settings. By default, color values were set to CSS custom properties. These values were then updated for dark mode within a <code>prefers-color-scheme</code> media query.</p> <aside class="callout"><p>Worth pointing out that it's totally possible to approach this the other way, starting with dark mode styles and overriding them with <code>light</code> or <code>no-preference</code> rulesets as Michelle explains in <a href="https://css-irl.info/quick-and-easy-dark-mode-with-css-custom-properties/">her article</a>.</p> </aside><p>In the following example, you'll notice that the base default colors are the same values in <code>data-theme=&quot;5&quot;</code> and then get updated to match <code>data-theme=&quot;1&quot;</code> for the dark color scheme preference.</p> <pre class="language-css"><code class="language-css"><span class="token selector">:root</span> <span class="token punctuation">{</span> <span class="token comment">/* same values used in theme 5 */</span> <span class="token property">--color-text</span><span class="token punctuation">:</span> darkslategray<span class="token punctuation">;</span> <span class="token property">--color-bg</span><span class="token punctuation">:</span> lightsalmon<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">prefers-color-scheme</span><span class="token punctuation">:</span> dark<span class="token punctuation">)</span></span> <span class="token punctuation">{</span> <span class="token comment">/* same values used in theme 1 */</span> <span class="token selector">:root</span> <span class="token punctuation">{</span> <span class="token property">--color-text</span><span class="token punctuation">:</span> papayawhip<span class="token punctuation">;</span> <span class="token property">--color-bg</span><span class="token punctuation">:</span> midnightblue<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre> <aside class="callout"><p>For visual history, I shared my site refresh and demonstrated the original theme slider implementation in <a href="https://twitter.com/hexagoncircle/status/1338885523658555394?s=20&amp;t=WebRdkKmXfB5ntsYPFwNwA">this tweet</a>. <em>Small note:</em> this was tweeted before I decided to ditch the old domain in favor of the one you're on now.</p> </aside><p>This first iteration felt limited. Preferred color scheme values were tied to specific themes (1 for dark, 5 for light/no-preference) and disconnected from the values sandwiched in between. It was a fine start but left me wondering about other ways to handle these preference settings.</p> <h2 id="decoupling-scheme-and-theme">Decoupling scheme and theme</h2> <p>When I first <a href="https://ryanmulligan.dev/blog/migrating-to-11ty">migrated over to 11ty</a> and added more pages to this site, the theme switcher was still only accessible on the homepage. While moving this component into a global layout, I was hit with some swell brain activity:</p> <blockquote> <p>Instead of relating light and dark mode settings to specific theme values on the slider, they could alter each theme as variants.</p> </blockquote> <p>🤯</p> <p>Here's what I came up with. The theme slider works the same as before but now has a new neighbor: a color scheme toggle button. This button sets a light or dark version of the current theme. My selection of colors may be somewhat arbitrary and subjective, but I tried pairing palettes that complement one another.</p> <p class="codepen" data-height="css,result" data-preview="false" data-default-tab="300" data-slug-hash="zYPrjNd" data-user="hexagoncircle"> <a href="https://codepen.io/hexagoncircle/pen/zYPrjNd"> <svg class="icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512" fill="currentcolor"><path d="M502.285 159.704l-234-156c-7.987-4.915-16.511-4.96-24.571 0l-234 156C3.714 163.703 0 170.847 0 177.989v155.999c0 7.143 3.714 14.286 9.715 18.286l234 156.022c7.987 4.915 16.511 4.96 24.571 0l234-156.022c6-3.999 9.715-11.143 9.715-18.286V177.989c-.001-7.142-3.715-14.286-9.716-18.285zM278 63.131l172.286 114.858-76.857 51.429L278 165.703V63.131zm-44 0v102.572l-95.429 63.715-76.857-51.429L234 63.131zM44 219.132l55.143 36.857L44 292.846v-73.714zm190 229.715L61.714 333.989l76.857-51.429L234 346.275v102.572zm22-140.858l-77.715-52 77.715-52 77.715 52-77.715 52zm22 140.858V346.275l95.429-63.715 76.857 51.429L278 448.847zm190-156.001l-55.143-36.857L468 219.132v73.714z"></path></svg> <span>Open CodePen demo</span> </a> </p> <script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script> <p>What I like about this theming model is that it welcomes future variants based on other user preferences and system settings. For instance, introducing high and low contrast styles for each theme using the <code>prefers-contrast</code> media query.</p> <aside class="callout"><p>At the time of writing, <code>prefers-contrast</code> is still considered an experimental feature according to the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-contrast">MDN docs</a>. This behavior may possibly change in the future.</p> </aside><h2 id="redundant-rulesets">Redundant rulesets</h2> <p>One minor issue is that the same set of styles need to be declared twice for an initial theme to handle dark mode as both a system setting and user-selected preference. Since I'm using Sass, I've abstracted the values into a mixin to avoid the repetition. Below is a reduced example; Review the SCSS tab in the CodePen above for the full code.</p> <pre class="language-scss"><code class="language-scss"><span class="token keyword">@mixin</span> <span class="token selector">color-scheme-dark </span><span class="token punctuation">{</span> <span class="token property">--color-text</span><span class="token punctuation">:</span> papayawhip<span class="token punctuation">;</span> <span class="token property">--color-bg</span><span class="token punctuation">:</span> midnightblue<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">:root </span><span class="token punctuation">{</span> <span class="token property">--color-text</span><span class="token punctuation">:</span> darkslategray<span class="token punctuation">;</span> <span class="token property">--color-bg</span><span class="token punctuation">:</span> lightsalmon<span class="token punctuation">;</span> <span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">prefers-color-scheme</span><span class="token punctuation">:</span> dark<span class="token punctuation">)</span></span> <span class="token punctuation">{</span> &amp;<span class="token punctuation">:</span><span class="token function">not</span><span class="token punctuation">(</span>[data-color-scheme]<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">@include</span> color-scheme-dark<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token selector"><span class="token parent important">&amp;</span>[data-color-scheme="dark"] </span><span class="token punctuation">{</span> <span class="token keyword">@include</span> color-scheme-dark<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre> <p>A hefty thanks to <a href="https://piccalil.li/tutorial/create-a-user-controlled-dark-or-light-mode/">Andy Bell's article</a> for its usage of the <code>:not</code> selector. This ensures that the system settings do not override the user-selected color scheme. It also helped reduce some of the redundant code.</p> <h2 id="keyboard-combo-control">Keyboard combo control</h2> <p>Keyboard navigation and interaction for these two elements works as one might suspect. The button toggles between dark and light mode. The range input updates the theme value. It's debatable that they are best left like that. However, I wanted to explore a version that combined these element interactions; A way to cycle through themes and toggle their light/dark variants as a single control.</p> <ul> <li><code>tabindex=&quot;-1&quot;</code> is set on the theme slider so it's no longer focusable in the keyboard tab sequence.</li> <li>When the color scheme toggle button is focused, left and right arrow keys cycle through the theme slider values.</li> <li>The space bar and enter key toggle light and dark mode.</li> <li>Visually, the toggle button has a focus outline with a left/right arrow icon beside it.</li> <li>A screen reader informs us that we can press the left and right arrow keys to change the theme in addition to our default button control.</li> </ul> <h2 id="theme-status">Theme status</h2> <p>One last feature is the usage of the status role, another gem from <a href="https://piccalil.li/tutorial/create-a-user-controlled-dark-or-light-mode/">Andy's article</a>. An HTML element with a <code>role=&quot;status&quot;</code> attribute is grouped next to each control. Although visually hidden, when the content inside these containers changes, assistive technology will relay that update back to us.</p> <ul> <li>Clicking the color scheme toggle button announces the change to light or dark mode.</li> <li>Changing the value in the theme slider announces the new theme being displayed.</li> </ul> <p>Something I enjoy about the latter bullet point is that it reveals the actual theme names which are based on ice cream flavors. Without inspecting code, it's currently the only path to this discovery. What's cooler than being cool? Ice cream.</p> <h2 id="ending-theme">Ending theme</h2> <p>Thanks for joining while I recounted the tale of my website's first theming trial and its follow-up adventure. Some of the patterns here may change over time but this has been a blast putting together. I'm no champion of color, but I think these are some good lookin' themes.</p> <p>If you have your own unique implementation or favorites out there on the wild web, <a href="https://twitter.com/hexagoncircle/status/1488589211577946114?s=20&amp;t=EldD8DIkTHYUdsKTsxJslQ">please share</a>! Max Böck and Josh Comeau have beautiful theme switchers and wrote detailed articles about their journeys. Definitely worth the read:</p> <ul> <li><a href="https://mxb.dev/blog/color-theme-switcher/">Color Theme Switcher</a></li> <li><a href="https://www.joshwcomeau.com/react/dark-mode/">The Quest for the Perfect Dark Mode</a></li> </ul> Horizontal Scrolling in a Centered Max-Width Container 2022-03-11T00:00:00Z https://ryanmulligan.dev/blog/x-scrolling-centered-max-width-container/ <h2 id="the-layout-challenge">The layout challenge</h2> <p>When I had first assembled a gallery of <a href="https://codepen.io/hexagoncircle">CodePen projects</a> to include on my personal site redesign in the summer of 2021, I imagined the following layout and interaction:</p> <ul> <li>The page's main content container is centered on the page with a max-width set.</li> <li>The first gallery item aligns with the left side of the content container.</li> <li>Items overflow to the right and beyond the viewport, indicating that it can be scrolled horizontally.</li> <li>Once scrolling is initiated, the left side of the gallery would break out of the content container, eventually sliding past the left edge of the viewport.</li> </ul> <p>This was a tough layout to get right! Ultimately, I decided to go with a slightly different <a href="https://ryanmulligan.dev/">homepage</a> design that didn't rely on aligning the start position inside the page content area.</p> <aside class="callout"><p>In case my site design has been updated, this is for my future friends reading: You can see the aforementioned version of my site in <a href="https://twitter.com/hexagoncircle/status/1338885523658555394?s=20&amp;t=u2zpk5LgvhQeV5_YwYB5rg">this tweet</a> from August 2021.</p> </aside><h2 id="revisiting-the-desired-result">Revisiting the desired result</h2> <p>The altered design worked well. But I still couldn't shake it. There had to be a way to build that original layout. Turns out nearly anything is possible with CSS these days. Here's a CodePen containing some gallery examples:</p> <p class="codepen" data-height="650" data-preview="false" data-default-tab="result" data-slug-hash="gOWjwme" data-user="hexagoncircle"> <a href="https://codepen.io/hexagoncircle/pen/gOWjwme"> <svg class="icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512" fill="currentcolor"><path d="M502.285 159.704l-234-156c-7.987-4.915-16.511-4.96-24.571 0l-234 156C3.714 163.703 0 170.847 0 177.989v155.999c0 7.143 3.714 14.286 9.715 18.286l234 156.022c7.987 4.915 16.511 4.96 24.571 0l234-156.022c6-3.999 9.715-11.143 9.715-18.286V177.989c-.001-7.142-3.715-14.286-9.716-18.285zM278 63.131l172.286 114.858-76.857 51.429L278 165.703V63.131zm-44 0v102.572l-95.429 63.715-76.857-51.429L234 63.131zM44 219.132l55.143 36.857L44 292.846v-73.714zm190 229.715L61.714 333.989l76.857-51.429L234 346.275v102.572zm22-140.858l-77.715-52 77.715-52 77.715 52-77.715 52zm22 140.858V346.275l95.429-63.715 76.857 51.429L278 448.847zm190-156.001l-55.143-36.857L468 219.132v73.714z"></path></svg> <span>Open CodePen demo</span> </a> </p> <script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script> <p>I shared <a href="https://twitter.com/hexagoncircle/status/1422559196088737797">this solution</a> on Twitter back before I had a blog space. Now that I do have one, I thought I'd take a deeper dive into how I achieved the final result.</p> <h2 id="using-a-full-bleed-layout">Using a full-bleed layout</h2> <p>Josh Comeau's <a href="https://www.joshwcomeau.com/css/full-bleed/">Full-Bleed Layout Using CSS Grid</a> is an article I reference often. It's a solid, modern approach to limit the maximum width of page content while allowing &quot;full-bleed&quot; elements such as images to stretch across the viewport width. This style of layout has been achieveable by other means as discussed at length in <a href="https://css-tricks.com/full-width-containers-limited-width-parents/">Full Width Containers in Limited Width Parents</a> on CSS-Tricks but I agree with Josh's sentiment about negative margin approaches being a bit hacky in comparison.</p> <p>The CodePen above follows Josh's <a href="https://www.joshwcomeau.com/css/full-bleed/#padding">padding example</a> but adds some named template areas which I'll explain:</p> <pre class="language-css"><code class="language-css"><span class="token selector">.content</span> <span class="token punctuation">{</span> <span class="token property">display</span><span class="token punctuation">:</span> grid<span class="token punctuation">;</span> <span class="token property">grid-template-columns</span><span class="token punctuation">:</span> [full-start] 1fr [content-start] <span class="token function">min</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--content-max-width<span class="token punctuation">)</span><span class="token punctuation">,</span> 100% - <span class="token function">var</span><span class="token punctuation">(</span>--space-md<span class="token punctuation">)</span> * 2<span class="token punctuation">)</span> [content-end] 1fr [full-end]<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">.content > *</span> <span class="token punctuation">{</span> <span class="token property">grid-column</span><span class="token punctuation">:</span> content<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">.gallery</span> <span class="token punctuation">{</span> <span class="token property">grid-column</span><span class="token punctuation">:</span> full<span class="token punctuation">;</span> <span class="token comment">/* other gallery code */</span> <span class="token punctuation">}</span></code></pre> <p>The first and third columns are set to <code>1fr</code>, causing them to fill the space surrounding either side of the second. The value of the second column is calculated by a CSS <code>min()</code> function, which selects the smaller of its two values depending on the window size. On screensizes smaller than <code>--content-max-width</code>, padding is created on either side by doubling a space value and subtracting it from 100% to suppress any unwanted page overflow.</p> <aside class="callout"><p>Something to note is that <code>calc()</code> can be used but is not necessary for calculations written inside <code>min()</code>, <code>max()</code>, and <code>clamp()</code> functions.</p> </aside><p>A noticeable difference in this code are the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout/Layout_using_Named_Grid_Lines">named grid lines</a> declared in <code>grid-template-columns</code>. Appending <code>-start</code> and <code>-end</code> creates a named area (or <a href="https://drafts.csswg.org/css-values-4/#custom-idents">custom-ident</a>) that can be referenced in a child element's <code>grid-column</code> property. When applied, an element will span the area between these two lines.</p> <ul> <li><code>content</code> becomes an identifier for the page content area. It will fill the second column of the grid and is the same as declaring <code>grid-column: 2</code>.</li> <li>The <code>full</code> identifier stretches across all the columns. This is equal to <code>grid-column: 1 / -1</code>.</li> </ul> <p>This approach removes the need for a &quot;full-bleed&quot; utility class on HTML elements. Instead, <code>full</code> and <code>content</code> become reusable values in the CSS for child elements when <code>grid-column</code> is declared. If the columns template should change at all (adding additional values, adjusting sizes) the named areas stay the same.</p> <h2 id="creating-the-gallery-styles">Creating the gallery styles</h2> <p>With the page layout finished, we can move on to the gallery component, starting on the top-level gallery element:</p> <pre class="language-css"><code class="language-css"><span class="token selector">.gallery</span> <span class="token punctuation">{</span> <span class="token property">grid-column</span><span class="token punctuation">:</span> full<span class="token punctuation">;</span> <span class="token property">display</span><span class="token punctuation">:</span> grid<span class="token punctuation">;</span> <span class="token property">grid-template-columns</span><span class="token punctuation">:</span> inherit<span class="token punctuation">;</span> <span class="token property">padding-block</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--gap<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">overflow-x</span><span class="token punctuation">:</span> scroll<span class="token punctuation">;</span> <span class="token property">overscroll-behavior-x</span><span class="token punctuation">:</span> contain<span class="token punctuation">;</span> <span class="token property">scroll-snap-type</span><span class="token punctuation">:</span> x mandatory<span class="token punctuation">;</span> <span class="token property">scrollbar-width</span><span class="token punctuation">:</span> none<span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>This is where scroll behavior and scroll snapping are handled, as well as stretching the viewport width. It inherits the <code>grid-columns-template</code> from the parent grid, acquiring the same column values and named grid lines.</p> <aside class="callout"><p><code>inherit</code> works as expected since the gallery spans the full row of the parent grid, so its column dimensions match. However, its grid is independent of the parent one, unlike <code>subgrid</code> which allows nested elements to utilize the parent grid. <a href="https://www.annalytic.com/css-subgrid-vs-nested-grid.html">This article</a> by Anna Monus explains it well. <a href="https://www.smashingmagazine.com/2022/03/new-css-features-2022/#subgrid">CSS Subgrid</a> support is very low at the time of writing.</p> </aside><p>In browser developer tools, we can enable layout grid lines visually and get a sense of how it's all working. I'm using Chrome dev tools in the screenshot below but Firefox and Safari share similar steps.</p> <ul> <li>Open the <em>Layout</em> panel and select &quot;Show line names&quot; from the <em>Overlay display settings</em> dropdown.</li> <li>In the <em>Elements</em> panel, click on the <code>grid</code> pills to the right of the main content and gallery elements to toggle their grid line visibility.</li> </ul> <figure><picture><source type="image/webp" srcset="https://ryanmulligan.dev/images/7blpR60R1Y-100.webp 100w, https://ryanmulligan.dev/images/7blpR60R1Y-400.webp 400w, https://ryanmulligan.dev/images/7blpR60R1Y-800.webp 800w, https://ryanmulligan.dev/images/7blpR60R1Y-1280.webp 1280w" sizes="100vw" /><img alt="Screenshot of dev tools showing the overlap of the gallery's grid lines on top of the parent container's grid lines." src="https://ryanmulligan.dev/images/7blpR60R1Y-100.jpeg" width="1280" height="813" srcset="https://ryanmulligan.dev/images/7blpR60R1Y-100.jpeg 100w, https://ryanmulligan.dev/images/7blpR60R1Y-400.jpeg 400w, https://ryanmulligan.dev/images/7blpR60R1Y-800.jpeg 800w, https://ryanmulligan.dev/images/7blpR60R1Y-1280.jpeg 1280w" sizes="100vw" /></picture><figcaption>Dev tools can be used to visualize the overlap of the gallery's grid lines on top of the parent container's grid lines.</figcaption></figure> <h3 id="the-inner-wrapper">The inner wrapper</h3> <p>In order to align the initial project item to the page content area, a wrapper element surrounds the project items and has <code>grid-column: content</code> declared. Remember that the gallery inherits <code>grid-template-column</code> from its parent so the named area identifiers are available.</p> <pre class="language-css"><code class="language-css"><span class="token selector">.gallery .wrapper</span> <span class="token punctuation">{</span> <span class="token property">grid-column</span><span class="token punctuation">:</span> content<span class="token punctuation">;</span> <span class="token property">display</span><span class="token punctuation">:</span> flex<span class="token punctuation">;</span> <span class="token property">align-items</span><span class="token punctuation">:</span> center<span class="token punctuation">;</span> <span class="token property">gap</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--space<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">.gallery .wrapper::after</span> <span class="token punctuation">{</span> <span class="token property">content</span><span class="token punctuation">:</span> <span class="token string">""</span><span class="token punctuation">;</span> <span class="token property">align-self</span><span class="token punctuation">:</span> stretch<span class="token punctuation">;</span> <span class="token property">padding-inline-end</span><span class="token punctuation">:</span> <span class="token function">max</span><span class="token punctuation">(</span> <span class="token function">var</span><span class="token punctuation">(</span>--space<span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">(</span>100vw - <span class="token function">var</span><span class="token punctuation">(</span>--content-max-width<span class="token punctuation">)</span><span class="token punctuation">)</span> / 2 - <span class="token function">var</span><span class="token punctuation">(</span>--space<span class="token punctuation">)</span> <span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>A flex display is applied to the wrapper so that its children line up in a single row. The <code>gap</code> property adds the gutters between each child.</p> <p>The wrapper also introduces a pseudo element as a spacer after the last project item to keep it from stopping right on the viewport edge. To make my original spacer code even better, Maarten Bruggink shared <a href="https://twitter.com/maartenbruggink/status/1422641189732462594?s=20&amp;t=05DWyNWsJX9CLrq9GQPuKQ">a fantastic suggestion</a> that supports scrolling until the last element aligns to the right side of the page content area, even on larger screensizes. 👏</p> <h3 id="the-projects">The projects</h3> <p>Adding <code>flex-shrink: 0</code> on project items keeps them from collapsing to fit within the gallery wrapper. I've applied a combination of inline-sizing and aspect-ratio to projects in this demo so that they share the same responsive dimensions. It's not required though! Depending on what the gallery intends to accomplish, some project items could be wider, some tighter, and the layout would work as you'd expect. In the <a href="https://ryanmulligan.dev/blog/x-scrolling-centered-max-width-container/#codepen-demo">CodePen demo</a>, scroll down a bit for an example.</p> <h2 id="a-fun-scroll-snap-tidbit">A fun scroll snap tidbit</h2> <p>Something that I found really interesting: <code>scroll-snap-align</code> can be declared on nested elements! Notice that <code>scroll-snap-align: center</code> is set on project items. Although, while this works nicely for the <code>center</code> value, the result is not what you might hope for when using <code>start</code> or <code>end</code>. The elements are aligning to the scroll container edges of the gallery, which handles the scroll snap positioning, not the wrapper.</p> <h2 id="reverse-scroll-direction">Reverse scroll direction</h2> <p>Scroll direction is handled quite gracefully. For languages that read from right to left, project items will be flipped appropriately thanks to their parent wrapper's flexbox display. The first item aligns to the right edge of the page content area and the gallery scrolls in from the left. Check the <a href="https://ryanmulligan.dev/blog/x-scrolling-centered-max-width-container/#codepen-demo">CodePen demo</a> for an example of this further down the page.</p> <p>For more information on this, <a href="https://rtlstyling.com/">RTL Styling 101</a> is an excellent guide. I recommend the <a href="https://rtlstyling.com/posts/rtl-styling#flexbox-layout-module">Flexbox Layout Module</a> section to learn more about flexbox and right-to-left styling.</p> <h2 id="css-is-awesome">CSS is awesome</h2> <p>CSS Grid and Flexbox open the doors to so many layout patterns that, not long ago, were nothing but impossible to produce without leaning into JavaScript. There are so many more exciting <a href="https://www.smashingmagazine.com/2022/03/new-css-features-2022/">new features coming</a> to CSS that will continue to push the boundaries of what we can create. If <a href="https://css-tricks.com/css-cascade-layers/">CSS Cascade Layers</a> are any indication, browser teams are working hard on implementing these features faster than ever.</p> <h2 id="helpful-resources">Helpful resources</h2> <ul> <li><a href="https://www.bram.us/2021/05/06/css-full-bleed-scroll-snapping-carousel-with-visible-overflow/">CSS Full-Bleed Scroll-Snapping Carousel with Centered Content and Visible Overflow</a></li> <li><a href="https://www.joshwcomeau.com/css/full-bleed/">Full-Bleed Layout Using CSS Grid</a></li> <li><a href="https://ryanmulligan.dev/blog/project-keyboard-navigation/">A Horizontal Scroll List and Custom Keyboard Navigation</a></li> <li><a href="https://web.dev/min-max-clamp/">min(), max(), and clamp(): three logical CSS functions to use today</a></li> <li><a href="https://www.annalytic.com/css-subgrid-vs-nested-grid.html">CSS subgrid vs nested grid — are they the same?</a></li> <li><a href="https://rtlstyling.com/">RTL Styling 101</a></li> </ul> Inverted Media Queries and Breakpoints 2022-07-05T00:00:00Z https://ryanmulligan.dev/blog/invert-media-queries/ <h2 id="the-occasional-breakpoint">The occasional breakpoint</h2> <p>Nowadays I lean on <a href="https://moderncss.dev/contextual-spacing-for-intrinsic-web-design/">modern CSS solutions</a>, <a href="https://css-tricks.com/responsive-layouts-fewer-media-queries/">fluid layout patterns</a>, and <a href="https://ishadeed.com/article/intrinsic-sizing-in-css/">intrinsic sizing</a> over viewport dimension-based media queries – typically referred to as <em>breakpoints</em> – that adapt a design at particular screen sizes. Let's not forget that <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Container_Queries">container queries</a> will soon join our CSS toolset, expanding the exciting universe of parent context styling.</p> <p>However, I do still find the occasional place to apply adaptive styles. Common example: a &quot;desktop&quot; menu (imagine a horizontal list of navigation items) that converts into its &quot;mobile&quot; counterpart (imagine a <a href="https://en.wikipedia.org/wiki/Hamburger_button">hamburger button</a> that toggles a vertically stacked menu's visibility). After building out the foundational styles, I often prefer separating adaptive CSS properties versus having to override or unset them. This means I end up with rulesets that might look like this:</p> <pre class="language-css"><code class="language-css"><span class="token selector">.menu</span> <span class="token punctuation">{</span> <span class="token comment">/* base styles */</span> <span class="token punctuation">}</span> <span class="token comment">/* below 600px */</span> <span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">max-width</span><span class="token punctuation">:</span> 599px<span class="token punctuation">)</span></span> <span class="token punctuation">{</span> <span class="token selector">.menu</span> <span class="token punctuation">{</span> <span class="token comment">/* narrow viewport styles */</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token comment">/* 600px and above */</span> <span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">min-width</span><span class="token punctuation">:</span> 600px<span class="token punctuation">)</span></span> <span class="token punctuation">{</span> <span class="token selector">.menu</span> <span class="token punctuation">{</span> <span class="token comment">/* wide viewport styles */</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre> <p>Notice the single pixel difference in the two values, <code>599px</code> and <code>600px</code>. If these min- and max-width queries shared the same value, then there would be a single pixel overlap where both styles would apply. Not ideal!</p> <h2 id="invert-the-media-query">Invert the media query</h2> <p>One way around this is to negate the media query. The <code>not</code> keyword in a media query will <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries#inverting_a_querys_meaning">invert the query's meaning</a>. To let both values in the previous example be the same, I could instead reuse a query and then invert it:</p> <pre class="language-css"><code class="language-css"><span class="token comment">/* below 600px */</span> <span class="token atrule"><span class="token rule">@media</span> <span class="token keyword">not</span> all <span class="token keyword">and</span> <span class="token punctuation">(</span><span class="token property">min-width</span><span class="token punctuation">:</span> 600px<span class="token punctuation">)</span></span> <span class="token punctuation">{</span> <span class="token comment">/* "... */</span> <span class="token punctuation">}</span> <span class="token comment">/* 600px and above */</span> <span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">min-width</span><span class="token punctuation">:</span> 600px<span class="token punctuation">)</span></span> <span class="token punctuation">{</span> <span class="token comment">/* "... */</span> <span class="token punctuation">}</span></code></pre> <aside class="callout"><p>The <code>not</code> keyword only seems to apply when first defining a <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries#targeting_media_types">media type</a> and then the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries#targeting_media_features">media feature</a> as I have above. Something like <code>@media not (min-width: 600px)</code> won't work.</p> </aside><p>If your current browser window is large enough, you can resize the CodePen result window below to see the text and background color change based on their respective media query declarations:</p> <p class="codepen" data-height="600" data-preview="false" data-default-tab="result" data-slug-hash="QWmbRXe" data-user="hexagoncircle"> <a href="https://codepen.io/hexagoncircle/pen/QWmbRXe"> <svg class="icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512" fill="currentcolor"><path d="M502.285 159.704l-234-156c-7.987-4.915-16.511-4.96-24.571 0l-234 156C3.714 163.703 0 170.847 0 177.989v155.999c0 7.143 3.714 14.286 9.715 18.286l234 156.022c7.987 4.915 16.511 4.96 24.571 0l234-156.022c6-3.999 9.715-11.143 9.715-18.286V177.989c-.001-7.142-3.715-14.286-9.716-18.285zM278 63.131l172.286 114.858-76.857 51.429L278 165.703V63.131zm-44 0v102.572l-95.429 63.715-76.857-51.429L234 63.131zM44 219.132l55.143 36.857L44 292.846v-73.714zm190 229.715L61.714 333.989l76.857-51.429L234 346.275v102.572zm22-140.858l-77.715-52 77.715-52 77.715 52-77.715 52zm22 140.858V346.275l95.429-63.715 76.857 51.429L278 448.847zm190-156.001l-55.143-36.857L468 219.132v73.714z"></path></svg> <span>Open CodePen demo</span> </a> </p> <script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script> <h2 id="future-css-solutions">Future CSS solutions</h2> <p>Level 4 <a href="https://www.bram.us/2021/10/26/media-queries-level-4-media-query-range-contexts/">media query range contexts</a>, which are getting closer to full modern browser support, will allow use of the same value:</p> <pre class="language-css"><code class="language-css"><span class="token comment">/* @media (max-width: 599px) becomes */</span> <span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span>width &lt; 600px<span class="token punctuation">)</span></span> <span class="token punctuation">{</span> <span class="token comment">/* ... */</span> <span class="token punctuation">}</span> <span class="token comment">/* @media (min-width: 600px) becomes */</span> <span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span>width >= 600px<span class="token punctuation">)</span></span> <span class="token punctuation">{</span> <span class="token comment">/* ... */</span> <span class="token punctuation">}</span></code></pre> <p>I really dig that syntax. I personally find it easier to understand and maintain.</p> <p>Another exciting solution involves <a href="https://www.stefanjudis.com/notes/can-we-have-custom-media-queries-please/">custom media queries</a>, which would allow us to store the media feature to a variable:</p> <pre class="language-css"><code class="language-css"><span class="token atrule"><span class="token rule">@custom-media</span> --breakpoint <span class="token punctuation">(</span><span class="token property">min-width</span><span class="token punctuation">:</span> 600px<span class="token punctuation">)</span><span class="token punctuation">;</span></span> <span class="token comment">/* below 600px */</span> <span class="token atrule"><span class="token rule">@media</span> <span class="token keyword">not</span> all <span class="token keyword">and</span> <span class="token punctuation">(</span>--breakpoint<span class="token punctuation">)</span></span> <span class="token punctuation">{</span> <span class="token comment">/* ... */</span> <span class="token punctuation">}</span> <span class="token comment">/* 600px and above */</span> <span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span>--breakpoint<span class="token punctuation">)</span></span> <span class="token punctuation">{</span> <span class="token comment">/* ... */</span> <span class="token punctuation">}</span></code></pre> <p>It seems that this won't be available for a while as it's in a draft of the <a href="https://drafts.csswg.org/mediaqueries-5/#custom-mq">level 5 media queries spec</a>, but the good news is that there's a <a href="https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-custom-media">PostCSS plugin</a> that give us this power today. 👏</p> <h2 id="helpful-resources">Helpful resources</h2> <ul> <li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries#combining_multiple_types_or_features">Using media queries</a></li> <li><a href="https://www.bram.us/2021/10/26/media-queries-level-4-media-query-range-contexts/">Media Queries Level 4: Media Query Range Contexts (Media Query Ranges)</a></li> <li><a href="https://www.stefanjudis.com/notes/can-we-have-custom-media-queries-please/">Can we have custom media queries, please?</a></li> <li><a href="https://css-tricks.com/logic-in-css-media-queries/">Logic in CSS Media Queries (If / Else / And / Or / Not)</a></li> </ul> The Infinite Marquee 2022-08-06T00:00:00Z https://ryanmulligan.dev/blog/css-marquee/ <h2 id="the-deprecated-tag">The deprecated tag</h2> <p>The HTML <code>&lt;marquee&gt;</code> element had blessed (cursed?) the early days of the internet with the ability to insert scrolling text onto a webpage. It even included options to control text behavior once it reached the end of its container with a handful of attributes. Review them <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/marquee">here on MDN</a> if you're curious. Also, when visiting that MDN link, notice the page starts with a deprecation warning that this feature is no longer recommended:</p> <blockquote> <p>Avoid using it, and update existing code if possible [...] Be aware that this feature may cease to work at any time.</p> </blockquote> <p>A handful of usability concerns led to <code>&lt;marquee&gt;</code> eventually being nixed. They can be too distracting, don't respect reduced-motion preferences, and in most cases render text unreadable. Things get really out of hand if there are multiple <code>&lt;marquee&gt;</code> visible on screen like <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/marquee#examples">this example</a> from MDN.</p> <p>Fun? Maybe. But maybe don't do that.</p> <h2 id="a-modern-approach">A modern approach</h2> <p>Now that we've gleaned a tiny slice of web history, it's arguable that a marquee-style animation can inject some pop to a page when done responsibly. Developers have discovered a few ways of reimagining the concept, the most popular accomplished with HTML and CSS. In this scenario, content is duplicated to create the illusion of it looping indefinitely. Here's a stripped-down example:</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>marquee<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>marquee__content<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span>Item 1<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span>Item 2<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span>Item 3<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">></span></span> <span class="token comment">&lt;!-- Mirrors the content above --></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>marquee__content<span class="token punctuation">"</span></span> <span class="token attr-name">aria-hidden</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>true<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span>Item 1<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span>Item 2<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span>Item 3<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span></code></pre> <aside class="callout"><p>Be sure to set <code>aria-hidden=&quot;true&quot;</code> to hide any repeated or redundant content from screen readers.</p> </aside><p>The marquee concept has been done plenty of times and may seem old hat. However, most of the examples I came across weren't fully responsive. Many rely on a fixed-width parent or having enough elements to overflow the container for a seamless loop. What if, when the parent container is wider than the content overflow, the items spread themselves out so that the loop works at any size? I experimented with a few ideas to see what's possible in making this concept more flexible.</p> <p>Here are the responsive styles that correspond to the HTML code block above:</p> <pre class="language-css"><code class="language-css"><span class="token selector">.marquee</span> <span class="token punctuation">{</span> <span class="token property">--gap</span><span class="token punctuation">:</span> 1rem<span class="token punctuation">;</span> <span class="token property">display</span><span class="token punctuation">:</span> flex<span class="token punctuation">;</span> <span class="token property">overflow</span><span class="token punctuation">:</span> hidden<span class="token punctuation">;</span> <span class="token property">user-select</span><span class="token punctuation">:</span> none<span class="token punctuation">;</span> <span class="token property">gap</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--gap<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">.marquee__content</span> <span class="token punctuation">{</span> <span class="token property">flex-shrink</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span> <span class="token property">display</span><span class="token punctuation">:</span> flex<span class="token punctuation">;</span> <span class="token property">justify-content</span><span class="token punctuation">:</span> space-around<span class="token punctuation">;</span> <span class="token property">min-width</span><span class="token punctuation">:</span> 100%<span class="token punctuation">;</span> <span class="token property">gap</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--gap<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>To get a better sense of what's happening, open up <a href="https://codepen.io/hexagoncircle/pen/eYMrGwW">this CodePen demo</a>. Try turning each CSS rule off and on to see how it affects the marquee. Adjust the amount of items in the marquee's HTML. Watch how they spread out as the viewport widens or naturally overflow as it narrows.</p> <p>Allow me to explain what this CSS is doing.</p> <ul> <li>A flexbox display is applied to both the <code>.marquee</code> parent and <code>.marquee__content</code> child containers. This places every item on a single row without any wrapping.</li> <li>There is a hidden overflow set on the parent. When the animation loops, the overflow conceals the elements snapping back to their start positions.</li> <li><code>user-select: none</code> disables highlighting or selecting text inside the marquee.</li> <li><code>flex-shrink: 0</code> prevents the child containers from shrinking, avoiding overlap of content.</li> <li><code>min-width: 100%</code> stretches each child container to the parent width. With this rule, the first child container is visible while the duplicate container is hidden in the overflow.</li> <li><code>justify-content: space-around</code> evenly distributes space between each child container item, then applies half of that as empty space before the first item and after the last.</li> </ul> <p>As items begin to overflow, gaps can be set to create room between each item. Gap values for the parent and child containers will need to match; Well that's a perfect case for defining a new <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties">CSS custom property</a>! The <code>gap: var(--gap)</code> declaration supplies the space between each item when content overflows the parent plus space between the two child containers. This variable also comes in handy to offset the end position in the animation precisely:</p> <pre class="language-css"><code class="language-css"><span class="token atrule"><span class="token rule">@keyframes</span> scroll</span> <span class="token punctuation">{</span> <span class="token selector">from</span> <span class="token punctuation">{</span> <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">translateX</span><span class="token punctuation">(</span>0<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">to</span> <span class="token punctuation">{</span> <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">translateX</span><span class="token punctuation">(</span><span class="token function">calc</span><span class="token punctuation">(</span>-100% - <span class="token function">var</span><span class="token punctuation">(</span>--gap<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre> <p>Without including <code>var(--gap)</code> in this calculation, there would be a visible misalignment when the animation loops. Try updating the value to <code>translateX(-100%)</code> to see the issue.</p> <p>The appearance of an infinite loop happens by animating the first child container completely out into the overflow while simultaneously pulling the duplicate container all the way into view. When the animation restarts, the first container picks up where the last left off. The illusion is complete! Yet it's also neverending... 😮</p> <h2 id="important-considerations">Important considerations</h2> <p>Really examine the use case for a marquee. They can be incredibly distracting and disorienting when implemented poorly.</p> <ul> <li>Use them sparingly. Overloading a page with a bunch of auto-scrolling areas is never a good time.</li> <li>Marquee content should be purely decorative. Leave out important page copy and focusable elements.</li> <li>Animation speeds should be slow. Content scrolling by super fast can be nauseating even for those that don't have reduced-motion enabled.</li> <li>Respect reduced-motion preferences. If set, best bet would be to completely disable auto-scrolling.</li> </ul> <h2 id="welcome-to-the-demo-zone">Welcome to the demo zone</h2> <p>Here are a couple of CodePen ideas I had thrown together while experimenting with marquee animations. The <a href="https://codepen.io/hexagoncircle/full/wvmjomb">logo wall</a> is especially fun, introducing reverse animations and the ability to toggle the axis for a vertical marquee.</p> <ul> <li><a href="https://codepen.io/hexagoncircle/full/wvmjomb">CSS Marquee Logo Wall</a></li> <li><a href="https://codepen.io/hexagoncircle/full/jOzZPJw">The Dogs of Unsplash</a></li> <li><a href="https://codepen.io/hexagoncircle/full/eYMrGwW">CSS Marquee Examples</a></li> </ul> <h2 id="explore-more-resources">Explore more resources</h2> <ul> <li><a href="https://dequeuniversity.com/rules/axe/4.1/marquee"><code>&lt;marquee&gt;</code> elements are deprecated and must not be used</a></li> <li><a href="https://tympanus.net/codrops/2020/03/31/css-only-marquee-effect/">CSS-Only Marquee Effect</a></li> <li><a href="https://olavihaapala.fi/2021/02/23/modern-marquee.html">Modern and Accessible <code>&lt;marquee&gt;</code> with TailwindCSS</a></li> </ul> Layout Breakouts with CSS Grid 2022-10-07T00:00:00Z https://ryanmulligan.dev/blog/layout-breakouts/ <h2 id="a-post-about-the-layout-you-re-looking-at-right-now">A post about the layout you're looking at right now</h2> <p>The previous structure of this page layout was virtually the same, the foundation of it expertly defined in the article <a href="https://www.joshwcomeau.com/css/full-bleed/">Full-Bleed Layout Using CSS Grid</a> by Josh Comeau. It's a technique I've used on many projects. I've even blogged about it previously in <a href="https://ryanmulligan.dev/blog/x-scrolling-centered-max-width-container/">Horizontal Scrolling in a Centered Max-Width Container</a>.</p> <p>What I'm documenting here is an extension of the full-bleed CSS Grid layout. In the last version of my site, selected elements – images, code blocks, quotes – were made wider than the page content area using negative margins. It worked well! For this next iteration, I explored applying these breakout offsets using CSS grid and <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout/Layout_using_Named_Grid_Lines">named grid lines</a>.</p> <h2 id="layout-setup">Layout setup</h2> <p>Below are the styles applied to the main content container, defining the grid display and its columns template:</p> <pre class="language-css"><code class="language-css"><span class="token selector">.content</span> <span class="token punctuation">{</span> <span class="token property">--gap</span><span class="token punctuation">:</span> <span class="token function">clamp</span><span class="token punctuation">(</span>1rem<span class="token punctuation">,</span> 6vw<span class="token punctuation">,</span> 3rem<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">--full</span><span class="token punctuation">:</span> <span class="token function">minmax</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--gap<span class="token punctuation">)</span><span class="token punctuation">,</span> 1fr<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">--content</span><span class="token punctuation">:</span> <span class="token function">min</span><span class="token punctuation">(</span>50ch<span class="token punctuation">,</span> 100% - <span class="token function">var</span><span class="token punctuation">(</span>--gap<span class="token punctuation">)</span> * 2<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">--popout</span><span class="token punctuation">:</span> <span class="token function">minmax</span><span class="token punctuation">(</span>0<span class="token punctuation">,</span> 2rem<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">--feature</span><span class="token punctuation">:</span> <span class="token function">minmax</span><span class="token punctuation">(</span>0<span class="token punctuation">,</span> 5rem<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">display</span><span class="token punctuation">:</span> grid<span class="token punctuation">;</span> <span class="token property">grid-template-columns</span><span class="token punctuation">:</span> [full-start] <span class="token function">var</span><span class="token punctuation">(</span>--full<span class="token punctuation">)</span> [feature-start] <span class="token function">var</span><span class="token punctuation">(</span>--feature<span class="token punctuation">)</span> [popout-start] <span class="token function">var</span><span class="token punctuation">(</span>--popout<span class="token punctuation">)</span> [content-start] <span class="token function">var</span><span class="token punctuation">(</span>--content<span class="token punctuation">)</span> [content-end] <span class="token function">var</span><span class="token punctuation">(</span>--popout<span class="token punctuation">)</span> [popout-end] <span class="token function">var</span><span class="token punctuation">(</span>--feature<span class="token punctuation">)</span> [feature-end] <span class="token function">var</span><span class="token punctuation">(</span>--full<span class="token punctuation">)</span> [full-end]<span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>In the <code>grid-template-columns</code> declaration, column areas are represented by keywords wrapped in square brackets suffixed with <code>-start</code> and <code>-end</code>. These keywords are set as <code>grid-column</code> values on elements residing in this container.</p> <p>Starting at the edge for an example: <code>[full-start]</code> and <code>[full-end]</code> represent the full-bleed. Any child element containing <code>grid-column: full;</code> will span its parent's available horizontal space.</p> <p>Each grid line is accompanied by a CSS variable of the same name, which supplies the inline size or width of the column. Outside of the center column block (the <code>content</code> area) that same variable is used after the <code>-start</code> and before the <code>-end</code> positions so their sizes match on either side. Continuing with the <code>full</code> keyword example, these values are <code>[full-start] var(--full)</code> and <code>var(--full) [full-end]</code>.</p> <p>I like to imagine each keyword's area blooms out from the center. <code>popout</code> grows out of <code>content</code>, <code>feature</code> from <code>popout</code>, then <code>full</code> blossoms all the way to the edge. The horizontal space each keyword covers is the sum of values between its <code>-start</code> and <code>-end</code> points.</p> <p>As a way to visualize this grid, I've created a fresh CodePen demo below. Click the &quot;show grid lines&quot; checkbox and resize the browser window to get a sense of how the layout expands and collapses.</p> <p class="codepen" data-height="600" data-preview="false" data-default-tab="result" data-slug-hash="dyejrpE" data-user="hexagoncircle"> <a href="https://codepen.io/hexagoncircle/pen/dyejrpE"> <svg class="icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512" fill="currentcolor"><path d="M502.285 159.704l-234-156c-7.987-4.915-16.511-4.96-24.571 0l-234 156C3.714 163.703 0 170.847 0 177.989v155.999c0 7.143 3.714 14.286 9.715 18.286l234 156.022c7.987 4.915 16.511 4.96 24.571 0l234-156.022c6-3.999 9.715-11.143 9.715-18.286V177.989c-.001-7.142-3.715-14.286-9.716-18.285zM278 63.131l172.286 114.858-76.857 51.429L278 165.703V63.131zm-44 0v102.572l-95.429 63.715-76.857-51.429L234 63.131zM44 219.132l55.143 36.857L44 292.846v-73.714zm190 229.715L61.714 333.989l76.857-51.429L234 346.275v102.572zm22-140.858l-77.715-52 77.715-52 77.715 52-77.715 52zm22 140.858V346.275l95.429-63.715 76.857 51.429L278 448.847zm190-156.001l-55.143-36.857L468 219.132v73.714z"></path></svg> <span>Open CodePen demo</span> </a> </p> <script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script> <aside class="callout"><p>Many modern browser developer tools include the ability to inspect CSS grid and display grid lines. Here's how to do it in <a href="https://developer.chrome.com/docs/devtools/css/grid/">Chrome</a>, <a href="https://firefox-source-docs.mozilla.org/devtools-user/page_inspector/how_to/examine_grid_layouts/index.html">Firefox</a>, and <a href="https://webkit.org/blog/11588/introducing-css-grid-inspector/">Safari</a>.</p> </aside><p>In the CSS tab of that demo, we can see how these grid areas are being applied. The first ruleset, <code>.content &gt; *</code>, matches all direct children of the container, setting them to the center <code>content</code> area. Cascading rulesets then revise <code>grid-column</code> with their respective keyword values.</p> <pre class="language-css"><code class="language-css"><span class="token selector">.content > *</span> <span class="token punctuation">{</span> <span class="token property">grid-column</span><span class="token punctuation">:</span> content<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">.popout</span> <span class="token punctuation">{</span> <span class="token property">grid-column</span><span class="token punctuation">:</span> popout<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">.feature</span> <span class="token punctuation">{</span> <span class="token property">grid-column</span><span class="token punctuation">:</span> feature<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">.full</span> <span class="token punctuation">{</span> <span class="token property">grid-column</span><span class="token punctuation">:</span> full<span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <h2 id="having-fun-with-sizing-functions">Having fun with sizing functions</h2> <p>Much of the real magic here is through the use of <code>minmax()</code>. It permits the flexible structure of this layout, elements breaking free on larger viewports and collapsing back in when less space is available. Or, as <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/minmax">MDN</a> and the CSS Spec describe it:</p> <blockquote> <p>The <code>minmax()</code> CSS function defines a size range greater than or equal to <em>min</em> and less than or equal to <em>max</em></p> </blockquote> <p>Let's revisit the CSS variables declared at the top of the ruleset. I'll explain how this all works in harmony.</p> <pre class="language-css"><code class="language-css"><span class="token property">--gap</span><span class="token punctuation">:</span> <span class="token function">clamp</span><span class="token punctuation">(</span>1rem<span class="token punctuation">,</span> 6vw<span class="token punctuation">,</span> 3rem<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">--full</span><span class="token punctuation">:</span> <span class="token function">minmax</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--gap<span class="token punctuation">)</span><span class="token punctuation">,</span> 1fr<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">--content</span><span class="token punctuation">:</span> <span class="token function">min</span><span class="token punctuation">(</span>50ch<span class="token punctuation">,</span> 100% - <span class="token function">var</span><span class="token punctuation">(</span>--gap<span class="token punctuation">)</span> * 2<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">--popout</span><span class="token punctuation">:</span> <span class="token function">minmax</span><span class="token punctuation">(</span>0<span class="token punctuation">,</span> 2rem<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">--feature</span><span class="token punctuation">:</span> <span class="token function">minmax</span><span class="token punctuation">(</span>0<span class="token punctuation">,</span> 5rem<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre> <ul> <li><code>--gap</code> represents a gutter size for the left and right sides of the page. This value leans into the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/clamp"><code>clamp()</code></a> function for more fluid, flexible sizing.</li> <li><code>--full</code> stretches an element so that it spans the entire horizontal space. By setting <code>--gap</code> as the <em>min</em> value, it also takes on the role of visible page gutters for smaller screens.</li> <li><code>--content</code> acts as the main content area. The <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/min"><code>min()</code></a> function sets the max-width of this column. Once the available space falls below this value, it then switches to 100% while also subtracting the left and right gutter sizes.</li> <li><code>--popout</code> and <code>--feature</code> extend elements beyond the content area by <code>2rem</code> and <code>5rem</code> respectively. As the available horizontal area tightens, these values collapse down to nothing, aligning elements with the main content space on smaller screens.</li> </ul> <h2 id="losing-floats">Losing floats</h2> <p>Alex Carpenter's <a href="https://twitter.com/hybrid_alex/status/1580173843267989506">tweet</a> exposes a limitation in this layout pattern: we lose the ability to float elements. For example, we wouldn't be able to float an image to the left and wrap text around it. The full-bleed solution from the CSS-Tricks article <a href="https://css-tricks.com/full-width-containers-limited-width-parents/#aa-no-calc-needed">Full Width Containers in Limited Width Parents</a> is handy in this situation.</p> <h2 id="breakout-session">Breakout session</h2> <p>That wraps things up! The potential of this concept doesn't stop here. How might you extend or refactor? Drop me a note on <a href="https://twitter.com/hexagoncircle">Twitter</a> with your awesome layout ideas. 🙌</p> <p><em>This article was updated on October 12th, 2022 to include the &quot;Losing Floats&quot; section.</em></p> <h2 id="helpful-resources">Helpful resources</h2> <ul> <li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout/Layout_using_Named_Grid_Lines">Grid layout using named grid lines</a></li> <li><a href="https://ishadeed.com/article/css-grid-minmax/">A Deep Dive Into CSS Grid <code>minmax()</code></a></li> <li><a href="https://web.dev/min-max-clamp/"><code>min()</code>, <code>max()</code>, and <code>clamp()</code>: three logical CSS functions to use today</a></li> <li><a href="https://florian.geierstanger.org/blog/css-layout-grid">Re-thinking the layout grid with CSS grid</a></li> <li><a href="https://gridbyexample.com/">Grid by Example</a></li> </ul> Creating Time 2023-01-20T00:00:00Z https://ryanmulligan.dev/blog/creating-time/ <p>For my birthday, my partner put together the sweetest, most thoughtful surprise I could ever imagine. She recognized that I had been in a complete creative rut the previous year. I'd complain that I work too much, play too little. I'm too tired. Way too busy. This, that, and the other thing is blocking my time. Sometimes I would straight up conclude that I'm just not <em>good</em> at anything—a collaboration between imposter syndrome and plateauing.</p> <p>I'd often sink into the couch after a long day instead of logging time towards hobbies or creative projects. All day screen time had frazzled my mind. But then I'd follow up with more of it, doomscrolling on my phone or gazing at the television. Not a lot was being accomplished outside of work hours.</p> <p>While I did play my guitar a good amount, I never completed any song arrangements. Last year I convinced myself to blog more, my goal set on writing a post every month or at least six articles in 2022. I acheived the latter, which I'm proud of, but maybe, <em>maybe</em>, I could have put more effort into my original milestone of twelve posts.</p> <p>Anyway, returning to where I began: On my birthday, my partner reveals that she booked a house where we would be staying with some close friends and family. To sprinkle on even more excitement, the property she booked includes a music studio with a drumkit ready for rockin’. I haven't been able to sit behind the drums for several years (blame the pandemic, me moving around a bunch, drums simply being too loud to play in a condo, etc.) so this news was absolutely thrilling.</p> <p>The people brought together for this getaway were selected based on a few magical moments throughout my life's musical journey. I have reminisced about week-long writing/recording sessions, staying up all night arranging tracks, playing take after take to get it right, and never letting exhaustion from the work day stop me. My partner understands how music lifts my spirit. Her gathering these folks for three nights of jamming was the greatest way to kick off a new year.</p> <p>I don't necessarily desire to chase new year resolutions. However, this music trip felt like a solid step towards—forgive me for saying it—changing my tune. I'm going to refocus my time and play more. I'll complete that song arrangement I've left fragmented. I'll write brand new ones. I'll make things out of clay, build with legos, draw, write, and take better care of my creative brain. All the while, I will remind myself that it's okay to be in a creative rut from time to time.</p> <p>Along with this week's jam sessions, I've also started reading <em>The Artist’s Way</em> by Julia Cameron. It's only week one, but I've been writing three pages each morning, pouring out stream of consciousness via pen on paper. The idea is to connect the mind and body, writing longhand to declutter brain space before starting the day. Doesn't matter what I'm jotting down. Just write.</p> <p>Once I really embraced these morning pages, I discovered that I feel accomplished, calm, and clear-headed. Other discoveries: My hand cramps up very quickly and my handwriting looks like garbage.</p> <p>It doesn't matter.</p> <p>This is not for anyone to read. It's not even something I plan to reflect back on. I'm looking forward to filling every page in that notebook and then promptly tossing it in the trash, moving on to the next one.</p> <p>So what's next? Here's are some things I would like to be doing more, starting right now:</p> <ul> <li>Finish song arrangements and record that music.</li> <li>Explore the amazing realm of guitar pedals.</li> <li>Make <a href="https://youtu.be/2jqKiVHS6x4">stop-motion claymation videos</a>.</li> <li>Write the music for those videos.</li> <li>Sketch while I watch television.</li> <li>Read more books—physical copies preferred.</li> <li>Stretch when I wake up. Like for <em>at least</em> five minutes.</li> <li>Discover a new hobby. Something different to awaken my creative brain.</li> <li>Dance. If I want to.</li> </ul> CSS Grid Gap Behavior with Hidden Elements 2023-02-14T00:00:00Z https://ryanmulligan.dev/blog/grid-gap/ <p>I was recently prototyping a component layout that included a way to toggle the visibility of sibling elements inside a grid display. What tripped me up was, while these elements were hidden, all of the container's <code>gap</code> gutters remained, leaving undesired extra visual spacing. I expected these gutters to collapse. The reason they stick around is related to explicitly defining grid templates.</p> <h2 id="template-or-auto-layout">Template or auto layout?</h2> <p>What are the differences between <code>grid-template-*</code> and <code>grid-auto-*</code> when declared for columns or rows in a grid layout? Ire Aderinokun has <a href="https://bitsofco.de/understanding-the-difference-between-grid-template-and-grid-auto/">a fantastic article</a> that thoroughly explains these distinctions and I recommend giving it a read. I'll try to quickly summarize: <code>grid-template-*</code> sets explicit column and row tracks, while <code>grid-auto-*</code> creates implicit track patterns.</p> <p>The following excerpt in the &quot;How grid-auto works&quot; section from the article stood out to me:</p> <blockquote> <p>Unlike the <code>grid-template-*</code> properties, the <code>grid-auto-*</code> properties only accept a single length value.</p> </blockquote> <p>After some experimentation and confirming through examples from the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/grid-auto-rows#syntax"><em>Syntax</em> section</a> in the <code>grid-auto-rows</code> MDN web docs, I found that multiple track-size values can be used as well. Let's try an example to create a layout commonly referred to as <a href="https://web.dev/patterns/layout/pancake-stack/">the pancake stack</a>. Its value of <code>auto 1fr auto</code> will either:</p> <ul> <li>explicitly size and position only the first three rows when used in <code>grid-template-rows</code></li> <li>act as a pattern to implicitly size each group of three rows in <code>grid-auto-rows</code></li> </ul> <h2 id="visualize-the-gap">Visualize the gap</h2> <p>In the CodePen demo below, tick on the &quot;Hide elements&quot; checkbox to assign <code>display: none</code> on all but the first two elements in both grid containers.</p> <p class="codepen" data-height="600" data-preview="false" data-default-tab="result" data-slug-hash="bGxbpjj" data-user="hexagoncircle"> <a href="https://codepen.io/hexagoncircle/pen/bGxbpjj"> <svg class="icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512" fill="currentcolor"><path d="M502.285 159.704l-234-156c-7.987-4.915-16.511-4.96-24.571 0l-234 156C3.714 163.703 0 170.847 0 177.989v155.999c0 7.143 3.714 14.286 9.715 18.286l234 156.022c7.987 4.915 16.511 4.96 24.571 0l234-156.022c6-3.999 9.715-11.143 9.715-18.286V177.989c-.001-7.142-3.715-14.286-9.716-18.285zM278 63.131l172.286 114.858-76.857 51.429L278 165.703V63.131zm-44 0v102.572l-95.429 63.715-76.857-51.429L234 63.131zM44 219.132l55.143 36.857L44 292.846v-73.714zm190 229.715L61.714 333.989l76.857-51.429L234 346.275v102.572zm22-140.858l-77.715-52 77.715-52 77.715 52-77.715 52zm22 140.858V346.275l95.429-63.715 76.857 51.429L278 448.847zm190-156.001l-55.143-36.857L468 219.132v73.714z"></path></svg> <span>Open CodePen demo</span> </a> </p> <script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script> <aside class="callout"><p><em>Note:</em> I'm toggling the container height value to help emphasize the difference between the explicitly-sized <code>grid-template-rows</code> and the implicit pattern created by <code>grid-auto-rows</code>.</p> </aside><p>So what's happening here? When collapsed, the <code>grid-template-rows</code> container is slightly taller than its <code>grid-auto-rows</code> counterpart because of the extra space appearing beneath the remaining visible elements. Recall that rows are <em>explicitly</em> set with <code>grid-template-rows</code>. In this situation, the <code>gap</code> gutters still apply even when elements are hidden or removed from the container.</p> <p>I ended up moving forward with <code>grid-auto-rows</code> for my component's layout needs. You can see a stripped down version of it in the CodePen below. The classic small screen navigation!</p> <p class="codepen" data-height="600" data-preview="false" data-default-tab="result" data-slug-hash="zYJOGbv" data-user="hexagoncircle"> <a href="https://codepen.io/hexagoncircle/pen/zYJOGbv"> <svg class="icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512" fill="currentcolor"><path d="M502.285 159.704l-234-156c-7.987-4.915-16.511-4.96-24.571 0l-234 156C3.714 163.703 0 170.847 0 177.989v155.999c0 7.143 3.714 14.286 9.715 18.286l234 156.022c7.987 4.915 16.511 4.96 24.571 0l234-156.022c6-3.999 9.715-11.143 9.715-18.286V177.989c-.001-7.142-3.715-14.286-9.716-18.285zM278 63.131l172.286 114.858-76.857 51.429L278 165.703V63.131zm-44 0v102.572l-95.429 63.715-76.857-51.429L234 63.131zM44 219.132l55.143 36.857L44 292.846v-73.714zm190 229.715L61.714 333.989l76.857-51.429L234 346.275v102.572zm22-140.858l-77.715-52 77.715-52 77.715 52-77.715 52zm22 140.858V346.275l95.429-63.715 76.857 51.429L278 448.847zm190-156.001l-55.143-36.857L468 219.132v73.714z"></path></svg> <span>Open CodePen demo</span> </a> </p> <script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script> <h2 id="a-template-solution">A template solution</h2> <p>If using <code>grid-template-*</code> is preferred or necessary, the solution is to override the property value to match the expected visual result. The above demo could even get by on a single ruleset that applies the template only when the menu is open:</p> <pre class="language-css"><code class="language-css"><span class="token selector">.nav.is-open</span> <span class="token punctuation">{</span> <span class="token property">grid-template-rows</span><span class="token punctuation">:</span> auto 1fr auto<span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>This same solution can also work for <code>grid-template-areas</code>. While it leads to writing more code, it self-documents really nicely.</p> <pre class="language-css"><code class="language-css"><span class="token selector">.nav</span> <span class="token punctuation">{</span> <span class="token property">grid-template-areas</span><span class="token punctuation">:</span> <span class="token string">"logo toggle"</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">.nav.is-open</span> <span class="token punctuation">{</span> <span class="token property">grid-template-areas</span><span class="token punctuation">:</span> <span class="token string">"logo toggle"</span> <span class="token string">"menu menu"</span> <span class="token string">"cta cta"</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">.nav .logo</span> <span class="token punctuation">{</span> <span class="token property">grid-area</span><span class="token punctuation">:</span> logo<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">.nav .toggle</span> <span class="token punctuation">{</span> <span class="token property">grid-area</span><span class="token punctuation">:</span> toggle<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">.nav .menu</span> <span class="token punctuation">{</span> <span class="token property">grid-area</span><span class="token punctuation">:</span> menu<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">.nav .cta</span> <span class="token punctuation">{</span> <span class="token property">grid-area</span><span class="token punctuation">:</span> cta<span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <h2 id="helpful-resources">Helpful resources</h2> <ul> <li><a href="https://bitsofco.de/understanding-the-difference-between-grid-template-and-grid-auto/">Understanding the difference between grid-template and grid-auto</a></li> <li><a href="https://marcus-obst.de/blog/mid-the-gap-hide-a-column-in-css-grid">Mind the Gap – Hide a Column in CSS-Grid</a></li> <li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/grid-auto-rows">grid-auto-rows on MDN</a></li> <li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-rows">grid-template-rows on MDN</a></li> </ul> Sticky Page Header Shadow on Scroll 2023-04-02T00:00:00Z https://ryanmulligan.dev/blog/sticky-header-scroll-shadow/ <p>We've seen it plenty of times around the web where a website's page header follows us as we scroll down the page. CSS makes doing this a breeze with <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/position#sticky_positioning">sticky positioning</a>:</p> <pre class="language-css"><code class="language-css"><span class="token selector">.page-header</span> <span class="token punctuation">{</span> <span class="token property">position</span><span class="token punctuation">:</span> sticky<span class="token punctuation">;</span> <span class="token property">top</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>What if we desired something a little bit extra, like applying a <code>box-shadow</code> to the sticky header as soon as the page is scrolled? I thought it was worth sharing one solution that has worked well for me to accomplish this goal. Check out the following CodePen demo. As soon as the page is scrolled, a shadow fades in below the header.</p> <p class="codepen" data-height="400" data-preview="false" data-default-tab="result" data-slug-hash="qBMeWqo" data-user="hexagoncircle"> <a href="https://codepen.io/hexagoncircle/pen/qBMeWqo"> <svg class="icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512" fill="currentcolor"><path d="M502.285 159.704l-234-156c-7.987-4.915-16.511-4.96-24.571 0l-234 156C3.714 163.703 0 170.847 0 177.989v155.999c0 7.143 3.714 14.286 9.715 18.286l234 156.022c7.987 4.915 16.511 4.96 24.571 0l234-156.022c6-3.999 9.715-11.143 9.715-18.286V177.989c-.001-7.142-3.715-14.286-9.716-18.285zM278 63.131l172.286 114.858-76.857 51.429L278 165.703V63.131zm-44 0v102.572l-95.429 63.715-76.857-51.429L234 63.131zM44 219.132l55.143 36.857L44 292.846v-73.714zm190 229.715L61.714 333.989l76.857-51.429L234 346.275v102.572zm22-140.858l-77.715-52 77.715-52 77.715 52-77.715 52zm22 140.858V346.275l95.429-63.715 76.857 51.429L278 448.847zm190-156.001l-55.143-36.857L468 219.132v73.714z"></path></svg> <span>Open CodePen demo</span> </a> </p> <script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script> <p>An element that I've decidedly dubbed an &quot;intercept&quot;—naming is hard and this felt right in the moment—is created and inserted above the page header at the top of the page. If we open the browser dev tools and inspect the <abbr title="Document Object Model">DOM</abbr>, we'll find:</p> <pre class="language-html"><code class="language-html"> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">data-observer-intercept</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>header</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>page-header<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> //... <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>header</span><span class="token punctuation">></span></span></code></pre> <p>The <a href="https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API">Intersection Observer API</a> is being used to observe when the intercept is no longer appearing in the visible viewport area which happens as soon as the page scrolls. So when the intercept is <em>not</em> intersecting, a class is applied to the header element.</p> <pre class="language-js"><code class="language-js"><span class="token keyword">const</span> observer <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">IntersectionObserver</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">[</span>entry<span class="token punctuation">]</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> header<span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">toggle</span><span class="token punctuation">(</span><span class="token string">"active"</span><span class="token punctuation">,</span> <span class="token operator">!</span>entry<span class="token punctuation">.</span>isIntersecting<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> observer<span class="token punctuation">.</span><span class="token function">observe</span><span class="token punctuation">(</span>intercept<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre> <p>Inspecting the DOM again, we'll catch the <code>active</code> class name on the page header element toggling on and off as we scroll down and back up.</p> <h2 id="delay-that-shadow">Delay that shadow</h2> <p>It's also possible to wait on when the shadow should appear by offsetting the intercept element. Try editing the above demo on CodePen. In the CSS panel add the following ruleset:</p> <pre class="language-css"><code class="language-css"><span class="token selector">[data-observer-intercept]</span> <span class="token punctuation">{</span> <span class="token property">position</span><span class="token punctuation">:</span> absolute<span class="token punctuation">;</span> <span class="token property">top</span><span class="token punctuation">:</span> 300px<span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>This will push the intercept down from the top of the page by 300 pixels. When scrolling the page again, notice that the shadow doesn't appear right away, waiting until the page has been scrolled passed the offset value.</p> <h2 id="css-scroll-driven-animations">CSS scroll-driven animations</h2> <p><strong>Updated on October 20th, 2023:</strong> Here's another <a href="https://codepen.io/hexagoncircle/pen/LYMweej">CodePen demo</a> that leans into CSS scroll-driven animations. Try it out in a browser that supports this feature.</p> <p class="codepen" data-height="400" data-preview="false" data-default-tab="result" data-slug-hash="LYMweej" data-user="hexagoncircle"> <a href="https://codepen.io/hexagoncircle/pen/LYMweej"> <svg class="icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512" fill="currentcolor"><path d="M502.285 159.704l-234-156c-7.987-4.915-16.511-4.96-24.571 0l-234 156C3.714 163.703 0 170.847 0 177.989v155.999c0 7.143 3.714 14.286 9.715 18.286l234 156.022c7.987 4.915 16.511 4.96 24.571 0l234-156.022c6-3.999 9.715-11.143 9.715-18.286V177.989c-.001-7.142-3.715-14.286-9.716-18.285zM278 63.131l172.286 114.858-76.857 51.429L278 165.703V63.131zm-44 0v102.572l-95.429 63.715-76.857-51.429L234 63.131zM44 219.132l55.143 36.857L44 292.846v-73.714zm190 229.715L61.714 333.989l76.857-51.429L234 346.275v102.572zm22-140.858l-77.715-52 77.715-52 77.715 52-77.715 52zm22 140.858V346.275l95.429-63.715 76.857 51.429L278 448.847zm190-156.001l-55.143-36.857L468 219.132v73.714z"></path></svg> <span>Open CodePen demo</span> </a> </p> <script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script> <p>I've been justifiably excited about browsers beginning to adopt this API, which I had written about in <a href="https://ryanmulligan.dev/blog/scroll-driven-animations/">this blog post</a>. It's <em>not quite</em> the same as using an intersection observer: The observer toggles a class selector that triggers an animation for the declared duration of time whereas this version links the fade progress to the page scroll position. I find that the latter feels more natural. If a browser doesn't yet support the feature, the styles gracefully degrade to a persistent static shadow.</p> <p>Have questions? Other ways to handle this? I'd love to hear about it! Reach out to me on <a href="https://fosstodon.org/@hexagoncircle">Mastodon</a> with your ideas.</p> Full-bleed Table Scrolling on Narrow Viewports 2023-05-20T00:00:00Z https://ryanmulligan.dev/blog/full-bleed-table-scrolling/ <p>I found the following to be a rather decent solution for having HTML tables overflow the inline edges of smaller/tighter/narrow viewports. Try resizing the width of the browser window if viewing this page on a larger screen.</p> <p class="codepen" data-height="600" data-preview="false" data-default-tab="result" data-slug-hash="ZEqjzKw" data-user="hexagoncircle"> <a href="https://codepen.io/hexagoncircle/pen/ZEqjzKw"> <svg class="icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512" fill="currentcolor"><path d="M502.285 159.704l-234-156c-7.987-4.915-16.511-4.96-24.571 0l-234 156C3.714 163.703 0 170.847 0 177.989v155.999c0 7.143 3.714 14.286 9.715 18.286l234 156.022c7.987 4.915 16.511 4.96 24.571 0l234-156.022c6-3.999 9.715-11.143 9.715-18.286V177.989c-.001-7.142-3.715-14.286-9.716-18.285zM278 63.131l172.286 114.858-76.857 51.429L278 165.703V63.131zm-44 0v102.572l-95.429 63.715-76.857-51.429L234 63.131zM44 219.132l55.143 36.857L44 292.846v-73.714zm190 229.715L61.714 333.989l76.857-51.429L234 346.275v102.572zm22-140.858l-77.715-52 77.715-52 77.715 52-77.715 52zm22 140.858V346.275l95.429-63.715 76.857 51.429L278 448.847zm190-156.001l-55.143-36.857L468 219.132v73.714z"></path></svg> <span>Open CodePen demo</span> </a> </p> <script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script> <p>Notice that the table overflows beyond the edge of the window. This can be acheived by wrapping the <code>table</code> element with another element.</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>figure</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>wrapper<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>table</span><span class="token punctuation">></span></span> <span class="token comment">&lt;!-- ... --></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>table</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>figure</span><span class="token punctuation">></span></span></code></pre> <p>On this wrapper, a combination of inline padding and negative margins create an offset that matches the page gutter size—that space to the left and right of the main content area. Here's a simplified example of those styles:</p> <pre class="language-css"><code class="language-css"><span class="token selector">body</span> <span class="token punctuation">{</span> <span class="token property">--page-gutter</span><span class="token punctuation">:</span> <span class="token function">clamp</span><span class="token punctuation">(</span>1rem<span class="token punctuation">,</span> 4vw<span class="token punctuation">,</span> 2rem<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">padding-inline</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--page-gutter<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">.wrapper</span> <span class="token punctuation">{</span> <span class="token property">margin-inline</span><span class="token punctuation">:</span> <span class="token function">calc</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--page-gutter<span class="token punctuation">)</span> * -1<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">padding-inline</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--page-gutter<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p class="callout"><code>clamp()</code> is used to create fluid padding. The gutter size shrinks as the viewport gets narrower. Unfamiliar with how this CSS function works? Check out the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/clamp">docs on MDN</a>.</p> <p>The inline margin will pull the table wrapper to the viewport edge. Then inline padding pushes the table back into position so that it's once again aligned with the page content. Here's all the CSS necessary for horizontal scrolling and wrapper repositioning:</p> <pre class="language-css"><code class="language-css"><span class="token selector">.wrapper</span> <span class="token punctuation">{</span> <span class="token property">display</span><span class="token punctuation">:</span> flex<span class="token punctuation">;</span> <span class="token property">overscroll-behavior-x</span><span class="token punctuation">:</span> contain<span class="token punctuation">;</span> <span class="token property">overflow-x</span><span class="token punctuation">:</span> auto<span class="token punctuation">;</span> <span class="token property">margin-inline</span><span class="token punctuation">:</span> <span class="token function">calc</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--page-gutter<span class="token punctuation">)</span> * -1<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">padding-inline</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--page-gutter<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p class="callout">Setting <code>display: flex</code> on the wrapper element fixes a tiny issue in Safari (version 16.4 at the time of writing) where the inline padding at the end appears collapsed.</p> <p>That's it! There are a handful of ways to display tables on smaller screens. I like that this solution requires very little code and doesn't rely on breakpoint changes. How might you solve this differently? <a href="https://fosstodon.org/@hexagoncircle">Let's discuss!</a></p> CSS Custom Property Fallbacks in Shorthand Values 2023-07-14T00:00:00Z https://ryanmulligan.dev/blog/css-custom-prop-fallbacks/ <p>CSS Custom Properties are incredibly versatile and have become especially useful as customizable props in common layout and component style patterns. Here's an example derived from the <a href="https://smolcss.dev/#smol-css-grid">SmolCSS</a> site:</p> <pre class="language-css"><code class="language-css"><span class="token selector">.grid</span> <span class="token punctuation">{</span> <span class="token property">--min</span><span class="token punctuation">:</span> 15ch<span class="token punctuation">;</span> <span class="token property">--gap</span><span class="token punctuation">:</span> 1rem<span class="token punctuation">;</span> <span class="token property">display</span><span class="token punctuation">:</span> grid<span class="token punctuation">;</span> <span class="token property">gap</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--gap<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">grid-template-columns</span><span class="token punctuation">:</span> <span class="token function">repeat</span><span class="token punctuation">(</span>auto-fit<span class="token punctuation">,</span> <span class="token function">minmax</span><span class="token punctuation">(</span><span class="token function">min</span><span class="token punctuation">(</span>100%<span class="token punctuation">,</span> <span class="token function">var</span><span class="token punctuation">(</span>--min<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span> 1fr<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>The <code>--gap</code> and <code>--min</code> custom property values can be customized by declaring new values for those properties, whether it's through inline styles or a custom CSS ruleset:</p> <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- inline style --></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>grid<span class="token punctuation">"</span></span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">--gap</span><span class="token punctuation">:</span> 2rem</span><span class="token punctuation">"</span></span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span>Item 1<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span>Item 2<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span>Item 3<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">></span></span> <span class="token comment">&lt;!-- custom ruleset --></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>style</span><span class="token punctuation">></span></span><span class="token style"><span class="token language-css"> <span class="token selector">.super-cool-list</span> <span class="token punctuation">{</span> <span class="token property">--gap</span><span class="token punctuation">:</span> 2rem<span class="token punctuation">;</span> <span class="token punctuation">}</span> </span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>style</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>super-cool-list grid<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span>Item 1<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span>Item 2<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span>Item 3<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">></span></span></code></pre> <p class="callout">Remember! The <code>super-cool-list</code> styles needs to be declared <em>after</em> the <code>grid</code> ruleset in the stylesheet. Otherwise the default <code>--gap</code> value inside <code>grid</code> would win with higher precedence. I'm a fan of using <a href="https://css-tricks.com/css-cascade-layers/">CSS cascade layers</a> where layout primitives like <code>grid</code> would reside in a lower priority layer than component-specific styles.</p> <p>I absolutely love this concept of altering layouts through exposed props like the example above. But what if we desired the ability to provide independent values for the horizontal and vertical spacing between each item? This is where a key feature of CSS custom properties comes into play: <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties#custom_property_fallback_values">fallback values</a>. In the revised version of the above code snippet, The <code>--gap</code> value declared at the start of the ruleset becomes the fallback—or default value—for two new variables.</p> <pre class="language-css"><code class="language-css"><span class="token selector">.grid</span> <span class="token punctuation">{</span> <span class="token property">--min</span><span class="token punctuation">:</span> 15ch<span class="token punctuation">;</span> <span class="token property">--gap</span><span class="token punctuation">:</span> 1rem<span class="token punctuation">;</span> <span class="token property">--row-gap</span><span class="token punctuation">:</span> initial<span class="token punctuation">;</span> <span class="token property">--column-gap</span><span class="token punctuation">:</span> initial<span class="token punctuation">;</span> <span class="token property">display</span><span class="token punctuation">:</span> grid<span class="token punctuation">;</span> <span class="token property">gap</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--row-gap<span class="token punctuation">,</span> <span class="token function">var</span><span class="token punctuation">(</span>--gap<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token function">var</span><span class="token punctuation">(</span>--column-gap<span class="token punctuation">,</span> <span class="token function">var</span><span class="token punctuation">(</span>--gap<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">grid-template-columns</span><span class="token punctuation">:</span> <span class="token function">repeat</span><span class="token punctuation">(</span>auto-fit<span class="token punctuation">,</span> <span class="token function">minmax</span><span class="token punctuation">(</span><span class="token function">min</span><span class="token punctuation">(</span>100%<span class="token punctuation">,</span> <span class="token function">var</span><span class="token punctuation">(</span>--min<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span> 1fr<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>The <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/gap"><code>gap</code> property</a> is shorthand for <code>row-gap</code> and <code>column-gap</code> respectively. With these values now split, we can pass in an override value to either axis.</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>grid<span class="token punctuation">"</span></span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">--row-gap</span><span class="token punctuation">:</span> 2rem</span><span class="token punctuation">"</span></span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span>Item 1<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span>Item 2<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span>Item 3<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">></span></span></code></pre> <p>The gap spacing between each row of items will now be <code>2rem</code> while the columns stick to the default <code>--gap</code> size of <code>1rem</code>.</p> <h2 id="guaranteed-invalid-values">Guaranteed-invalid values</h2> <p><code>--row-gap</code> and <code>--column-gap</code> are both set to the <code>initial</code> keyword because it's a <a href="https://drafts.csswg.org/css-variables/#guaranteed-invalid-value">guaranteed-invalid value</a> in custom properties. This means that these two custom property values will become invalid at computed-value time and revert to a fallback if one is available. I think this concept is summed up nicely in <a href="https://css-tricks.com/using-custom-property-stacks-to-tame-the-cascade/">a snippet from this article</a>:</p> <blockquote> <p>[...] rather than being passed along to set <code>background: initial</code> or <code>color: initial</code>, the custom property becomes <code>undefined</code>, and we fallback to the next value in our stack [...]</p> </blockquote> <p>In the example above, since <code>--row-gap</code> and <code>--column-gap</code> are undefined through the <code>initial</code> keyword, the fallback <code>--gap</code> value is applied.</p> <h2 id="why-not-only-use-fallbacks">Why not only use fallbacks?</h2> <p>Custom properties can have more than one fallback value—a concept Miriam Suzanne refers to as <a href="https://css-tricks.com/using-custom-property-stacks-to-tame-the-cascade/">custom property &quot;stacks&quot; in this article</a>, which I love. It's also where I discovered how <code>initial</code> works in custom properties as mentioned above.</p> <p>So then if custom properties can have multiple fallback values, could we instead write our CSS like this?</p> <pre class="language-css"><code class="language-css"><span class="token selector">.grid</span> <span class="token punctuation">{</span> <span class="token property">display</span><span class="token punctuation">:</span> grid<span class="token punctuation">;</span> <span class="token property">gap</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--row-gap<span class="token punctuation">,</span> <span class="token function">var</span><span class="token punctuation">(</span>--gap<span class="token punctuation">,</span> 1rem<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token function">var</span><span class="token punctuation">(</span>--column-gap<span class="token punctuation">,</span> <span class="token function">var</span><span class="token punctuation">(</span>--gap<span class="token punctuation">,</span> 1rem<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">grid-template-columns</span><span class="token punctuation">:</span> <span class="token function">repeat</span><span class="token punctuation">(</span>auto-fit<span class="token punctuation">,</span> <span class="token function">minmax</span><span class="token punctuation">(</span><span class="token function">min</span><span class="token punctuation">(</span>100%<span class="token punctuation">,</span> <span class="token function">var</span><span class="token punctuation">(</span>--min<span class="token punctuation">,</span> 15ch<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span> 1fr<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>This works as one would expect. However, keep in mind that on the occasion there is a nested element that uses the <code>grid</code> selector, that element would inherit the <code>--gap</code> set on the parent.</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>grid<span class="token punctuation">"</span></span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">--gap</span><span class="token punctuation">:</span> 2rem</span><span class="token punctuation">"</span></span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span>Item 1<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span>Item 2<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span> Item 3 <span class="token comment">&lt;!-- This &lt;ul> will also have a 2rem gap --></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>grid<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span>Item 1<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span>Item 2<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span>Item 3<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">></span></span></code></pre> <p>By setting <code>--gap</code> at the top of the <code>grid</code> ruleset, the nested element's gap value will reset to that declared default. I personally prefer this. I can imagine headaches may come from having a very deeply (hopefully not too deep!) nested element where the gap value is different than the presumed default. It wouldn't be immediately clear, especially in a componentized codebase.</p> <h2 id="inheritance-is-a-good-thing">Inheritance is a good thing</h2> <p><strong>This content has been revised on July 15th</strong> after a valid argument was made on <a href="https://fosstodon.org/@hexagoncircle/110713633805281051">my Mastodon post sharing the article</a> in favor of inheriting ancestor custom property values:</p> <blockquote> <p>Isn’t inheritance of custom properties a good thing? I thought that’s how they’re meant to be used. Setting a custom property <em>once</em> on an outer container, and then it inherits to <em>all</em> the nested components. I’m not sure that intentionally breaking this system is a good idea.</p> </blockquote> <p>Excellent point, and agreed: Inheritance of custom properties is a good thing. This has certainly given me some pause on my preferred approach. I had imagined layout primitives such as the <code>grid</code> example would set ideal default values every time the selector is applied. Instead, when inheriting properties on a nested element, we would then have to add a &quot;reset&quot; value to revert it back, which arguably may be the optimal method.</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>grid<span class="token punctuation">"</span></span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">--gap</span><span class="token punctuation">:</span> 2rem</span><span class="token punctuation">"</span></span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span>Item 1<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span>Item 2<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span> Item 3 <span class="token comment">&lt;!-- Revert the value on this element --></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>grid<span class="token punctuation">"</span></span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">--gap</span><span class="token punctuation">:</span> 1rem</span><span class="token punctuation">"</span></span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span>Item 1<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span>Item 2<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span>Item 3<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">></span></span></code></pre> <p>What do you think? Please feel free to join us on <a href="https://fosstodon.org/@hexagoncircle/110713633805281051">the Mastodon thread</a> with your opinions and feedback. Also, <a href="https://codepen.io/hexagoncircle/pen/ExOEjGG">check out this CodePen</a> if you'd like to experiment with the different methods described here.</p> <h2 id="helpful-resources">Helpful resources</h2> <ul> <li><a href="https://every-layout.dev/">Every Layout</a> has been a key staple in my layout style diet and I highly recommend going through all of it if you haven't already.</li> <li><a href="https://smolcss.dev/">SmolCSS</a> is a fantastic, robust collection of modern layout and component snippets. A must-bookmark for many revisits.</li> <li><a href="https://css-tricks.com/using-custom-property-stacks-to-tame-the-cascade/">Using Custom Property “Stacks” to Tame the Cascade</a>—a special thanks to Miriam's article for introducing me to some amazing, new (to me) custom property concepts.</li> </ul> Starting Exploration of Scroll-driven Animations in CSS 2023-08-21T00:00:00Z https://ryanmulligan.dev/blog/scroll-driven-animations/ <p>CSS Scroll-driven Animations has recently made its debut on the main stage in the latest versions of Chrome and Edge. Before this module became available, linking an element's animation to a scroll position was only possible through JavaScript. I've been (and still am) a huge fan of <a href="https://greensock.com/scrolltrigger/">GSAP ScrollTrigger</a> as one way to achieve such an effect. I never imagined it would become a reality in CSS, but this new API lets us hook right into CSS animation <code>@keyframes</code> and scrub through the animation progress as we scroll the page.</p> <p class="callout">My article will share demos and some early learnings about scroll-driven animations. If it's all new to you as well, I urge you to read <a href="https://developer.chrome.com/articles/scroll-driven-animations/">Animate elements on scroll with Scroll-driven animations</a> by Bramus and Michelle Barker's <a href="https://developer.mozilla.org/en-US/blog/scroll-progress-animations-in-css/">Scroll progress animations in CSS</a>. They are both excellent deep dives into this new spec and helped me get a handle on how it works.</p> <p>I had the chance to noodle around with both timeline types introduced in the <a href="https://drafts.csswg.org/scroll-animations-1/">Scroll-driven Animations spec</a>:</p> <ul> <li><a href="https://developer.chrome.com/articles/scroll-driven-animations/#scroll-progress-timeline">Scroll Progress Timeline</a> is connected to the scroll position of a scroll container along an axis.</li> <li><a href="https://developer.chrome.com/articles/scroll-driven-animations/#view-progress-timeline">View Progress Timeline</a> links a timeline to the relative position of an element within a scroll container.</li> </ul> <p>When getting started, these <a href="https://scroll-driven-animations.style/#tools">progress visualizer tools</a> were immensely helpful. They were frequently referenced while I tinkered on scroll timeline animation ideas.</p> <h2 id="experiment-1-photo-figures">Experiment #1: Photo figures</h2> <p>I turned to a recent <a href="https://codepen.io/challenges">CodePen Challenge</a> to begin my exploration, which leans into View Progress Timeline features. When scrolling the page in the <a href="https://codepen.io/hexagoncircle/full/PoxMPzM">Photo figures CodePen demo</a>, notice that the heading text follows down as it fades out, the first three Polaroid-style photos have a &quot;develop&quot; effect, and the last stack of photos shuffle between themselves.</p> <figure class="video"> <video preload="metadata" loop="" muted="" playsinline="" controls=""> <source src="https://ryanmulligan.dev/videos/scroll-driven-animations-1.webm#t=0.001" type="video/webm" /> <source src="https://ryanmulligan.dev/videos/scroll-driven-animations-1.mp4#t=0.001" type="video/mp4" /> <p>Your browser cannot play the provided video file.</p> </video> <figcaption>Scrolling through the <a href="https://codepen.io/hexagoncircle/full/PoxMPzM">Photo figures CodePen demo</a></figcaption></figure> <p>To make this happen, the way we write animation <code>@keyframes</code> hasn't changed. Instead, when applying that animation to an element, we introduce two new properties: <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timeline"><code>animation-timeline</code></a> and <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/animation-range"><code>animation-range</code></a>. Here's the simplified HTML for each &quot;developing&quot; photo as an example:</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>figure</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>img</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>develop-photo<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>figcaption</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>figcaption</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>figure</span><span class="token punctuation">></span></span></code></pre> <p>And the CSS for its scroll-driven animation:</p> <pre class="language-css"><code class="language-css"><span class="token selector">figure</span> <span class="token punctuation">{</span> <span class="token property">view-timeline-name</span><span class="token punctuation">:</span> --photo<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">.develop-photo</span> <span class="token punctuation">{</span> <span class="token property">animation</span><span class="token punctuation">:</span> linear develop both<span class="token punctuation">;</span> <span class="token property">animation-timeline</span><span class="token punctuation">:</span> --photo<span class="token punctuation">;</span> <span class="token property">animation-range</span><span class="token punctuation">:</span> entry 30% cover 40%<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token atrule"><span class="token rule">@keyframes</span> develop</span> <span class="token punctuation">{</span> <span class="token selector">from</span> <span class="token punctuation">{</span> <span class="token property">filter</span><span class="token punctuation">:</span> <span class="token function">blur</span><span class="token punctuation">(</span>30px<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">scale</span><span class="token punctuation">:</span> 1.1<span class="token punctuation">;</span> <span class="token property">opacity</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">to</span> <span class="token punctuation">{</span> <span class="token property">filter</span><span class="token punctuation">:</span> <span class="token function">blur</span><span class="token punctuation">(</span>0<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">scale</span><span class="token punctuation">:</span> 1<span class="token punctuation">;</span> <span class="token property">opacity</span><span class="token punctuation">:</span> 1<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre> <p>When applying animations to the heading and shuffling photos, declaring <code>animation-timeline: view()</code> with an <code>animation-range</code> were the magic ingredients to enable scrubbing through animation progress on scroll. The <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timeline/view"><code>view()</code></a> function binds the animation to the element as it appears in the viewport on the block axis. This function takes two parameters:</p> <ul> <li>The <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/view-timeline-axis"><code>&lt;axis&gt;</code></a> on which the timeline progresses.</li> <li>A <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/view-timeline-inset"><code>&lt;view-timeline-inset&gt;</code></a> that adjusts the position of the box where the element is considered visible.</li> </ul> <p>Since the default values are <code>block</code> and <code>auto</code> respectively, they can be omitted here.</p> <p>Back to the example code above, I initially attemped to use the <code>view()</code> function on the &quot;developing&quot; photos but had no success. It seems that wrapping the <code>img</code> inside a <code>div</code> may be the reason—I believe that the <code>overflow: hidden</code> rule on the <code>div</code> now makes it the nearest scroll container for the <code>img</code> element. To get this photo animation working, setting <code>view-timeline-name</code> on the parent <code>figure</code> and then referencing it via <code>animation-timeline</code> ended up being the solution.</p> <p>As for my chosen <code>animation-range</code> values? That was the result of much experimentation, playing with different combinations. I'm still getting the hang of it, but the <a href="https://scroll-driven-animations.style/tools/view-timeline/ranges/">Ranges and Progress Animation Visualizer Tool</a> proved to be a crucial guide on my journey.</p> <h3 id="additional-notes">Additional notes</h3> <p>When working with these new animation properties, there are a few important bits to keep in mind:</p> <ul> <li><code>animation-timeline</code> is not part of the <code>animation</code> shorthand, so it must be declared separately. Also, be sure to have it appear <em>after</em> the <code>animation</code> declaration because that shorthand will reset any animation longhand value, including <code>animation-timeline</code>.</li> <li>An <code>animation-duration</code> value in seconds won't affect a scroll progress timeline at all—always set it to <code>auto</code>. Since <code>auto</code> is the default value for this property, it can be omitted.</li> <li>The <code>both</code> value represents the <code>animation-fill-mode</code>. This ensures the animation follows the <code>@keyframes</code> rules fowards and backwards, animating in both directions on the timeline.</li> </ul> <h2 id="experiment-2-weather-app-prototype">Experiment #2: Weather app prototype</h2> <p>What excites me the most about scroll-driven animations is that it provides us the power to pull off some native-specific animation techniques directly in CSS. For example: the iOS weather app has been part of my daily ritual for quite some time. A lot of the app's animations are perfect for Scroll Progress Timeline! Check out my <a href="https://codepen.io/hexagoncircle/full/OJrJZqR">Weather app prototype</a> on CodePen.</p> <figure class="video"> <video preload="metadata" loop="" muted="" playsinline="" controls=""> <source src="https://ryanmulligan.dev/videos/scroll-driven-animations-2.webm#t=0.001" type="video/webm" /> <source src="https://ryanmulligan.dev/videos/scroll-driven-animations-2.mp4#t=0.001" type="video/mp4" /> <p>Your browser cannot play the provided video file.</p> </video> <figcaption>Scrolling through the <a href="https://codepen.io/hexagoncircle/full/OJrJZqR">Weather app prototype</a> on CodePen</figcaption></figure> <p>As explained in the previous demo, the <code>animation-range</code> property seems to be very versatile. I've only just scratched the surface of what it can do. In my first attempt to set an <code>animation-range</code> on the intro text fades, I used percentage values. Unfortunately, those animations would become slightly misaligned as the scroll container changed in height. In retrospect, that makes sense but, at the time, I had not realized that any <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/length-percentage"><code>&lt;length-percentage&gt;</code></a> is a valid <code>animation-range</code> value. Once I switched from percentages to <code>rem</code> units, my animations lined up as expected, regardless of the scroll container height.</p> <h2 id="scroll-driven-for-more">Scroll-driven for more</h2> <p>Something that I dig about both of these demos? The scroll timeline magic acts as a progressive enhancement. That won't always be the case, but it's awesome to see that both demos work as expected without it.</p> <p>It has been such a thrill being introduced to the delight that is CSS Scroll-driven Animations. With this and <a href="https://developer.chrome.com/docs/web-platform/view-transitions/">View Transitions API</a> on the horizon, it seems that simulating native app animation behaviors on the web is upon us. Maybe we'll soon see the end of companies constantly nudging us to download their native apps while we're browing their web app? Maybe they'll let us navigate around their home on the web as we intended without interruption?</p> <p>Dream big.</p> <h2 id="helpful-resources">Helpful resources</h2> <ul> <li><a href="https://developer.chrome.com/articles/scroll-driven-animations/">Animate elements on scroll with Scroll-driven animations</a></li> <li><a href="https://developer.mozilla.org/en-US/blog/scroll-progress-animations-in-css/">Scroll progress animations in CSS</a></li> <li><a href="https://scroll-driven-animations.style/">scroll-driven-animations.style</a></li> <li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_scroll-driven_animations">CSS scroll-driven animations on MDN</a></li> </ul> Scrollspy Navigation Web Component 2023-10-07T00:00:00Z https://ryanmulligan.dev/blog/scrollspy-nav/ <p>Just here for the code and demos? Check out the <a href="https://github.com/hexagoncircle/scrollspy-nav">scrollspy-nav repository</a> on GitHub and its corresponding <a href="https://hexagoncircle.github.io/scrollspy-nav/">demo page</a>.</p> <h2 id="the-backstory">The backstory</h2> <p>A &quot;scrollspy&quot; is a method of tracking which link in a menu is active based on a relevant section of information being visible in the viewport. Typically, the menu position is fixed to the browser window and the active link is indicated with some additional styling. I'm not 100% sure, but it might have started as a <a href="https://getbootstrap.com/docs/5.3/components/scrollspy/">Bootstrap plugin</a>. There have been a number of other versions and variations to follow.</p> <p>This particular <code>scrollspy-nav</code> component had more specific needs, so allow me to break it all down:</p> <ul> <li>A page contains sections of content, each with a unique <code>id</code> attribute.</li> <li>There is a horizontal menu list of page anchor links. When a link is clicked, it jumps the page down to a related section of content.</li> <li>When a section passes a certain threshold in the viewport, it becomes &quot;active&quot; along with its anchor link counterpart. It uses the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API">Intersection Observer API</a> to keep track of the active section.</li> <li>A change in the active section updates the position of a marker element in the menu. The marker animates from the previous active anchor link to the next, resizing itself to the dimensions of the current link's inline size.</li> <li>If a menu item is obscured in the viewport overflow horizontally, when it becomes active it will be scrolled fully into view.</li> </ul> <p>I had messed around with this general idea some time ago, but a recent project design brought me back to those old experiments. This is my attempt at turning the concept into a web component and I thought I'd share the results with you all. The project hasn't been packaged on <a href="https://www.npmjs.com/">npm</a> or anything because it's still, in my opinion, a work in progress. I've always been keen on web components but I am quite fresh in sharing my own.</p> <h2 id="whats-in-a-name">What's in a name?</h2> <p>Deciding on what to call this component was tough—naming things is perpetually difficult. I settled on <code>scrollspy-nav</code> for conciseness, but I debated and refactored for a bunch of different names:</p> <ul> <li><code>sticky-scrollspy-nav</code></li> <li><code>scrollspy-section-menu</code></li> <li><code>animated-marker-nav</code></li> <li><code>marker-menu</code></li> <li><code>scrollspy-navigation-with-sweet-animated-marker</code></li> </ul> <p>That last one isn't true.</p> <h2 id="web-c">WebC</h2> <p>The first iteration of this was built as a <a href="https://www.11ty.dev/docs/languages/webc/">WebC</a> component since my project happened to be using 11ty and WebC. This allowed me to combine the <code>script</code> and <code>style</code> elements into a single file, then let 11ty and WebC bundle them to their designated buckets in my page layout. Sticking to that vibe, I have included a <a href="https://github.com/hexagoncircle/scrollspy-nav/blob/main/scrollspy-nav.webc">scrollspy-nav.webc</a> file in the repo. All it does is pull in the css and js files. When the custom element is used on a page, the component code is then bundled appropriately.</p> <h2 id="styling">Styling</h2> <p>This is where I'd really love to hear feedback from all the web component makers and advocates out there.</p> <p>I opted to keep all the base styles in <a href="https://github.com/hexagoncircle/scrollspy-nav/blob/main/scrollspy-nav.css">a separate css file</a>. Most of the layout styles are necessary, although some of the gap spacing and margins are a bit opinionated. While I have tried moving the styles into a shadow DOM, I wasn't quite sure how I'd apply styling to nested selectors. Passing styles to the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:host">host</a> and the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/::slotted">slotted</a> <code>ul</code> can be done:</p> <pre class="language-css"><code class="language-css"><span class="token selector">:host</span> <span class="token punctuation">{</span><span class="token punctuation">}</span> <span class="token selector">::slotted(ul)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span></code></pre> <p>But targeting any nested elements of the unordered list won't work. At least not from what I've tried. Regardless, I prefer the styles detached from the script so that they still get applied if javascript happens to be disabled in the browser.</p> <p>For style overrides, this component provides a handful of <code>--scrollspy-nav-*</code> CSS custom properties. The <a href="https://hexagoncircle.github.io/scrollspy-nav/">demo page</a> showcases a couple examples where the marker position, duration, easing, and style are altered with author-selected values.</p> <h2 id="flip-the-marker">FLIP the marker</h2> <p>In my previous experiments, the marker element was inserted as a direct descendant of the <code>scrollspy-nav</code> element. At first, everything looked great. The marker animated smoothly as the active link changed. However, this presented a couple issues. Most notably, when resizing the browser window, the marker would lose its positioning visually as the menu started overflowing the parent container.</p> <p>So I thought: Maybe I could listen to a resize event and reposition it? That felt hacky and it might lead to other problems. What about appending the element to the active anchor link? That fixes the positioning woes, but how would I animate the marker from the previous active link to the next while keeping the animation smooth and performant?</p> <p>It then <em>finally</em> dawned on me: I had forgotten about the wonderful FLIP (First, Last, Invert, Play) technique! I had even written about it before in <a href="https://ryanmulligan.dev/blog/gsap-flip-cart/">Animating with the Flip Plugin for GSAP</a> and, while <a href="https://css-tricks.com/animating-layouts-with-the-flip-technique/">Animating Layouts with the FLIP Technique</a> on CSS-Tricks is now over six years old, it's still perfectly relevant to the topic.</p> <p>For the marker animation, I capture the width and position of the previous (first) and new (last) active links, update layout so that the marker is now appended to the new element, get the delta between the two link positions (invert), and then run the animation (play) from the previous position to the new one. The resulting <code>animateMarker</code> method can be reviewed in <a href="https://github.com/hexagoncircle/scrollspy-nav/blob/main/scrollspy-nav.js">the component script</a>.</p> <h2 id="adjusting-for-overflow">Adjusting for overflow</h2> <p>One last piece to call out is how the component handles active items hidden outside of the visible viewport area. Check out the <a href="https://hexagoncircle.github.io/scrollspy-nav/">demo page</a> in a narrow viewport size. A hidden or partially hidden active link will slide fully into view by calling the <code>scrollTo</code> method on the menu and scrolling it along the X axis by setting the distance to the <code>left</code> option value.</p> <h2 id="thoughts">Thoughts?</h2> <p>I'll wrap things up here. There are still plenty of UX enhancements to explore. Clearer indication that the menu scrolls horizontally and layout considerations in different writing modes are some that come immediately to mind. I'd love to hear what you like (or don't like) and how this component could be improved. <a href="https://fosstodon.org/@hexagoncircle">Reach out to me on Mastodon</a> and let's talk web components.</p> Site Rebuild, Here We Go! 2023-11-22T00:00:00Z https://ryanmulligan.dev/blog/site-rebuild/ <p>There are still a few bits to work out, but why wait any longer? The latest version of my site is here and it has been rebuilt from the ground up. I'm feeling pretty good about it and invite you all to celebrate the magic with me! ✨</p> <h2 id="inspiration">Inspiration</h2> <p>While playing Super Mario Wonder, I found myself intrigued by the title screen transitions before each level. The skewed text atop its grid background pattern looked sleek yet playful. I began tinkering with some ideas and eventually came up with the blog post heading style seen above. At that point, I decided it was time to go for a full site rebuild.</p> <h2 id="some-more-details">Some more details</h2> <p>I plan to dive into some site features in future blog posts, but here's a quick list of all the exciting parts. Feel free to dig around <a href="https://github.com/hexagoncircle/ryan-mulligan-dev">the site repo</a> as well.</p> <ul> <li>I rebuilt from scratch. I stuck with 11ty but this time leaned 100% into <a href="https://www.11ty.dev/docs/languages/webc/">WebC</a> for templating.</li> <li>There are some pretty neat (to me!) web components to be discovered in this project. Both of the following were built to progressively enhance the experience. <ul> <li>The <code>&lt;scroll-pen&gt;</code> extends the homepage CodePen collection's keyboard interactions, adds video previews that can be disabled, introduces input range slider control, and the ability to toggle the skew.</li> <li>The <code>&lt;theme-machine&gt;</code> is what you might guess: A total package for changing the site appearance and adjusting theme colors.</li> </ul> </li> <li>The homepage includes a few dynamic stats about the latest site deployment date, what the weather was like during that time, and the latest track I had been jamming to on Spotify.</li> <li>I've been messing around with <a href="https://ryanmulligan.dev/blog/scroll-driven-animations/">scroll-driven animations in CSS</a> a lot and why not be a little extra? When using a supported browser on a wide enough viewport, notice that a progress timer appears on blog posts. The progress ring fills itself up as the page is scrolled.</li> <li>My <a href="https://github.com/hexagoncircle/ryan-mulligan-dev/blob/main/eleventy.config.js">eleventy config</a> is feeling more organized than ever. It was very much inspired by Lene Saile's <a href="https://www.lenesaile.com/en/blog/organizing-the-eleventy-config-file/">Organizing the Eleventy config file</a> article.</li> </ul> <p>It's very early in the morning as I write this. I know I'm forgetting so many of the finer details but I couldn't wait to launch. There is still much for me to clean up and tinker on, but the time feels right to go public. <a href="https://fosstodon.org/@hexagoncircle">Toot at me on Mastodon</a> and tell me what you think!</p> <p>...&quot;Toot at me&quot; may not be the best way to word that.</p> We can :has it all 2023-12-19T00:00:00Z https://ryanmulligan.dev/blog/we-can-has-it-all/ <p><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:has">The functional <code>:has()</code> CSS pseudo class</a> is now shipping in all evergreen browsers! 🎉</p> <p>With <a href="https://www.mozilla.org/en-US/firefox/121.0/releasenotes/">the release of Firefox 121.0</a>, I'm excited to see that my semi-dusty <code>:has()</code> demos are finally realizing their full potential in Firefox. The amount of opportunity unlocked with this selector seems nearly infinite. It can simplify some of the more complex CSS selectors and hacks used in the past. It also opens the door to replacing JavaScript solutions that weren't yet possible to achieve with only CSS.</p> <p>This post is merely a celebration of <code>:has()</code> browser support and shares a quick dive into some of my previous experiments. At the end of this article are <a href="https://ryanmulligan.dev/blog/we-can-has-it-all/#helpful-resources">helpful resources</a> that do an amazing job explaining how the selector works and the unbelieveable power it gives us.</p> <h2 id="themes-layouts-and-filters">Themes, layouts, and filters</h2> <p>This first demo showcases how <code>:has()</code> can be used to set a dark mode, change the layout, and toggle the visibility of elements. All of it can be achieved through a combination of the <code>:has()</code> and <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:checked"><code>:checked</code></a> selectors.</p> <p class="codepen" data-height="600" data-preview="false" data-default-tab="result" data-slug-hash="KKBBXQO" data-user="hexagoncircle"> <a href="https://codepen.io/hexagoncircle/full/KKBBXQO"> <svg class="icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512" fill="currentcolor"><path d="M502.285 159.704l-234-156c-7.987-4.915-16.511-4.96-24.571 0l-234 156C3.714 163.703 0 170.847 0 177.989v155.999c0 7.143 3.714 14.286 9.715 18.286l234 156.022c7.987 4.915 16.511 4.96 24.571 0l234-156.022c6-3.999 9.715-11.143 9.715-18.286V177.989c-.001-7.142-3.715-14.286-9.716-18.285zM278 63.131l172.286 114.858-76.857 51.429L278 165.703V63.131zm-44 0v102.572l-95.429 63.715-76.857-51.429L234 63.131zM44 219.132l55.143 36.857L44 292.846v-73.714zm190 229.715L61.714 333.989l76.857-51.429L234 346.275v102.572zm22-140.858l-77.715-52 77.715-52 77.715 52-77.715 52zm22 140.858V346.275l95.429-63.715 76.857 51.429L278 448.847zm190-156.001l-55.143-36.857L468 219.132v73.714z"></path></svg> <span>Open CodePen demo</span> </a> </p> <script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script> <p>To pull off this primitive filtering technique, each card has a <code>data-category</code> attribute. When a filter option is selected, only the cards with that particular category will remain visible. Check out the following HTML example:</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>article</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>card<span class="token punctuation">"</span></span> <span class="token attr-name">data-category</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>bakery<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token comment">&lt;!-- card contents --></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>article</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>article</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>card<span class="token punctuation">"</span></span> <span class="token attr-name">data-category</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>taquería<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token comment">&lt;!-- card contents --></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>article</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>article</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>card<span class="token punctuation">"</span></span> <span class="token attr-name">data-category</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>café<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token comment">&lt;!-- card contents --></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>article</span><span class="token punctuation">></span></span></code></pre> <p>If the <code>bakery</code> filter option is selected, then the second and third cards would be hidden. Here's the CSS that hides all non-bakery cards:</p> <pre class="language-scss"><code class="language-scss"><span class="token property">body</span><span class="token punctuation">:</span><span class="token function">has</span><span class="token punctuation">(</span>[name=<span class="token string">"filter"</span>][value=<span class="token string">"bakery"</span>]<span class="token punctuation">:</span>checked<span class="token punctuation">)</span> .<span class="token property">card</span><span class="token punctuation">:</span><span class="token function">not</span><span class="token punctuation">(</span>[data-category=<span class="token string">"bakery"</span>]<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token property">display</span><span class="token punctuation">:</span> none<span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p class="callout"><code>[name=&quot;filter&quot;]</code> can be omitted from the selector above given the current circumstances. As things get more complex, however, there could be value overlaps that cause unintended results. This explicitness does raise <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity">specificity</a>, but it can be reduced by using a <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:where"><code>:where()</code></a> selector if preferred. Semi-related: <a href="https://blogs.windows.com/msedgedev/2023/01/17/the-truth-about-css-selector-performance/">The truth about CSS selector performance</a> is a good read!</p> <p>Using <code>:has()</code> to alter layout and filter collections of elements is incredibly powerful. Although, let's understand that there are important accessibility considerations to make here. Don't do something like this in production without ensuring all folks are enabled with a proper experience.</p> <h2 id="skate-or-theme">Skate or theme!</h2> <p>In the next demo, the select dropdown acts as a progressive enhancement. The skateboard's theme will update based on the option selected. The following is a simplified example:</p> <pre class="language-scss"><code class="language-scss"><span class="token selector">:root </span><span class="token punctuation">{</span> <span class="token property">--color</span><span class="token punctuation">:</span> black<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token property">body</span><span class="token punctuation">:</span><span class="token function">has</span><span class="token punctuation">(</span>[value=<span class="token string">"lightning"</span>]<span class="token punctuation">:</span>checked<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token property">--color</span><span class="token punctuation">:</span> yellow<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token property">body</span><span class="token punctuation">:</span><span class="token function">has</span><span class="token punctuation">(</span>[value=<span class="token string">"holiday"</span>]<span class="token punctuation">:</span>checked<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token property">--color</span><span class="token punctuation">:</span> green<span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p class="codepen" data-height="600" data-preview="false" data-default-tab="result" data-slug-hash="GRBJLwE" data-user="hexagoncircle"> <a href="https://codepen.io/hexagoncircle/full/GRBJLwE"> <svg class="icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512" fill="currentcolor"><path d="M502.285 159.704l-234-156c-7.987-4.915-16.511-4.96-24.571 0l-234 156C3.714 163.703 0 170.847 0 177.989v155.999c0 7.143 3.714 14.286 9.715 18.286l234 156.022c7.987 4.915 16.511 4.96 24.571 0l234-156.022c6-3.999 9.715-11.143 9.715-18.286V177.989c-.001-7.142-3.715-14.286-9.716-18.285zM278 63.131l172.286 114.858-76.857 51.429L278 165.703V63.131zm-44 0v102.572l-95.429 63.715-76.857-51.429L234 63.131zM44 219.132l55.143 36.857L44 292.846v-73.714zm190 229.715L61.714 333.989l76.857-51.429L234 346.275v102.572zm22-140.858l-77.715-52 77.715-52 77.715 52-77.715 52zm22 140.858V346.275l95.429-63.715 76.857 51.429L278 448.847zm190-156.001l-55.143-36.857L468 219.132v73.714z"></path></svg> <span>Open CodePen demo</span> </a> </p> <script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script> <p>This project has always been one of my personal favorites. It brings me a fair amount of joy seeing it working fully in Firefox.</p> <h2 id="helpful-resources">Helpful resources</h2> <ul> <li><a href="https://www.smashingmagazine.com/2023/01/level-up-css-skills-has-selector/">Level Up Your CSS Skills With The :has() Selector</a></li> <li><a href="https://ishadeed.com/article/css-has-parent-selector/">CSS :has Parent Selector</a></li> <li><a href="https://webkit.org/blog/13096/css-has-pseudo-class/">Using :has() as a CSS Parent Selector and much more</a></li> <li><a href="https://tobiasahlin.com/blog/previous-sibling-css-has/">Selecting previous siblings with CSS :has()</a></li> <li><a href="https://www.bram.us/2021/12/21/the-css-has-selector-is-way-more-than-a-parent-selector/">The CSS :has() selector is way more than a “Parent Selector”</a></li> </ul> <target-toggler> Web Component 2023-12-22T00:00:00Z https://ryanmulligan.dev/blog/target-toggler/ <p>There are very rare occasions that I want <code>&lt;details&gt;</code> element disclosure widget-style funtionality but would like to have the <code>&lt;summary&gt;</code> element detached or live outside of it's related <code>&lt;details&gt;</code> container. This commonly stems from designs that may, for example, expect a toggle button to appear inline with other controls or content. Here's my attempt at a Web Component to handle that pattern.</p> <ul> <li><a href="https://github.com/hexagoncircle/target-toggler">Source code</a></li> <li><a href="https://hexagoncircle.github.io/target-toggler/demo.html">Demo</a></li> </ul> <p>The gist of this component is to enhance an HTML <code>&lt;button&gt;</code> with the ability to toggle an element's visibility anywhere on a page. Simply wrap a button element with this component and supply a <code>target-id</code> attribute that matches the <code>id</code> of any page element.</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>module<span class="token punctuation">"</span></span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>target-toggler.js<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>target-toggler</span> <span class="token attr-name">target-id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>more-info<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span><span class="token punctuation">></span></span>Show more info<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>target-toggler</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>section</span><span class="token punctuation">></span></span>A special announcement<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>section</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>section</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>more-info<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token comment">&lt;!-- some additional information --></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>section</span><span class="token punctuation">></span></span></code></pre> <p>In the above example, a <code>hidden</code> attribute will be added to the targeted <code>more-info</code> element. Now the button toggle can control the visibility of that piece of content. Want that content to be visible by default? Add a <code>target-visible</code> attribute.</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>target-toggler</span> <span class="token attr-name">target-id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>more-info<span class="token punctuation">"</span></span> <span class="token attr-name">target-visible</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span><span class="token punctuation">></span></span>Show more info<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>target-toggler</span><span class="token punctuation">></span></span></code></pre> <p>Be sure to check out <a href="https://hexagoncircle.github.io/target-toggler/demo.html">the demo page</a> for examples of this component in action.</p> <h2 id="improvements">Improvements</h2> <p>Want to weigh in? <a href="https://github.com/hexagoncircle/target-toggler/issues/new">Add a new issue</a> to the repo and share your ideas! I highly value any community feedback on how to improve this implementation.</p> <h2 id="helpful-resources">Helpful resources</h2> <ul> <li><a href="https://open-ui.org/components/invokers.explainer/">Open UI: Invoker Buttons</a></li> <li><a href="https://12daysofweb.dev/2023/web-components/">12 days of Web: Web Components</a></li> </ul> Click Spark 2024-01-02T00:00:00Z https://ryanmulligan.dev/blog/click-spark/ <p>Last week I had made this fun little experiment. When clicking or tapping on the page, sparks (of joy) fly out from the mouse cursor/tap position. It started with me just messing around a bit in CodePen, but after sharing and getting a few friendly nudges on <a href="https://mastodon.social/@hexagoncircle@fosstodon.org/111659424760546483">my Mastodon post</a>, this fun little experiment evolved into the <code>&lt;click-spark&gt;</code> Web Component which is now available in a <a href="https://github.com/hexagoncircle/click-spark">GitHub repo</a>.</p> <p>Try it out in the CodePen demo below.</p> <p class="codepen" data-height="350" data-preview="false" data-default-tab="result" data-slug-hash="bGZdWyw" data-user="hexagoncircle"> <a href="https://codepen.io/hexagoncircle/pen/bGZdWyw"> <svg class="icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512" fill="currentcolor"><path d="M502.285 159.704l-234-156c-7.987-4.915-16.511-4.96-24.571 0l-234 156C3.714 163.703 0 170.847 0 177.989v155.999c0 7.143 3.714 14.286 9.715 18.286l234 156.022c7.987 4.915 16.511 4.96 24.571 0l234-156.022c6-3.999 9.715-11.143 9.715-18.286V177.989c-.001-7.142-3.715-14.286-9.716-18.285zM278 63.131l172.286 114.858-76.857 51.429L278 165.703V63.131zm-44 0v102.572l-95.429 63.715-76.857-51.429L234 63.131zM44 219.132l55.143 36.857L44 292.846v-73.714zm190 229.715L61.714 333.989l76.857-51.429L234 346.275v102.572zm22-140.858l-77.715-52 77.715-52 77.715 52-77.715 52zm22 140.858V346.275l95.429-63.715 76.857 51.429L278 448.847zm190-156.001l-55.143-36.857L468 219.132v73.714z"></path></svg> <span>Open CodePen demo</span> </a> </p> <script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script> <p>The spark color can be modified by setting a color value to the <code>--click-spark-color</code> custom property:</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>click-spark</span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">--click-spark-color</span><span class="token punctuation">:</span> hotpink</span><span class="token punctuation">"</span></span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>click-spark</span><span class="token punctuation">></span></span></code></pre> <p><strong>Updated on July 26th, 2024</strong> — The update below was a bit silly of me in retrospect. I'm going to leave it for the sake of keeping a rich history. I've pushed a <em>new</em> release that will instead contain the click sparks to the parent element. I believe this is a much cleaner implementation than what I initially attempted. Check out <a href="https://github.com/hexagoncircle/click-spark/pull/3">pull request #3</a> if you'd like to dig into the details.</p> <p><strong>Updated on January 5th, 2023</strong> — I had been thinking about a case where I'd like to have click sparks, but only when clicking on particular elements. I've updated the <a href="https://github.com/hexagoncircle/click-spark">code</a> so that an <code>active-on</code> attribute can be set on the custom element to target a comma-separated list of selectors. If any of the selectors match, let the sparks fly. Here's a <a href="https://codepen.io/hexagoncircle/pen/rNReOPd">CodePen demo</a> of the following example.</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>click-spark</span> <span class="token attr-name">active-on</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>.send-sparks, #i-love-sparks, [data-sparks]<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>click-spark</span><span class="token punctuation">></span></span></code></pre> <p>Have your sparks your way. ✨</p> Using External Links as GitHub Issue Template Options 2024-01-18T00:00:00Z https://ryanmulligan.dev/blog/external-links-issue-template-options/ <p>I've been down the road of <a href="https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository">configuring custom issue templates</a> on GitHub repos before. It even seems like there have been some nice improvements to help make creating them even easier. Thanks for setting me up with a reasonable bug report template to start from so I don't have to build one from scratch. 🐛</p> <p>However, teams may rely on a variety of other tools—Jira, Asana, Linear, an Excel spreadsheet (Kidding! But maybe?)—to manage a backlog of tasks. I'd prefer teammates not add new issues on the repo only to then recreate those items in some other workflow. It would be great if folks could be guided to the right place and avoid double entry.</p> <p>There is a solution for this! It's new to me, so in the spirit of &quot;today I learned&quot;, here's a quick tip for us to share.</p> <h2 id="creating-custom-config">Creating custom config</h2> <p>We can <a href="https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository#configuring-the-template-chooser">configure the template chooser</a> with external links and even lock down the ability to open new issues on the repo. To do this, we create a <code>config.yml</code> file in the repo's <code>.github/ISSUE_TEMPLATE</code> directory.</p> <p class="callout">The <code>.github/ISSUE_TEMPLATE</code> directory may not exist yet if templates have not been configured for the repo. If it needs to be manually created, the path should start at the root of the project.</p> <pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">blank_issues_enabled</span><span class="token punctuation">:</span> <span class="token boolean important">false</span> <span class="token key atrule">contact_links</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> 🐛 Bug Report <span class="token key atrule">url</span><span class="token punctuation">:</span> <span class="token punctuation">[</span>link to create bug report<span class="token punctuation">]</span> <span class="token key atrule">about</span><span class="token punctuation">:</span> Please file a bug report in our team's app of choice. <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> 💡 Feature Request <span class="token key atrule">url</span><span class="token punctuation">:</span> <span class="token punctuation">[</span>link to create feature request<span class="token punctuation">]</span> <span class="token key atrule">about</span><span class="token punctuation">:</span> Have some great ideas to improve our site or this codebase<span class="token punctuation">?</span> Open a new feature request in our team's app of choice.</code></pre> <p>The screenshot below shows how these items will render on the &quot;issue chooser&quot; page. The config explicitly sets only the two options, both linking to their respective places where new tasks can be added to the team's backlog.</p> <p><picture><source type="image/webp" srcset="https://ryanmulligan.dev/images/WsW3LTCvaw-100.webp 100w, https://ryanmulligan.dev/images/WsW3LTCvaw-400.webp 400w, https://ryanmulligan.dev/images/WsW3LTCvaw-800.webp 800w, https://ryanmulligan.dev/images/WsW3LTCvaw-1280.webp 1280w" sizes="100vw" /><img alt="Screenshot of custom external options added to the GitHub issue selection interface." src="https://ryanmulligan.dev/images/WsW3LTCvaw-100.jpeg" width="1280" height="335" srcset="https://ryanmulligan.dev/images/WsW3LTCvaw-100.jpeg 100w, https://ryanmulligan.dev/images/WsW3LTCvaw-400.jpeg 400w, https://ryanmulligan.dev/images/WsW3LTCvaw-800.jpeg 800w, https://ryanmulligan.dev/images/WsW3LTCvaw-1280.jpeg 1280w" sizes="100vw" /></picture></p> <p>The <code>blank_issues_enabled: false</code> line in the config code hides the &quot;open a blank issue&quot; hyperlink that normally appears below these custom options. Without this line, folks would still have the ability to add a new issue on the repo.</p> <p>With all of this in order, the repo remains free of user-entered issues, reducing some friction and redundancy. As an aside: I'm not 100% certain, but I'd wager that automated bot issues would still appear in the repo's issue queue.</p> CSS Scroll-triggered Animations with Style Queries 2024-01-27T00:00:00Z https://ryanmulligan.dev/blog/scroll-triggered-animations-style-queries/ <p>Topping my CSS wishlist in 2024 are <a href="https://developer.chrome.com/docs/css-ui/scroll-driven-animations">scroll-driven animations</a> and <a href="https://developer.chrome.com/docs/css-ui/style-queries">style queries</a>. At the time of writing this post, both lack full support but I've got fingers crossed they become available in all evergreen browsers not too long from now. I had done some <a href="https://ryanmulligan.dev/blog/scroll-driven-animations/">exploration of scroll-driven animations</a> but have not yet spent much time with style queries beyond reading and daydreaming about the amazing possibilities they'll unlock.</p> <h2 id="discovery-zone">Discovery zone</h2> <p>I happened upon <a href="https://codepen.io/jh3y/pen/qBgRLxb">a CodePen by Jhey Tompkins</a> that kicked off my curiosity. In that demo, as the page is scrolled, animations are triggered that smoothly highlight passages of text within the copy. It's all powered by CSS. That's incredible! I've achieved this effect in past demos using <a href="https://codepen.io/hexagoncircle/pen/gOPMwvd">GSAP ScrollTrigger</a> and the <a href="https://codepen.io/hexagoncircle/pen/OJMXZzB">Intersection Observer API</a>. How is this same concept accomplished with only CSS?</p> <p>Diving into Jhey's code, we find a <code>--highlighted</code> custom property set to <code>0</code>. Using scroll-driven animations, the value is updated to <code>1</code> as the <code>mark</code> element reaches the end of its <code>animation-range</code>. That value is passed into a <code>calc()</code> function that transitions a <code>background-position</code> property to create the highlighting effect.</p> <p>This scroll-driven animation mimics intersection observer functionality. In fact, if we inspect the JS panel in the CodePen editor, we'll find that Jhey provides an intersection observer fallback for browsers that don't support CSS view progress timelines.</p> <h2 id="scroll-driven-animations-and-style-queries-join-forces">Scroll-driven animations and style queries join forces</h2> <p>That demo got me jazzed. What else might be possible with this bonafied CSS trick? Could we also trigger a <code>@keyframes</code> animation sequence? I've tested and scrapped a handful of ideas, deciding that it may not be feasible in scroll-driven animations. At least not without a little help from a new friend.</p> <p>Style queries give us the ability to supply styling based on the value of a parent CSS custom property. Ahmad Shadeed's <a href="https://ishadeed.com/article/css-container-style-queries/">Style Queries</a> deep dive demonstrates this in a variety of ways. I ran with Jhey's view progress timeline approach, &quot;toggling&quot; a custom property value in a <code>@keyframes</code> ruleset, then added a style query that triggered an animation on a child element.</p> <p>How about that—it works! 🎉 Or rather, it works in browsers that support both style queries and scroll-driven animations. When unsupported, the demo falls back to displaying the text without the animations.</p> <p class="codepen" data-height="600" data-preview="false" data-default-tab="result" data-slug-hash="wvOPmGO" data-user="hexagoncircle"> <a href="https://codepen.io/hexagoncircle/pen/wvOPmGO"> <svg class="icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512" fill="currentcolor"><path d="M502.285 159.704l-234-156c-7.987-4.915-16.511-4.96-24.571 0l-234 156C3.714 163.703 0 170.847 0 177.989v155.999c0 7.143 3.714 14.286 9.715 18.286l234 156.022c7.987 4.915 16.511 4.96 24.571 0l234-156.022c6-3.999 9.715-11.143 9.715-18.286V177.989c-.001-7.142-3.715-14.286-9.716-18.285zM278 63.131l172.286 114.858-76.857 51.429L278 165.703V63.131zm-44 0v102.572l-95.429 63.715-76.857-51.429L234 63.131zM44 219.132l55.143 36.857L44 292.846v-73.714zm190 229.715L61.714 333.989l76.857-51.429L234 346.275v102.572zm22-140.858l-77.715-52 77.715-52 77.715 52-77.715 52zm22 140.858V346.275l95.429-63.715 76.857 51.429L278 448.847zm190-156.001l-55.143-36.857L468 219.132v73.714z"></path></svg> <span>Open CodePen demo</span> </a> </p> <script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script> <p>The magic is in the following CSS code. It has been stripped back from the demo CSS to focus on the trigger animation specifics.</p> <pre class="language-scss"><code class="language-scss"><span class="token selector">.box </span><span class="token punctuation">{</span> <span class="token property">animation</span><span class="token punctuation">:</span> trigger <span class="token function">steps</span><span class="token punctuation">(</span>1<span class="token punctuation">)</span> both<span class="token punctuation">;</span> <span class="token property">animation-timeline</span><span class="token punctuation">:</span> <span class="token function">view</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">animation-range</span><span class="token punctuation">:</span> entry 80% contain 40%<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token atrule"><span class="token rule">@container</span> <span class="token function">style</span><span class="token punctuation">(</span><span class="token property">--animate</span><span class="token punctuation">:</span> <span class="token boolean">true</span><span class="token punctuation">)</span></span> <span class="token punctuation">{</span> <span class="token selector">.text </span><span class="token punctuation">{</span> <span class="token comment">/* animate! */</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token atrule"><span class="token rule">@keyframes</span> trigger</span> <span class="token punctuation">{</span> <span class="token selector">to </span><span class="token punctuation">{</span> <span class="token property">--animate</span><span class="token punctuation">:</span> <span class="token boolean">true</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre> <p class="callout">Note that only the <code>animation-range</code> end value is relevant for the trigger. Declaring <code>animation-range-end: contain 40%</code> instead would also work here. However, the demo includes the start value to explicitly set where the <code>fade</code> animation starts on the same element.</p> <p>Once the <code>.box</code> element reaches the end of the <code>animation-range</code>, the <code>trigger</code> animation runs instantly, sets <code>--animate: true</code> on the element, then kicks off the elastic popup and background gradient transition on its child <code>.text</code> element. If the page is scrolled back up, the text recedes back to its starting position.</p> <h2 id="additional-thoughts">Additional thoughts</h2> <p>I find this fascinating. Modern CSS continues to deliver fresh delight. However, keep in mind that the CodePen demo works well here because the animated elements are hidden outside of the viewport on initial page load. We'd see the text animate on load if it were visible on screen, which may not be ideal. There are a few ways to handle supressing animation playback on load using JavaScript but I'd love to have this control through a CSS rule.</p> <p>Another thought I had, which Bramus asks the reader in the intro of his <a href="https://www.bram.us/2023/10/05/run-a-scroll-driven-animation-only-once/">article about scroll-driven animations</a>:</p> <blockquote> <p>[...] what if you want a scroll-driven animation to stay on its endframe once it was entirely played?</p> </blockquote> <p>Play through one and done? Sounds like an excellent option. Unfortunately, this cannot be done in CSS but Bramus created a set of <a href="https://github.com/bramus/sda-utilities">scroll-driven animation utilities</a> which includes a way to run a scroll-driven animation only once.</p> <p>Have any feedback or other ideas? Come and <a href="https://fosstodon.org/@hexagoncircle/111829670640360211">join the conversation</a> on Mastodon.</p> <p><strong>Updated on January 29th, 2024</strong>: Bramus shared with me his own <a href="https://www.bram.us/2023/06/15/scroll-triggered-animations/">experiment with this concept</a> from last year. The article does an excellent job explaining how it works and I recommend checking it out. Our conclusions on this seem to be the same.</p> <blockquote> <p>This was a fun experiment to do. However, it’s only an experiment and to me makes the case that we still need proper Scroll-Triggered Animations in the future – maybe something to work on for <code>scroll-animations-2</code>? 😉</p> </blockquote> <p>Now there's a sequel I would be excited to see. 👀</p> The Fifty-Fifty Split and Overflow 2024-02-19T00:00:00Z https://ryanmulligan.dev/blog/50-50-overflow/ <p>The fifty-fifty split—or 50/50 for a dash of brevity—is a classic layout pattern where two elements occupy the same amount of inline space inside a row. These two elements will stack once it becomes too narrow to properly display them side by side. Both flexbox and CSS grid can accommodate this pattern.</p> <p>I had recently shared a demo on CodePen built with this layout pattern, but the component in the demo contains an extra feature: The content of one section overflows and can be scrolled. Try it out by scrolling the section sandwiched between the &quot;header&quot; and &quot;footer&quot; elements.</p> <p class="codepen" data-height="600" data-preview="false" data-default-tab="result" data-slug-hash="PoLdzzo" data-user="hexagoncircle"> <a href="https://codepen.io/hexagoncircle/pen/PoLdzzo"> <svg class="icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512" fill="currentcolor"><path d="M502.285 159.704l-234-156c-7.987-4.915-16.511-4.96-24.571 0l-234 156C3.714 163.703 0 170.847 0 177.989v155.999c0 7.143 3.714 14.286 9.715 18.286l234 156.022c7.987 4.915 16.511 4.96 24.571 0l234-156.022c6-3.999 9.715-11.143 9.715-18.286V177.989c-.001-7.142-3.715-14.286-9.716-18.285zM278 63.131l172.286 114.858-76.857 51.429L278 165.703V63.131zm-44 0v102.572l-95.429 63.715-76.857-51.429L234 63.131zM44 219.132l55.143 36.857L44 292.846v-73.714zm190 229.715L61.714 333.989l76.857-51.429L234 346.275v102.572zm22-140.858l-77.715-52 77.715-52 77.715 52-77.715 52zm22 140.858V346.275l95.429-63.715 76.857 51.429L278 448.847zm190-156.001l-55.143-36.857L468 219.132v73.714z"></path></svg> <span>Open CodePen demo</span> </a> </p> <script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script> <p>Let's find out how it all works. We'll jump into flexbox and grid versions of the 50/50 layout as well as how to handle overflow scrolling.</p> <h2 id="the-flexbox-50-50">The flexbox 50/50</h2> <p>In the <a href="https://codepen.io/hexagoncircle/pen/YzgdVEp">50/50 flexbox layout demo</a>, the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/flex-basis"><code>flex-basis</code></a> value represents how tight the section elements within the container can get before wrapping. <code>flex-grow: 1</code> insists these sections grow beyond their <code>flex-basis</code> value to equally fill the inline space. Once the container becomes too narrow, the two sections will stack.</p> <pre class="language-scss"><code class="language-scss"><span class="token selector">.fifty-fifty </span><span class="token punctuation">{</span> <span class="token property">display</span><span class="token punctuation">:</span> flex<span class="token punctuation">;</span> <span class="token property">flex-wrap</span><span class="token punctuation">:</span> wrap<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">.fifty-fifty > * </span><span class="token punctuation">{</span> <span class="token property">flex-grow</span><span class="token punctuation">:</span> 1<span class="token punctuation">;</span> <span class="token property">flex-basis</span><span class="token punctuation">:</span> 250px<span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p class="callout">In the CodePen demos, we'll find a <code>--min-inline-size</code> variable is utilized. I've removed it from these article code blocks to keep them simple. The reason for having it? It acts as a configuration property for the <code>.fifty-fifty</code> selector. When necessary, we can set a custom value to it and override how tight the sections get before they stack. Otherwise, it'll use the fallback value.</p> <h2 id="the-grid-50-50">The grid 50/50</h2> <p>This same layout can be achieved with CSS grid as we'll discover in the <a href="https://codepen.io/hexagoncircle/pen/poYYoLX">50/50 grid layout demo</a>.</p> <ul> <li>A columns template is declared to represent the two sections.</li> <li><code>auto-fit</code> expands the columns so that they evenly fill the parent container.</li> <li>A <code>minmax()</code> function provides the minimum width for when the sections should stack once the parent container becomes too narrow.</li> </ul> <pre class="language-scss"><code class="language-scss"><span class="token selector">.fifty-fifty </span><span class="token punctuation">{</span> <span class="token property">display</span><span class="token punctuation">:</span> grid<span class="token punctuation">;</span> <span class="token property">grid-template-columns</span><span class="token punctuation">:</span> <span class="token function">repeat</span><span class="token punctuation">(</span> auto-fit<span class="token punctuation">,</span> <span class="token function">minmax</span><span class="token punctuation">(</span>250px<span class="token punctuation">,</span> 1fr<span class="token punctuation">)</span> <span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <h2 id="scrolling-overflow-content">Scrolling overflow content</h2> <p>Imagine a design requirement in which one of the sections within the 50/50 is scrollable. The intrinsic height of the non-scrolling side should instruct the overall height of the parent container. Problem is, we can't just declare a static height value because the content in the non-scrolling section could change. Maybe the text is translated by the user. Maybe the font size is increased for readability.</p> <p>This seems tricky to pull off, but both the flexbox and grid patterns can accommodate such a feature. The code blocks below contain the essential HTML and CSS for the grid version. We can also get a look at how both versions work in this <a href="https://codepen.io/hexagoncircle/pen/qBvvdbg">CodePen demo</a>.</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>article</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>fifty-fifty<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>section</span><span class="token punctuation">></span></span> <span class="token comment">&lt;!-- this content controls overall height --></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>section</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>section</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>scroll-container<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token comment">&lt;!-- overflowing section content --></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>section</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>article</span><span class="token punctuation">></span></span></code></pre> <pre class="language-scss"><code class="language-scss"><span class="token selector">.fifty-fifty </span><span class="token punctuation">{</span> <span class="token property">display</span><span class="token punctuation">:</span> grid<span class="token punctuation">;</span> <span class="token property">grid-template-columns</span><span class="token punctuation">:</span> <span class="token function">repeat</span><span class="token punctuation">(</span>auto-fit<span class="token punctuation">,</span> <span class="token function">minmax</span><span class="token punctuation">(</span>250px<span class="token punctuation">,</span> 1fr<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">grid-auto-rows</span><span class="token punctuation">:</span> 1fr<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">.scroll-container </span><span class="token punctuation">{</span> <span class="token property">contain</span><span class="token punctuation">:</span> size<span class="token punctuation">;</span> <span class="token property">overflow-y</span><span class="token punctuation">:</span> auto<span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>My original take on this has been massively improved after some <a href="https://front-end.social/@kizu/111959588855601850">feedback from Roma Komarov</a>. I totally slept on the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/contain"><code>contain</code></a> property! After setting the <code>contain: size</code> rule, I can drop my previous iteration which had an additional nested element with absolute positioning applied. This simplifies both the template and styles. Thank you, Roma! 👏</p> <p>Now, when the sections are side by side, the height of the <code>fifty-fifty</code> container is based on the size of the non-scrolling section.</p> <h3 id="visually-collapsed-when-stacked">Visually collapsed when stacked</h3> <p>For either layout, it is important to apply a <em>minimum</em> height on the scrollable section. Otherwise, when the sections stack, the scrolling section would disappear visually. Although, if we use CSS grid, we get an extra twist of magic: the <code>grid-auto-rows</code> property. Instead of setting a static &quot;magic number&quot; minimum height, <code>grid-auto-rows: 1fr</code> can be declared. What makes this an attractive alternative is that, whether stacked or split, the two sections are always the same size. In other words, the scrollable section height will consistently mirror the height of the non-scrolling section.</p> <h2 id="where-it-all-started">Where it all started</h2> <p>The <a href="https://codepen.io/hexagoncircle/pen/PoLdzzo">CodePen demo</a> introduced at the beginning of the article has its rules organized another way. Since there are distinct border styles applied depending on whether the sections are stacked or split, we'll find that a media query ruleset handles the layout breakpoint instead. When the browser viewport is larger than the <code>min-width</code> in the query, each section is explicitly set to <code>1fr</code> to distribute their inline sizes evenly. The code below focuses on those specifics.</p> <pre class="language-scss"><code class="language-scss"><span class="token selector">.container </span><span class="token punctuation">{</span> <span class="token property">display</span><span class="token punctuation">:</span> grid<span class="token punctuation">;</span> <span class="token property">grid-auto-rows</span><span class="token punctuation">:</span> 1fr<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">.details </span><span class="token punctuation">{</span> <span class="token property">border-block-start</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--border<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">min-width</span><span class="token punctuation">:</span> 40rem<span class="token punctuation">)</span></span> <span class="token punctuation">{</span> <span class="token selector">.container </span><span class="token punctuation">{</span> <span class="token property">grid-template-columns</span><span class="token punctuation">:</span> 1fr 1fr<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">.details </span><span class="token punctuation">{</span> <span class="token property">border-block-start</span><span class="token punctuation">:</span> unset<span class="token punctuation">;</span> <span class="token property">border-inline-start</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--border<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre> <p>If we desired more modular control than a media query can provide, we could consider using a <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_queries">container query</a> to manage these styles. A solid enhancement to add when it becomes necessary.</p> <h2 id="helpful-resources">Helpful resources</h2> <ul> <li><a href="https://codepen.io/collection/NqoVpN">Fifty-Fifty Split CodePen collection</a></li> <li><a href="https://stackoverflow.com/questions/48943233/how-can-you-set-the-height-of-an-outer-div-to-always-be-equal-to-a-particular-in/48943583#48943583">Equal height and scrolling solutions on Stack Overflow</a></li> <li><a href="https://blog.logrocket.com/using-css-contain-property-deep-dive/">Using the CSS contain property: A deep dive</a></li> </ul> Someone Great 2024-03-13T00:00:00Z https://ryanmulligan.dev/blog/someone-great/ <p>Where to start? The grief is heavy and far too real. I barely slept last night. The brain fog is thick but finding the words and placing them here may provide some catharsis.</p> <p>I lost someone great. Oscar was so much bigger than most folks will ever understand. His love, not just for my partner and I, but for all walks of life was unconditional. His level of patience and kindness is aspirational. I've never met someone like him. I reflect fondly on the memories of him, some of my best years being filled with his presence every single day.</p> <p>Oscar was an avid US traveler, bouncing back and forth between the coasts. He spent much of his life right outside NYC being a friend of the people. I often dubbed him the mayor of downtown Jersey City. Countless times when Oscar and I went out for a stroll together passersby would gush with joy, interacting with Oscar as if nobody else existed. He walked proud, eyes beaming, perpetually present in the moment.</p> <p>It was also in Jersey City where my partner and I became great friends. It was where we fell in love. Oscar was integral to our story. We'd trek all over the city with him, hanging around the local coffee shop, strolling along the waterfront, sprawling out in the grass of Liberty State Park. Our conversations were natural, meaningful, and incredibly deep. Oscar was there with us, always attentive, reacting with his calming stare and gentle grin. At the beginning of our relationship, his approval meant everything. Oscar made me feel accepted.</p> <p>When the pandemic lockdowns started, Oscar insisted we get outdoors for socially-distanced walks, leading us around an exceptionally quiet city. Being with him helped maintain our sanity during such a difficult experience. We spent a majority of our time quarantined in a small studio apartment, but Oscar was there urging us to keep our cool, letting us know it was going to be alright. His warm aura would fill the room. We felt it. In that challenging time, we felt at peace. We felt joy.</p> <p>When travel began to return, lockdown restrictions being lifted, we made a decision to spend our days on the opposite coast. Once we got there, Oscar needed no time to adjust. He immediately embraced his golden years, escaping the harsh Northeast winters and heavy springtime rain for the endless sunny beaches of southern California. It brought my partner and I so much delight seeing his excitement. The new sites, sounds, the smells—everything was so fresh and welcome. We'd head down to the beach where Oscar would stretch across the sand and soak up the sun. He would smile back at us. Everything felt right.</p> <p>At home, Oscar would watch intently as we cooked meals. When the doorbell rang, he'd jump up to greet guests with glee. I'd play the acoustic guitar and Oscar would settle nearby to listen, often drifting off to sleep. He never said it, but I'm pretty confident he enjoyed hearing me play. I tell myself that anyway.</p> <p>Late at night, the three of us would snuggle up on the couch and watch television. Oscar was typically first to leave for bed. If it were getting too late, he'd sometimes peek his head back into the room. His stare seemed to say, &quot;It's bedtime, you two, come along!&quot; Then he'd stalk off as quickly as he appeared. In retrospect, he was probably right as we woke up groggy the next morning.</p> <p>I'd like to continue but I've reached a breaking point emotionally. My eyes are starting to swell. There is so much wonder around Oscar, so many stories to share. He elevated the mood of anyone that met him. On a personal level, he made me feel important and loved in times I felt small or invisible. There was not and never will be anyone that matches his unique disposition. He was our emotional companion and a beautiful friend.</p> <p>I love you, Oscar. You are forever missed.</p> Detect JavaScript Support in CSS 2024-04-20T00:00:00Z https://ryanmulligan.dev/blog/detect-js-support-in-css/ <p>I had been aware of the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/scripting"><code>scripting</code> CSS media feature</a> but I was still under the impression that cross-browser support was lacking. What a pleasant surprise to discover that it has been available in all modern browsers as of December 2023 according to <a href="https://caniuse.com/?search=scripting">caniuse.com</a>. With this feature, we can provide alternative CSS rules depending on whether or not JavaScript is available in the user's browser. It can also help reduce flashes of unstyled content or undesirable layout shifts.</p> <p><em>Before we dive in:</em> As exciting as this feature is, I've learned that there are a couple unfortunate gotchas. I've amended the article with an <a href="https://ryanmulligan.dev/blog/detect-js-support-in-css/#issues">issues</a> section below.</p> <h2 id="usage">Usage</h2> <p>We can progressively enhance our styles:</p> <pre class="language-scss"><code class="language-scss"><span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">scripting</span><span class="token punctuation">:</span> enabled<span class="token punctuation">)</span></span> <span class="token punctuation">{</span> <span class="token selector">.my-element </span><span class="token punctuation">{</span> <span class="token comment">/* enhanced styles if JS is available */</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre> <p>Or we can gracefully fall back to some alternate styles:</p> <pre class="language-scss"><code class="language-scss"><span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">scripting</span><span class="token punctuation">:</span> none<span class="token punctuation">)</span></span> <span class="token punctuation">{</span> <span class="token selector">.my-element </span><span class="token punctuation">{</span> <span class="token comment">/* fallback styles when JS is not supported */</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre> <p>There's also an <code>initial-only</code> value, which is for scripting that is enabled during page load but not after. The <a href="https://www.w3.org/TR/mediaqueries-5/#scripting">Media Queries Level 5 W3C Working Draft</a> includes a couple cases where it can be useful.</p> <blockquote> <p>Examples are printed pages, or pre-rendering network proxies that render a page on a server and send a nearly-static version of the page to the user.</p> </blockquote> <p>I don't personally imagine using <code>initial-only</code> much, if ever. Although, I'd be interested to find more specific examples of it in practice.</p> <h2 id="the-time-before-the-query">The time before the query</h2> <p>Before this feature, one approach for detecting JavaScript support was by setting a custom selector on the opening <code>html</code> tag—a common one seen in the wild is the <code>no-js</code> class name. If JavaScript is supported and enabled, it removes that selector just prior to rendering page content. When JavaScript is disabled, we can supply alternative styles that adapt to the experience.</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>html</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>no-js<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token comment">&lt;!-- page content --></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>html</span><span class="token punctuation">></span></span></code></pre> <pre class="language-scss"><code class="language-scss"><span class="token selector">.no-js .my-element </span><span class="token punctuation">{</span> <span class="token comment">/* styles when JS is disabled */</span> <span class="token punctuation">}</span></code></pre> <h2 id="is-this-real-life">Is this real life?</h2> <p>Imagine a new web campaign is on the cusp of going live and it's time to connect with all the key stakeholders. Everything looks great, most of the team satisfied with the result, but then suddenly some hip marketer in the meeting emphatically requests a complex intro animation on the hero component when the page loads. They gesture wildly as they ask for the main headline to fade in, shrink away as if it were being pulled back on a sling shot, and then... at this point they make an explosion noise with their mouth. &quot;Make it pop!&quot; they decree a mere 24 hours before launch.</p> <p>Woof. Better get started.</p> <p>To handle the complexity of this work, we might reach for an animation library such as <a href="https://gsap.com/">GSAP</a>. But what does the user see when JavaScript is not available, not to mention if a user's <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion"><code>prefers reduced motion</code></a> setting is enabled? We'll need to consider an alternate version of the hero without all that swooping and scaling.</p> <p>This media query unlocks the ability to provide CSS rules that are a better fit to the user's experience. In the CodePen demo below, if we disable JavaScript, we'll find that the animation is skipped and the static headline is displayed.</p> <p class="codepen" data-height="600" data-preview="false" data-default-tab="result" data-slug-hash="e1e382620c5897b4c72fa29b36227fd4" data-user="hexagoncircle"> <a href="https://codepen.io/hexagoncircle/pen/NWmEMmJ/e1e382620c5897b4c72fa29b36227fd4"> <svg class="icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512" fill="currentcolor"><path d="M502.285 159.704l-234-156c-7.987-4.915-16.511-4.96-24.571 0l-234 156C3.714 163.703 0 170.847 0 177.989v155.999c0 7.143 3.714 14.286 9.715 18.286l234 156.022c7.987 4.915 16.511 4.96 24.571 0l234-156.022c6-3.999 9.715-11.143 9.715-18.286V177.989c-.001-7.142-3.715-14.286-9.716-18.285zM278 63.131l172.286 114.858-76.857 51.429L278 165.703V63.131zm-44 0v102.572l-95.429 63.715-76.857-51.429L234 63.131zM44 219.132l55.143 36.857L44 292.846v-73.714zm190 229.715L61.714 333.989l76.857-51.429L234 346.275v102.572zm22-140.858l-77.715-52 77.715-52 77.715 52-77.715 52zm22 140.858V346.275l95.429-63.715 76.857 51.429L278 448.847zm190-156.001l-55.143-36.857L468 219.132v73.714z"></path></svg> <span>Open CodePen demo</span> </a> </p> <script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script> <h2 id="watch-that-flash">Watch that flash</h2> <p>To really make the intro animation feel smooth on page load, the demo relies on the <code>scripting</code> media query to hide the headline with CSS. By doing so, we won't catch a flash of unstyled text before the GSAP animation is loaded. Also, we only want to hide the headline if JavaScript <em>is</em> available, otherwise it would be hidden for users when it's disabled.</p> <p>In the following video, watch what happens when the headline is not hidden on page load. The text flashing is even more glaring when throttling on a slower network.</p> <figure class="video"> <video preload="metadata" loop="" muted="" playsinline="" controls=""> <source src="https://ryanmulligan.dev/videos/detect-js-support-in-css.webm#t=0.001" type="video/webm" /> <source src="https://ryanmulligan.dev/videos/detect-js-support-in-css.mp4#t=0.001" type="video/mp4" /> <p>Your browser cannot play the provided video file.</p> </video> <figcaption>In the video, the headline is no longer hidden on page load to share that pesky flash of unstyled text. When emulating slower network speeds, the issue becomes even more egregious.</figcaption></figure> <h2 id="combining-queries">Combining queries</h2> <p>In the CSS tab of the demo, notice that the media queries are combined to check both scripting and reduced-motion conditions.</p> <pre class="language-scss"><code class="language-scss"><span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">scripting</span><span class="token punctuation">:</span> enabled<span class="token punctuation">)</span> <span class="token operator">and</span> <span class="token punctuation">(</span><span class="token property">prefers-reduced-motion</span><span class="token punctuation">:</span> no-preference<span class="token punctuation">)</span></span> <span class="token punctuation">{</span> <span class="token comment">/* JS available and motion OK */</span> <span class="token punctuation">}</span> <span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">scripting</span><span class="token punctuation">:</span> none<span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">(</span>prefers-reduced-motion<span class="token punctuation">)</span></span> <span class="token punctuation">{</span> <span class="token comment">/* JS disabled or reduced motion enabled */</span> <span class="token punctuation">}</span></code></pre> <p>Each condition can surely have exclusive styles if the desired outcome calls for it, but it's nice that we can combine them where there's overlap in rulesets.</p> <h2 id="issues">Issues</h2> <p><strong>Updated on April 21st, 2024</strong> - After publishing this post, some feedback surfaced explaining where this media feature unexpectedly fails.</p> <ol> <li>It does not behave as anticipated when a browser extension such as NoScript or uBlock Origin is used to disable page scripts. <code>scripting: enabled</code> still matches even though the extension has JavaScript turned off.</li> <li>If a script gets blocked or fails to load, a fallback would need to be handled via JavaScript. In the demo above, the fallback would need to tap into the demo's <code>scripting: none</code> media query ruleset so that the static version of the hero is displayed.</li> </ol> <p>Tremendous thanks to <a href="https://front-end.social/@SaraSoueidan/112307456267714875">Sara</a>, <a href="https://mastodon.social/@simevidas/112305703318360235">Šime</a>, and <a href="https://mastodon.social/@pepelsbey/112308080752283580">Vadim</a> for sharing!</p> <h2 id="helpful-resources">Helpful resources</h2> <ul> <li><a href="https://blog.stephaniestimac.com/posts/2023/12/css-media-query-scripting/">CSS Media Query for Scripting Support</a></li> <li><a href="https://www.stefanjudis.com/blog/how-to-detect-disabled-javascript-in-css/">New on the web: How to detect disabled JavaScript in CSS</a></li> <li><a href="https://www.matuzo.at/blog/2023/100daysof-day106">Day 106: the scripting media feature</a></li> </ul> Penguin! 2024-05-06T00:00:00Z https://ryanmulligan.dev/blog/penguin/ <p>My partner and I finally finished our epic three-part claymation saga. It's our first attempt at creating stop-motion videos with clay and it truly is <em>a lot</em> of work for such a short result. <a href="https://www.youtube.com/watch?v=LCUze7kuNas">That claymation episode of Parks and Recreation</a> is a little too real. I won't compare our movie to Avatar though.</p> <p>Regardless, we had a blast doing it. Making art with our hands brings a different style of satisfaction. Anyway, I'll keep this post short and get to it. Enjoy!</p> <p><a href="https://www.youtube.com/watch?v=3mUdINzprdI">https://www.youtube.com/watch?v=3mUdINzprdI</a></p> Notes on handling events in Web Components 2024-07-27T00:00:00Z https://ryanmulligan.dev/blog/handling-events-web-components/ <p>My <a href="https://ryanmulligan.dev/blog/click-spark/"><code>click-spark</code> web component</a> was a fun, silly project at best. Yet I've seen it's had some love since being shared. So why not publish it as an <a href="https://www.npmjs.com/package/click-spark">npm package</a>? No better time than the present, some say.</p> <p>I had done a major refactor before publishing, the most notable was that the spark would now be contained to the custom element's parent node. After announcing the updates in a <a href="https://fosstodon.org/@hexagoncircle/112855152216537788">Mastodon post</a>, I soon received a <a href="https://github.com/hexagoncircle/click-spark/pull/7#discussion_r1693933865">PR</a> with some quality feedback including a more advantageous way to handle the parent click event using the <code>handleEvent()</code> method.</p> <h2 id="the-click-spark-click">The click-spark click</h2> <p>Let's dive into a &quot;before and after&quot; for handling the click event on this component.</p> <p class="callout">In both examples, notice that the parent node is being stored in a variable when <code>connectedCallback</code> runs. This ensures that the click event is properly removed from the parent since it's not available by the time <code>disconnectedCallback</code> is invoked as <a href="https://github.com/hexagoncircle/click-spark/pull/7#discussion_r1693936577">FND's comment</a> explains.</p> <p>In the &quot;before&quot; approach, the event handler is stored in a variable so that it's cleared from the parent node whenever the <code>click-spark</code> element is removed from the DOM.</p> <pre class="language-js"><code class="language-js"><span class="token function">constructor</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">this</span><span class="token punctuation">.</span>clickEvent <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">handleClick</span><span class="token punctuation">.</span><span class="token function">bind</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token function">connectedCallback</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">this</span><span class="token punctuation">.</span>_parent <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>parentNode<span class="token punctuation">;</span> <span class="token keyword">this</span><span class="token punctuation">.</span>_parent<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">"click"</span><span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span>clickEvent<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token function">disconnectedCallback</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">this</span><span class="token punctuation">.</span>_parent<span class="token punctuation">.</span><span class="token function">removeEventListener</span><span class="token punctuation">(</span><span class="token string">"click"</span><span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span>clickEvent<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">delete</span> <span class="token keyword">this</span><span class="token punctuation">.</span>_parent<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token function">handleClick</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// Run code on click</span> <span class="token punctuation">}</span></code></pre> <p>Switching to <code>handleEvent()</code> removes the need to store the event handler. Passing <code>this</code> into the event listener will run the component's <code>handleEvent()</code> method every time we click.</p> <pre class="language-js"><code class="language-js"><span class="token function">constructor</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// No longer need to store the callback</span> <span class="token punctuation">}</span> <span class="token function">connectedCallback</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">this</span><span class="token punctuation">.</span>_parent <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>parentNode<span class="token punctuation">;</span> <span class="token keyword">this</span><span class="token punctuation">.</span>_parent<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">"click"</span><span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token function">disconnectedCallback</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">this</span><span class="token punctuation">.</span>_parent<span class="token punctuation">.</span><span class="token function">removeEventListener</span><span class="token punctuation">(</span><span class="token string">"click"</span><span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">delete</span> <span class="token keyword">this</span><span class="token punctuation">.</span>_parent<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token function">handleEvent</span><span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// Run code on click</span> <span class="token punctuation">}</span></code></pre> <h2 id="helpful-resources">Helpful resources</h2> <p>Chris Ferdinandi's article, <a href="https://gomakethings.com/the-handleevent-method-is-the-absolute-best-way-to-handle-events-in-web-components/">The handleEvent() method is the absolute best way to handle events in Web Components</a> is an excellent read on why <code>handleEvent()</code> is a top choice for architecting event listeners in Web Components. He also shares insight from Andrea Giammarchi's <a href="https://webreflection.medium.com/dom-handleevent-a-cross-platform-standard-since-year-2000-5bf17287fd38">DOM handleEvent: a cross-platform standard since year 2000</a> article which contains solid techniques for handling multiple event types.</p> <p>Rather than me regurgitating, I recommend jumping into both of those articles to get a proper grasp on <code>handleEvent()</code>.</p> <p><strong>Updated on July 29th, 2024</strong> — FND published <a href="https://prepitaph.org/articles/event-garbage/">Garbage Collection for Event Listeners</a> about event listener cleanup after an element is removed from the DOM. The included demo is a nice touch. One interesting footnote that stood out to me: moving the element to a new location in the DOM will invoke <code>disconnectedCallback</code> before invoking <code>connectedCallback</code> again.</p> Center Items in First Row with CSS Grid 2024-08-19T00:00:00Z https://ryanmulligan.dev/blog/grid-stacks/ <p>Imagine the following section on a website:</p> <ol> <li>A collection of elements, like a series of cards with marketing information, are presented in a grid display.</li> <li>The elements are arranged in rows of three.</li> <li>When there are an odd number of elements left over, they will be center-aligned horizontally.</li> </ol> <p>There are a few ways to accomplish styling such a layout. <a href="https://css-irl.info/controlling-leftover-grid-items/">Controlling Leftover Grid Items with Pseudo-selectors</a> by Michelle Barker shares a clever CSS Grid solution. But here's a twist: What if the centered odd number of elements should appear in the <em>first</em> row instead of the last?</p> <p>I've included a <a href="https://ryanmulligan.dev/blog/grid-stacks/#demo">CodePen demo</a> at the end of this article if you'd like to jump ahead. Otherwise, continue on a journey of style discovery.</p> <h2 id="grid-stacks">Grid Stacks</h2> <p>Is it a trapezoid grid? Brick grid? Grid pyramid? Pyragrid? For the sake of this article, I ultimately picked a more generic name, calling it <em>Grid Stack</em>. Here's how we'll build a <em>Grid Stack</em> that contains five cards displayed in a three-column grid.</p> <pre class="language-scss"><code class="language-scss"><span class="token selector">.grid-stack </span><span class="token punctuation">{</span> <span class="token property">display</span><span class="token punctuation">:</span> grid<span class="token punctuation">;</span> <span class="token property">grid-template-columns</span><span class="token punctuation">:</span> <span class="token function">repeat</span><span class="token punctuation">(</span>6<span class="token punctuation">,</span> 1fr<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token selector">> * </span><span class="token punctuation">{</span> <span class="token property">grid-column-end</span><span class="token punctuation">:</span> span 2<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">> :first-child </span><span class="token punctuation">{</span> <span class="token property">grid-column-start</span><span class="token punctuation">:</span> 2<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre> <p class="callout"><a href="https://caniuse.com/?search=nesting">CSS nesting</a> is being used here, which is newly supported across major browsers. Not feeling ready for that yet? We can move the nested rules into their own top-level rulesets. They just need to start with the parent selector name, i.e. <code>.grid-stack &gt; * { }</code></p> <p>The parent <code>grid-stack</code> container produces a template with six columns. Notice that the <code>grid-template-columns</code> repeat count is double the amount of columns we want visually present in each row. Each child element will then span across two columns instead of one. Finally, the first child element is aligned to the start of the second column. The result is a visually centered top row.</p> <figure><picture><source type="image/webp" srcset="https://ryanmulligan.dev/images/wo3rHkZkNz-100.webp 100w, https://ryanmulligan.dev/images/wo3rHkZkNz-400.webp 400w, https://ryanmulligan.dev/images/wo3rHkZkNz-800.webp 800w, https://ryanmulligan.dev/images/wo3rHkZkNz-1280.webp 1280w" sizes="100vw" /><img alt="A screenshot showcasing grid lines of a container element on a webpage. The Chrome dev tools panel is open, including the Layout panel which has options for customizing the grid lines." src="https://ryanmulligan.dev/images/wo3rHkZkNz-100.jpeg" width="1280" height="760" srcset="https://ryanmulligan.dev/images/wo3rHkZkNz-100.jpeg 100w, https://ryanmulligan.dev/images/wo3rHkZkNz-400.jpeg 400w, https://ryanmulligan.dev/images/wo3rHkZkNz-800.jpeg 800w, https://ryanmulligan.dev/images/wo3rHkZkNz-1280.jpeg 1280w" sizes="100vw" /></picture><figcaption>Grid lines (enabled in dev tools) help show where each child element is positioned on the grid.</figcaption></figure> <h2 id="variations">Variations</h2> <p>So far, the styles we've created only apply when there are five cards positioned across three columns. We may want different variations depending on our designs. Three cards displayed in two columns? Seven cards in four? Let's tweak the above ruleset to utilize a CSS variable for the <code>grid-template-columns</code> repeat count. Recall that this value should be twice the amount of expected columns.</p> <pre class="language-scss"><code class="language-scss"><span class="token selector">.grid-stack </span><span class="token punctuation">{</span> <span class="token property">display</span><span class="token punctuation">:</span> grid<span class="token punctuation">;</span> <span class="token property">grid-template-columns</span><span class="token punctuation">:</span> <span class="token function">repeat</span><span class="token punctuation">(</span><span class="token function">calc</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--columns<span class="token punctuation">)</span> <span class="token operator">*</span> 2<span class="token punctuation">)</span><span class="token punctuation">,</span> 1fr<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token selector">> * </span><span class="token punctuation">{</span> <span class="token property">grid-column-end</span><span class="token punctuation">:</span> span 2<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">> :first-child </span><span class="token punctuation">{</span> <span class="token property">grid-column-start</span><span class="token punctuation">:</span> 2<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre> <p>The <code>--columns</code> value gets doubled as it passes through the CSS <code>calc()</code> function. Now we're able to define the preferred amount of columns directly on the parent container.</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>grid-stack<span class="token punctuation">"</span></span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">--columns</span><span class="token punctuation">:</span> 2</span><span class="token punctuation">"</span></span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>grid-stack<span class="token punctuation">"</span></span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">--columns</span><span class="token punctuation">:</span> 3</span><span class="token punctuation">"</span></span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>grid-stack<span class="token punctuation">"</span></span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">--columns</span><span class="token punctuation">:</span> 4</span><span class="token punctuation">"</span></span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span></code></pre> <h2 id="demo">Demo</h2> <p class="codepen" data-height="600" data-preview="false" data-default-tab="result" data-slug-hash="gONvbNL" data-user="hexagoncircle"> <a href="https://codepen.io/hexagoncircle/pen/gONvbNL"> <svg class="icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512" fill="currentcolor"><path d="M502.285 159.704l-234-156c-7.987-4.915-16.511-4.96-24.571 0l-234 156C3.714 163.703 0 170.847 0 177.989v155.999c0 7.143 3.714 14.286 9.715 18.286l234 156.022c7.987 4.915 16.511 4.96 24.571 0l234-156.022c6-3.999 9.715-11.143 9.715-18.286V177.989c-.001-7.142-3.715-14.286-9.716-18.285zM278 63.131l172.286 114.858-76.857 51.429L278 165.703V63.131zm-44 0v102.572l-95.429 63.715-76.857-51.429L234 63.131zM44 219.132l55.143 36.857L44 292.846v-73.714zm190 229.715L61.714 333.989l76.857-51.429L234 346.275v102.572zm22-140.858l-77.715-52 77.715-52 77.715 52-77.715 52zm22 140.858V346.275l95.429-63.715 76.857 51.429L278 448.847zm190-156.001l-55.143-36.857L468 219.132v73.714z"></path></svg> <span>Open CodePen demo</span> </a> </p> <script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script> <h2 id="bonus-pyramid-stacks">Bonus! Pyramid stacks</h2> <p>In the above demo, you may have discovered some configurations for the <em>Grid Stack</em> that result in a pyramid-style stack. Maybe <em>now</em> we can call it a pyragrid? Still not sure about that one... Anyway, to achieve this layout involves a few extra ingredients. We'll need to adjust the <code>grid-column-start</code> position of the first element in each row. Let's jump right to the last example with the <code>grid-stack-15</code> selector:</p> <pre class="language-scss"><code class="language-scss"><span class="token selector">.grid-stack-15 </span><span class="token punctuation">{</span> <span class="token property">--columns</span><span class="token punctuation">:</span> 5<span class="token punctuation">;</span> <span class="token selector">> :first-child </span><span class="token punctuation">{</span> <span class="token property">grid-column-start</span><span class="token punctuation">:</span> 5<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token operator">></span> <span class="token punctuation">:</span><span class="token function">nth-child</span><span class="token punctuation">(</span>2<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token property">grid-column-start</span><span class="token punctuation">:</span> 4<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token operator">></span> <span class="token punctuation">:</span><span class="token function">nth-child</span><span class="token punctuation">(</span>4<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token property">grid-column-start</span><span class="token punctuation">:</span> 3<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token operator">></span> <span class="token punctuation">:</span><span class="token function">nth-child</span><span class="token punctuation">(</span>7<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token property">grid-column-start</span><span class="token punctuation">:</span> 2<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre> <ul> <li>This calls for a five-column grid visually, so it sets <code>--columns: 5</code>. Recall that this value gets doubled and outputs a template of ten columns.</li> <li>We'll nudge the first item in each row with <code>grid-column-start</code>. The top row element's start position is equal to the <code>--column</code> value. Subsequent rules will decrease this value by 1.</li> </ul> <p>It's surely possible to develop a Sass or PostCSS function that could dynamically generate this CSS output but that seemed a bit overkill for the demo. As an added bonus, check out Temani Afif's <a href="https://stackoverflow.com/a/67267124">Stack Overflow answer</a> that styles elements in a pyramid using <code>float</code> and <code>shape-outside</code>. Very cool!</p> <h2 id="limitations">Limitations</h2> <p>Each layout variation expects a specific odd-number of child elements to be rendered. I have explored ways of automatically adjusting the layout based on the element count but there were too many edge cases to consider. It created more problems than it solved. Additionally, while these layouts work nicely on a wider viewport, it may not fare as well where less space is available. A <code>media</code> or <code>container</code> query ruleset can ensure our content adapts appropriately, but it certainly couldn't be a one-size-fits-all conditional set of styles.</p> <h2 id="from-the-community">From the community</h2> <p><em>Updated on September 5th</em> — <a href="https://fosstodon.org/@kevinpowell@front-end.social/113085254739679608">Kevin Powell reached out on Mastodon</a> and shared a <a href="https://codepen.io/kevinpowell/pen/abgQreW">CodePen Example</a> that uses <code>:has(:nth-child():last-child)</code> to result in this layout. I certainly dig the approach! Keep in mind that the same aforementioned limitations still apply.</p> CSS @property and the New Style 2024-09-02T00:00:00Z https://ryanmulligan.dev/blog/css-property-new-style/ <p>The <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@property"><code>@property</code></a> at-rule recently gained support across all modern browsers, unlocking the ability to explicitly define a syntax, initial value, and inheritance for CSS custom properties. It seems like forever ago that CSS Houdini and its <a href="https://developer.mozilla.org/en-US/docs/Web/API/CSS_Properties_and_Values_API">CSS Properties and Values API</a> were initially introduced. I experimented sparingly over time, reading articles that danced around the concepts, but I had barely scratched the surface of what <code>@property</code> could offer. The ensuing demo explores what's possible in the next generation of CSS.</p> <h2 id="calls-to-action">Calls to action</h2> <p>Ever seen those sleek, attention-seeking, shiny call-to-action webpage elements? Waves of sites across the web, especially the ones marketing services and software urging for you to &quot;Upgrade your account&quot; or &quot;Sign up today,&quot; have discovered the look and latched on. I'm not here to knock it and admittedly think it's kind of fresh. I thought I'd give that style a try myself. Check out the result in the CodePen below.</p> <p class="codepen" data-height="500" data-preview="false" data-default-tab="result" data-slug-hash="MWMqXbK" data-user="hexagoncircle"> <a href="https://codepen.io/hexagoncircle/pen/MWMqXbK??editors=0100"> <svg class="icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512" fill="currentcolor"><path d="M502.285 159.704l-234-156c-7.987-4.915-16.511-4.96-24.571 0l-234 156C3.714 163.703 0 170.847 0 177.989v155.999c0 7.143 3.714 14.286 9.715 18.286l234 156.022c7.987 4.915 16.511 4.96 24.571 0l234-156.022c6-3.999 9.715-11.143 9.715-18.286V177.989c-.001-7.142-3.715-14.286-9.716-18.285zM278 63.131l172.286 114.858-76.857 51.429L278 165.703V63.131zm-44 0v102.572l-95.429 63.715-76.857-51.429L234 63.131zM44 219.132l55.143 36.857L44 292.846v-73.714zm190 229.715L61.714 333.989l76.857-51.429L234 346.275v102.572zm22-140.858l-77.715-52 77.715-52 77.715 52-77.715 52zm22 140.858V346.275l95.429-63.715 76.857 51.429L278 448.847zm190-156.001l-55.143-36.857L468 219.132v73.714z"></path></svg> <span>Open CodePen demo</span> </a> </p> <script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script> <p>There's a ton to unpack in this demo. Let's start with that shine looping around the button. Toggle open the demo's CSS panel to find a collection of <code>@property</code> rules related to those custom properties that need to animate. Here's the one defined for the <code>--gradient-angle</code>:</p> <pre class="language-scss"><code class="language-scss"><span class="token atrule"><span class="token rule">@property</span> --gradient-angle</span> <span class="token punctuation">{</span> <span class="token property">syntax</span><span class="token punctuation">:</span> <span class="token string">"&lt;angle>"</span><span class="token punctuation">;</span> <span class="token property">initial-value</span><span class="token punctuation">:</span> 0deg<span class="token punctuation">;</span> <span class="token property">inherits</span><span class="token punctuation">:</span> <span class="token boolean">false</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>The <code>@property</code> rule communicates to the browser that <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/angle"><code>&lt;angle&gt;</code></a> is the allowed syntax for this custom property and its initial value is <code>0deg</code>. This enables the browser to smoothly transition from <code>0deg</code> to <code>360deg</code> and output a rotating gradient.</p> <pre class="language-scss"><code class="language-scss"><span class="token atrule"><span class="token rule">@keyframes</span> rotate-gradient</span> <span class="token punctuation">{</span> <span class="token selector">to </span><span class="token punctuation">{</span> <span class="token property">--gradient-angle</span><span class="token punctuation">:</span> 360deg<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token selector">.rotate-gradient </span><span class="token punctuation">{</span> <span class="token property">background</span><span class="token punctuation">:</span> <span class="token function">conic-gradient</span><span class="token punctuation">(</span>from <span class="token function">var</span><span class="token punctuation">(</span>--gradient-angle<span class="token punctuation">)</span><span class="token punctuation">,</span> transparent<span class="token punctuation">,</span> black<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">animation</span><span class="token punctuation">:</span> rotate-gradient 10s linear infinite<span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>I put together a simple gradient spin demo to focus on the handful of lines necessary to render this concept.</p> <p class="codepen" data-height="300" data-preview="true" data-default-tab="result" data-slug-hash="eYwLqJx" data-user="hexagoncircle"> <a href="https://codepen.io/hexagoncircle/pen/eYwLqJx"> <svg class="icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512" fill="currentcolor"><path d="M502.285 159.704l-234-156c-7.987-4.915-16.511-4.96-24.571 0l-234 156C3.714 163.703 0 170.847 0 177.989v155.999c0 7.143 3.714 14.286 9.715 18.286l234 156.022c7.987 4.915 16.511 4.96 24.571 0l234-156.022c6-3.999 9.715-11.143 9.715-18.286V177.989c-.001-7.142-3.715-14.286-9.716-18.285zM278 63.131l172.286 114.858-76.857 51.429L278 165.703V63.131zm-44 0v102.572l-95.429 63.715-76.857-51.429L234 63.131zM44 219.132l55.143 36.857L44 292.846v-73.714zm190 229.715L61.714 333.989l76.857-51.429L234 346.275v102.572zm22-140.858l-77.715-52 77.715-52 77.715 52-77.715 52zm22 140.858V346.275l95.429-63.715 76.857 51.429L278 448.847zm190-156.001l-55.143-36.857L468 219.132v73.714z"></path></svg> <span>Open CodePen demo</span> </a> </p> <script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script> <p>We can achieve the shiny animated border effect by evolving this code a bit. We'll introduce a <code>linear-gradient</code> as the first value of the element's <code>background</code> property and set a <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/background-origin"><code>background-origin</code></a> to each value.</p> <ul> <li>The origin of the <code>linear-gradient</code> is set to <code>padding-box</code>. This prevents the gradient from spilling into the border area.</li> <li>The <code>conic-gradient</code> origin is set to <code>border-box</code>. This gradient overflows into the space created by the border width.</li> <li>To reveal the rotating <code>conic-gradient</code>, a single-pixel transparent border is added.</li> </ul> <pre class="language-scss"><code class="language-scss"><span class="token selector">.border-gradient </span><span class="token punctuation">{</span> <span class="token property">background</span><span class="token punctuation">:</span> <span class="token function">linear-gradient</span><span class="token punctuation">(</span>black<span class="token punctuation">,</span> black<span class="token punctuation">)</span> padding-box<span class="token punctuation">,</span> <span class="token function">conic-gradient</span><span class="token punctuation">(</span>from <span class="token function">var</span><span class="token punctuation">(</span>--gradient-angle<span class="token punctuation">)</span><span class="token punctuation">,</span> transparent 25%<span class="token punctuation">,</span> white<span class="token punctuation">,</span> transparent 50%<span class="token punctuation">)</span> border-box<span class="token punctuation">;</span> <span class="token property">border</span><span class="token punctuation">:</span> 1px solid transparent<span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>In the CSS panel of the <a href="https://ryanmulligan.dev/blog/css-property-new-style/#cp_embed_eYwLqJx">simple gradient spin demo</a>, uncomment the <code>.border-gradient</code> ruleset to reveal the shiny animated border. Looking pretty slick! For more examples, I've included a bunch of animated gradient border articles in the <a href="https://ryanmulligan.dev/blog/css-property-new-style/#helpful-resources">resources section</a> at the end of the post.</p> <h2 id="silky-smooth-hover-transitions">Silky smooth hover transitions</h2> <p>A few special ingredients help facilitate a buttery smooth gradient transition when the element is hovered. Let's dig into its <code>background</code> values:</p> <pre class="language-scss"><code class="language-scss"><span class="token selector">.shiny-cta </span><span class="token punctuation">{</span> <span class="token property">background</span><span class="token punctuation">:</span> <span class="token function">linear-gradient</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--shiny-cta-bg<span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token function">var</span><span class="token punctuation">(</span>--shiny-cta-bg<span class="token punctuation">)</span><span class="token punctuation">)</span> padding-box<span class="token punctuation">,</span> <span class="token function">conic-gradient</span><span class="token punctuation">(</span> <span class="token keyword">from</span> <span class="token function">calc</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--gradient-angle<span class="token punctuation">)</span> <span class="token operator">-</span> <span class="token function">var</span><span class="token punctuation">(</span>--gradient-angle-offset<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span> transparent<span class="token punctuation">,</span> <span class="token function">var</span><span class="token punctuation">(</span>--shiny-cta-highlight<span class="token punctuation">)</span> <span class="token function">var</span><span class="token punctuation">(</span>--gradient-percent<span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token function">var</span><span class="token punctuation">(</span>--gradient-shine<span class="token punctuation">)</span> <span class="token function">calc</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--gradient-percent<span class="token punctuation">)</span> <span class="token operator">*</span> 2<span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token function">var</span><span class="token punctuation">(</span>--shiny-cta-highlight<span class="token punctuation">)</span> <span class="token function">calc</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--gradient-percent<span class="token punctuation">)</span> <span class="token operator">*</span> 3<span class="token punctuation">)</span><span class="token punctuation">,</span> transparent <span class="token function">calc</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--gradient-percent<span class="token punctuation">)</span> <span class="token operator">*</span> 4<span class="token punctuation">)</span> <span class="token punctuation">)</span> border-box<span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>Each custom property that needs to animate has a <code>syntax</code> declared in its <code>@property</code> definition so that the browser can interpolate between corresponding value changes and transition them seamlessly. The size of the shiny area is determined by the <code>--gradient-percent</code> value. On hover, a higher percentage lengthens the shine. The <code>--gradient-angle-offset</code> value is used to readjust the gradient angle so that the shine doesn't rubber band back and forth on hover.</p> <figure class="video"> <video preload="metadata" loop="" muted="" playsinline="" controls=""> <source src="https://ryanmulligan.dev/videos/shiny-cta-angle-offset.webm#t=0.001" type="video/webm" /> <source src="https://ryanmulligan.dev/videos/shiny-cta-angle-offset.mp4#t=0.001" type="video/mp4" /> <p>Your browser cannot play the provided video file.</p> </video> <figcaption>Demonstrating the transition behavior without the angle offset value</figcaption></figure> <p>I had to fine-tune the percent and offset values until the shine length and transition felt optically aligned. Finally, the <code>--gradient-shine</code> brightness gets toned down to blend more seamlessly with the adjacent highlight colors.</p> <h2 id="slow-it-on-down">Slow it on down</h2> <p>This <a href="https://css-tip.com/slow-down-rotation/">CSS tip to slow down a rotation on hover</a> truly blew my mind. In the tip's example code, the same rotate animation is declared twice. The second one is reversed and paused, its duration divided in half. When the element is hovered, <code>animation-play-state: running</code> overrides the <code>paused</code> value and slows the rotation to half speed. The mind-blowing part, at least to me, is that the animation speeds back up at the current position when the element is no longer hovered. No snapping back to a start position, no extra wrapper elements necessary. That is one heck of a tip.</p> <p>The <a href="https://ryanmulligan.dev/blog/css-property-new-style/#cp_embed_MWMqXbK">call-to-action animations</a> rely on this method to slow them down when the button is hovered. This technique keeps all the rotations and movements in sync as they change speed.</p> <h2 id="tiny-shiny-dots">Tiny shiny dots</h2> <p>Looking even closer, we'll discover pinhole-sized dots shimmering inside the button as the shiny border passes near them. To render this dot pattern, a <code>radial-gradient</code> background is created.</p> <pre class="language-scss"><code class="language-scss"><span class="token selector">.shiny-cta::before </span><span class="token punctuation">{</span> <span class="token property">--position</span><span class="token punctuation">:</span> 2px<span class="token punctuation">;</span> <span class="token property">--space</span><span class="token punctuation">:</span> <span class="token function">calc</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--position<span class="token punctuation">)</span> <span class="token operator">*</span> 2<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">background</span><span class="token punctuation">:</span> <span class="token function">radial-gradient</span><span class="token punctuation">(</span> circle at <span class="token function">var</span><span class="token punctuation">(</span>--position<span class="token punctuation">)</span> <span class="token function">var</span><span class="token punctuation">(</span>--position<span class="token punctuation">)</span><span class="token punctuation">,</span> white <span class="token function">calc</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--position<span class="token punctuation">)</span> <span class="token operator">/</span> 4<span class="token punctuation">)</span><span class="token punctuation">,</span> transparent 0 <span class="token punctuation">)</span> padding-box<span class="token punctuation">;</span> <span class="token property">background-size</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--space<span class="token punctuation">)</span> <span class="token function">var</span><span class="token punctuation">(</span>--space<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">background-repeat</span><span class="token punctuation">:</span> space<span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>Remember that <code>--gradient-angle</code> custom property? It has returned! But this time, it's being used in a <code>conic-gradient</code> mask that reveals parts of the dot pattern as it rotates. The gradient angle is offset by 45 degrees to align it perfectly with the shiny border rotation.</p> <pre class="language-scss"><code class="language-scss"><span class="token selector">.shiny-cta::before </span><span class="token punctuation">{</span> <span class="token property">mask-image</span><span class="token punctuation">:</span> <span class="token function">conic-gradient</span><span class="token punctuation">(</span> <span class="token keyword">from</span> <span class="token function">calc</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--gradient-angle<span class="token punctuation">)</span> <span class="token operator">+</span> 45deg<span class="token punctuation">)</span><span class="token punctuation">,</span> black<span class="token punctuation">,</span> transparent 10% 90%<span class="token punctuation">,</span> black <span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>For one last touch of magic, a gradient containing the highlight color is added to the <code>::after</code> pseudo element, spinning in unison with the shine area. These highlights flowing through the button add a pleasant, welcoming ambience that was previously missing.</p> <h2 id="enhancing-the-hover-colors">Enhancing the hover colors</h2> <p>The hover styles looked decent. But they didn't seem totally finished. I felt the desire to enhance. Create more depth. <a href="https://ryanmulligan.dev/blog/detect-js-support-in-css/#:~:text=%22Make%20it%20pop!%22">Make it pop, as they say</a>.</p> <p>The button's <code>::before</code> and <code>::after</code> pseudo elements were already in use so I wrapped the button text in a <code>span</code> element. A blurred <code>box-shadow</code> containing the highlight color is applied to one of its pseudo elements which is then expanded to fill the button dimensions. On hover, the pseudo element slowly scales up and down, evoking a vibe similar to relaxed breathing. Paired with the spinning highlight color inside the button, the effect finally resonated with me. This intricately designed call-to-action button felt complete.</p> <h2 id="in-with-the-new-style">In with the new style</h2> <p>Many of the above techniques would have been nearly impossible only a short time ago. Explicitly defining custom properties unlocks a great big world of opportunity. I'm especially eager to see how <code>@property</code> will be utilized in large-scale applications and design systems. <a href="https://moderncss.dev/providing-type-definitions-for-css-with-at-property/">Providing Type Definitions for CSS with @property</a> by Stephanie Eckles as well as Adam Argyle's <a href="https://nerdy.dev/cant-break-this-design-system">Type safe CSS design systems with @property</a> are just a couple glimpses into a really promising future for publishing our CSS.</p> <h2 id="helpful-resources">Helpful resources</h2> <ul> <li><a href="https://www.learnwithjason.dev/blog/animated-css-gradient-border/">Animated CSS gradient borders (no JavaScript, no hacks)</a></li> <li><a href="https://ibelick.com/blog/create-animated-gradient-borders-with-css">Creating an animated gradient border with CSS</a></li> <li><a href="https://web.dev/articles/css-border-animations">CSS border animations</a></li> <li><a href="https://www.bram.us/2021/01/29/animating-a-css-gradient-border/">Animating a CSS Gradient Border</a></li> <li><a href="https://codepen.io/hexagoncircle/full/LYKJPjm">CSS border ripple effect</a></li> <li><a href="https://www.smashingmagazine.com/2024/05/times-need-custom-property-instead-css-variable/">The Times You Need A Custom @property Instead Of A CSS Variable</a></li> <li><a href="https://web.dev/blog/at-property-baseline">@property: Next-gen CSS variables now with universal browser support</a></li> </ul> Web Components for Password Input Enhancements 2024-09-20T00:00:00Z https://ryanmulligan.dev/blog/password-input-components/ <p>So there I was, experimenting with HTML password inputs and Web Components. I'm not sure why the idea even came up but it quickly snowballed into a curious expedition. The result from the journey was a set of custom elements that provide extra functionality and information about the text being typed into a password input field. I shared my <a href="https://codepen.io/hexagoncircle/pen/LYKKjmj">CodePen demo</a> in a <a href="https://fosstodon.org/@hexagoncircle/113164872411660242">Mastodon post</a> and soon after decided to push these scripts up to a <a href="https://github.com/hexagoncircle/password-input-components">GitHub repo</a>.</p> <p class="codepen" data-height="450" data-preview="false" data-default-tab="result" data-slug-hash="LYKKjmj" data-user="hexagoncircle"> <a href="https://codepen.io/hexagoncircle/pen/LYKKjmj"> <svg class="icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512" fill="currentcolor"><path d="M502.285 159.704l-234-156c-7.987-4.915-16.511-4.96-24.571 0l-234 156C3.714 163.703 0 170.847 0 177.989v155.999c0 7.143 3.714 14.286 9.715 18.286l234 156.022c7.987 4.915 16.511 4.96 24.571 0l234-156.022c6-3.999 9.715-11.143 9.715-18.286V177.989c-.001-7.142-3.715-14.286-9.716-18.285zM278 63.131l172.286 114.858-76.857 51.429L278 165.703V63.131zm-44 0v102.572l-95.429 63.715-76.857-51.429L234 63.131zM44 219.132l55.143 36.857L44 292.846v-73.714zm190 229.715L61.714 333.989l76.857-51.429L234 346.275v102.572zm22-140.858l-77.715-52 77.715-52 77.715 52-77.715 52zm22 140.858V346.275l95.429-63.715 76.857 51.429L278 448.847zm190-156.001l-55.143-36.857L468 219.132v73.714z"></path></svg> <span>Open CodePen demo</span> </a> </p> <script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script> <h2 id="get-started">Get started</h2> <p>The repo includes two Web Component scripts. They operate independent of one another. I recommend reading through <a href="https://github.com/hexagoncircle/password-input-components/blob/main/README.md">the repo documentation</a> but here's a rundown of what's included.</p> <ul> <li><code>&lt;password-rules&gt;</code> adds an <code>input</code> event listener to capture when a list of rules (password length, includes an uppercase letter, etc.) are matched as the user is typing in their new password.</li> <li><code>&lt;password-toggle&gt;</code> shows and hides the password input value on click.</li> </ul> <p>To get started, add the scripts to a project and include them on the page.</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>module<span class="token punctuation">"</span></span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>path/to/password-rules.js<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>module<span class="token punctuation">"</span></span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>path/to/password-toggle.js<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span></code></pre> <p>Below is an example of using both custom elements with a password input.</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span> <span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>new-password<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Password<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>password<span class="token punctuation">"</span></span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>new-password<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>status<span class="token punctuation">"</span></span> <span class="token attr-name">aria-live</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>polite<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>password-toggle</span> <span class="token attr-name">data-input-id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>new-password<span class="token punctuation">"</span></span> <span class="token attr-name">data-status-id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>status<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>button<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Toggle password visibility<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>password-toggle</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>password-rules</span> <span class="token attr-name">data-input-id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>new-password<span class="token punctuation">"</span></span> <span class="token attr-name">data-rules</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>.{9}, [A-Z], .*\d<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span> <span class="token attr-name">data-rule-index</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>0<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Longer than 8 characters<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span> <span class="token attr-name">data-rule-index</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>1<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Includes an uppercase letter<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span> <span class="token attr-name">data-rule-index</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>2<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Includes a number<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>password-rules</span><span class="token punctuation">></span></span></code></pre> <h2 id="password-toggle">Password toggle</h2> <p><code>password-toggle</code> expects a <code>button</code> element to be inside it. This button will be augmented with the ability to toggle the visibility of the input field's value.</p> <p>When the toggle button is clicked, the &quot;status&quot; element containing the <a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-live"><code>aria-live</code></a> attribute will send a notification to screen readers that the password value is currently visible or hidden. For instance, when clicking for the first time, the string &quot;Password is visible&quot; is inserted into the container and announced by a screen reader.</p> <p>We can also style the toggle button when it enters its <em>pressed</em> or &quot;visible password&quot; state. In the <a href="https://codepen.io/hexagoncircle/pen/LYKKjmj">CodePen demo</a>, this is how the eye icon (aye aye!) is being swapped.</p> <pre class="language-scss"><code class="language-scss"><span class="token selector">button svg:last-of-type </span><span class="token punctuation">{</span> <span class="token property">display</span><span class="token punctuation">:</span> none<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">button[aria-pressed="true"] </span><span class="token punctuation">{</span> <span class="token selector">svg:first-of-type </span><span class="token punctuation">{</span> <span class="token property">display</span><span class="token punctuation">:</span> none<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">svg:last-of-type </span><span class="token punctuation">{</span> <span class="token property">display</span><span class="token punctuation">:</span> block<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre> <p class="callout">Targeting the <code>[aria-pressed]</code> attribute selector ensures that our styles stay in sync with their accessibility counterpart. It also means that we don't need to manage a semantic attribute value as well as some generic class selector like &quot;is-active&quot;. Ben Myers shares great knowledge on this subject in <a href="https://benmyers.dev/blog/semantic-selectors/">Style with stateful, semantic selectors</a>. A must-have in the bookmarks 🏆</p> <h2 id="password-rules">Password rules</h2> <p>The <code>password-rules</code> element is passed a comma-separated list of <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions">regular expression</a> strings, each related to a specific rule. We also have the option to connect any child element to a rule by passing the index of that string to a <code>data-rule-index</code> attribute. The placement or type of element doesn't matter as long as it's contained within the <code>password-rules</code>.</p> <p>Here's an alternate version to drive that point home:</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>password-rules</span> <span class="token attr-name">data-input-id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>new-password<span class="token punctuation">"</span></span> <span class="token attr-name">data-rules</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>.{8}, [A-Z], .*\d<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>one-column<span class="token punctuation">"</span></span> <span class="token attr-name">data-rule-index</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>0<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> Longer than 8 characters <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>two-columns<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">data-rule-index</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>2<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Includes a number<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">data-rule-index</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>1<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Includes an uppercase letter<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>password-rules</span><span class="token punctuation">></span></span></code></pre> <h3 id="check-it-off">Check it off</h3> <p>When a rule is met that matches the <code>data-rule-index</code> value on an element, an <code>is-match</code> class gets added to the element. The <a href="https://codepen.io/hexagoncircle/pen/LYKKjmj">demo</a> styles use this selector to add a checkmark emoji when present.</p> <pre class="language-scss"><code class="language-scss"><span class="token selector">.password-rules__checklist .is-match::before </span><span class="token punctuation">{</span> <span class="token property">content</span><span class="token punctuation">:</span> <span class="token string">"✅"</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <h3 id="score-total">score/total</h3> <p>The current password &quot;score&quot; and rules &quot;total&quot; are passed to the custom element as data attributes and CSS variables. The score value updates as rules are met. This allows us to do some fancy things like change the colors in a score meter and present the current tally. All of it done with CSS.</p> <pre class="language-scss"><code class="language-scss"><span class="token comment">/** Incrementally adjust background colors */</span> password-rules[data-score=<span class="token string">"1"</span>] .<span class="token property">password-rules__meter</span> <span class="token punctuation">:</span>first-child<span class="token punctuation">,</span> password-rules[data-score=<span class="token string">"2"</span>] .<span class="token property">password-rules__meter</span> <span class="token punctuation">:</span><span class="token function">nth-child</span><span class="token punctuation">(</span>-n <span class="token operator">+</span> 2<span class="token punctuation">)</span><span class="token punctuation">,</span> password-rules[data-score=<span class="token string">"3"</span>] .<span class="token property">password-rules__meter</span> <span class="token punctuation">:</span><span class="token function">nth-child</span><span class="token punctuation">(</span>-n <span class="token operator">+</span> 3<span class="token punctuation">)</span><span class="token punctuation">,</span> password-rules[data-score=<span class="token string">"4"</span>] .<span class="token property">password-rules__meter</span> <span class="token punctuation">:</span><span class="token function">nth-child</span><span class="token punctuation">(</span>-n <span class="token operator">+</span> 4<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token property">background-color</span><span class="token punctuation">:</span> dodgerblue<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token comment">/** When all rules are met, swap to a new color for each meter element */</span> password-rules[data-score=<span class="token string">"5"</span>] .<span class="token property">password-rules__meter</span> <span class="token punctuation">:</span><span class="token function">nth-child</span><span class="token punctuation">(</span>-n <span class="token operator">+</span> 5<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token property">background-color</span><span class="token punctuation">:</span> mediumseagreen<span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>CSS variables are passed into a <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_counter_styles/Using_CSS_counters">CSS <code>counter()</code></a> to render the current score and total.</p> <pre class="language-scss"><code class="language-scss"><span class="token selector">.password-rules__score::before </span><span class="token punctuation">{</span> <span class="token property">counter-reset</span><span class="token punctuation">:</span> score <span class="token function">var</span><span class="token punctuation">(</span>--score<span class="token punctuation">,</span> 0<span class="token punctuation">)</span> total <span class="token function">var</span><span class="token punctuation">(</span>--total<span class="token punctuation">,</span> 5<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">content</span><span class="token punctuation">:</span> <span class="token function">counter</span><span class="token punctuation">(</span>score<span class="token punctuation">)</span> <span class="token string">"/"</span> <span class="token function">counter</span><span class="token punctuation">(</span>total<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>I added fallback values to the CSS variables when I realized that the <code>--total</code> value, specifically, renders as <code>0</code> on page load and doesn't update until we begin typing in the input field. I did discover that we could skip the fallback by registering the custom property. This ensures the total is correctly reflected when the component initializes. But, to be honest, this feels unnecessary when the fallback here will suffice.</p> <pre class="language-scss"><code class="language-scss"><span class="token atrule"><span class="token rule">@property</span> --total</span> <span class="token punctuation">{</span> <span class="token property">syntax</span><span class="token punctuation">:</span> <span class="token string">"&lt;number>"</span><span class="token punctuation">;</span> <span class="token property">initial-value</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span> <span class="token property">inherits</span><span class="token punctuation">:</span> <span class="token boolean">true</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p class="callout">If this @property stuff is unfamiliar, Stephanie Eckles has got you covered in <a href="https://moderncss.dev/providing-type-definitions-for-css-with-at-property/">Providing Type Definitions for CSS with @property</a>. Another one to bookmark! I've also recently spent time with this newly supported at-rule in <a href="https://ryanmulligan.dev/blog/css-property-new-style/">CSS @property and the New Style</a>.</p> <h2 id="progressively-enhanced-for-the-win">Progressively enhanced for the win</h2> <p>I believe this tells a fairly nice progressive enhancement story. Without JavaScript, the password input still works as expected. But when these scripts run, users get additional feedback and interactivity. Developers get access to extra selectors that can be useful for styling state changes. And listen, I get it–there are better ways to handle client-side form validation, but this was a fun exploration nonetheless.</p> The Pixel Canvas Shimmer Effect 2024-12-03T00:00:00Z https://ryanmulligan.dev/blog/pixel-canvas/ <p>I recently stumbled on a super cool, well-executed hover effect from the <a href="https://clerk.com/">clerk.com</a> website where a bloom of tiny pixels light up, their glow staggering from the center to the edges of its container. With some available free time over this Thanksgiving break, I hacked together my own version of a pixel canvas background shimmer. It quickly evolved into a <code>pixel-canvas</code> Web Component that can be enjoyed in the demo below. The component script and demo code have also been pushed up to a <a href="https://github.com/hexagoncircle/pixel-canvas">GitHub repo</a>.</p> <p class="codepen" data-height="600" data-preview="false" data-default-tab="result" data-slug-hash="KwPpdBZ" data-user="hexagoncircle"> <a href="https://codepen.io/hexagoncircle/pen/KwPpdBZ"> <svg class="icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512" fill="currentcolor"><path d="M502.285 159.704l-234-156c-7.987-4.915-16.511-4.96-24.571 0l-234 156C3.714 163.703 0 170.847 0 177.989v155.999c0 7.143 3.714 14.286 9.715 18.286l234 156.022c7.987 4.915 16.511 4.96 24.571 0l234-156.022c6-3.999 9.715-11.143 9.715-18.286V177.989c-.001-7.142-3.715-14.286-9.716-18.285zM278 63.131l172.286 114.858-76.857 51.429L278 165.703V63.131zm-44 0v102.572l-95.429 63.715-76.857-51.429L234 63.131zM44 219.132l55.143 36.857L44 292.846v-73.714zm190 229.715L61.714 333.989l76.857-51.429L234 346.275v102.572zm22-140.858l-77.715-52 77.715-52 77.715 52-77.715 52zm22 140.858V346.275l95.429-63.715 76.857 51.429L278 448.847zm190-156.001l-55.143-36.857L468 219.132v73.714z"></path></svg> <span>Open CodePen demo</span> </a> </p> <script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script> <h2 id="usage">Usage</h2> <p>Include the component script and then insert a <code>pixel-canvas</code> custom element inside the container it should fill.</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>module<span class="token punctuation">"</span></span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>pixel-canvas.js<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>container<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>pixel-canvas</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>pixel-canvas</span><span class="token punctuation">></span></span> <span class="token comment">&lt;!-- other elements --></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span></code></pre> <p>The <code>pixel-canvas</code> stretches to the edges of the parent container. When the parent is hovered, glimmering pixel fun ensues.</p> <h2 id="options">Options</h2> <p>The custom element has a few optional attributes available to customize the effect. Check out the CodePen demo's html panel to see how each variation is made.</p> <ul> <li><code>data-colors</code> takes a comma separated list of color values.</li> <li><code>data-gap</code> sets the amount of space between each pixel.</li> <li><code>data-speed</code> controls the general duration of the shimmer. This value is slightly randomized on each pixel that, in my opinion, adds a little more character.</li> <li><code>data-no-focus</code> is a boolean attribute that tells the Web Component to not run its animation whenever sibling elements are focused. The animation runs on sibling focus by default.</li> </ul> <p>There's likely more testing and tweaking necessary before I'd consider using this anywhere, but my goal was to run with this inspiration simply for the joy of coding. What a mesmerizing concept. I tip my hat to the creative engineers over at Clerk.</p> The Shape of Runs to Come 2024-12-08T00:00:00Z https://ryanmulligan.dev/blog/the-shape-of-runs-to-come/ <p>Over the last few months or so, I have been fairly consistent with getting outside for Sunday morning runs. A series of lower body issues had prevented me from doing so for many years, but it was an exercise I had enjoyed back then. It took time to rebuild that habit and muscle but I finally bested the behavior of doing so begrudgingly.</p> <p>Back in the day (what a weird phrase to say, how old am I?) I would purchase digital copies of full albums. I'd use my run time to digest the songs in the order the artist intended. Admittedly, I've become a lazy listener now, relying on streaming services to surface playlists that I mindlessly select to get going. I want to be better than that, but that's a story for another time.</p> <p>These days, my mood for music on runs can vary: Some sessions I'll pop in headphones and throw on some tunes, other times I head out free of devices (besides a watch to track all those sweet, sweaty workout stats) and simply take in the city noise.</p> <p>Before I headed out for my journey this morning, a friend shared a track from <a href="https://refused.bandcamp.com/album/the-shape-of-punk-to-come-obliterated">an album of song covers</a> in tribute to The Refused's <em><a href="https://refused.bandcamp.com/album/the-shape-of-punk-to-come">The Shape Of Punk To Come</a></em>. The original is a treasured classic, a staple LP from my younger years, and I can still remember the feeling of the first time it struck my ears. Its magic is reconjured every time I hear it. When that reverb-soaked feedback starts on <a href="https://refused.bandcamp.com/track/worms-of-the-senses-faculties-of-the-skull">Worms of the Senses / Faculties of the Skull</a>, my heart rate begins to ascend. The anticipation builds, my entire body well aware of the explosion of sound imminent. As my run began, I wasn't sure if I had goosebumps from the morning chill or the wall of noise about to ensue. My legs were already pumping. I was fully present, listening intently, ready for the blast. The sound abruptly detonated sending me rocketing down the street towards the rising sun.</p> <p>My current running goal is <em>4-in-40</em>, traversing four miles under forty minutes. I'm certainly no Prefontaine, but it's a fair enough objective for my age and ability. I'll typically finish my journey in that duration or slightly spill over the forty-minute mark. Today was different. Listening to <em>The Shape Of Punk To Come</em> sent me cruising an extra quarter mile beyond the four before my workout ended. The unstoppable energy from that album is truly pure runner's fuel.</p> <p>There's certainly some layer of nostalgia, my younger spirit awakened and reignited by thrashing guitars and frantic rhythms, but many elements and themes on this record were so innovative at the time it was released. <a href="https://refused.bandcamp.com/track/new-noise">New Noise</a> is a prime example that executes the following feeling flawlessly: Build anticipation, increase the energy level, and then right as the song seems prepped to blast off, switch to something unexpected. In this case, the guitars drop out to make way for some syncopated celestial synths layered over a soft drum rhythm. The energy sits in a holding pattern, unsure whether it should burst or cool down, when suddenly—</p> <blockquote> <p>Can I scream?!</p> </blockquote> <p>Oh my goodness, yes. Yes you can. I quickly morphed into a runner decades younger. I had erupted, my entire being barreling full speed ahead. The midpoint of this track pulls out the same sequence of build up, drop off, and teasing just long enough before unleashing another loud burst of noise, driving to its explosive outro. As the song wraps up, &quot;The New Beat!&quot; is howled repeatedly to a cheering crowd that, I would imagine, had not been standing still.</p> <p>I definitely needed a long stretch after this run.</p> Scrolling Rails and Button Controls 2024-12-23T00:00:00Z https://ryanmulligan.dev/blog/scrolly-rail/ <p>Once again, here I am, hackin' away on horizontal scroll ideas. This iteration starts with a custom HTML tag. All the necessities for scroll overflow, scroll snapping, and row layout are handled with CSS. Then, as a little progressive enhancement treat, <code>button</code> elements are connected that scroll the previous or next set of items into view when clicked.</p> <p>Behold! The holy grail of scrolling rails... the <code>scrolly-rail</code>!</p> <ul> <li><a href="https://codepen.io/hexagoncircle/full/yyBMGrL">CodePen demo</a></li> <li><a href="https://github.com/hexagoncircle/scrolly-rail">GitHub repo</a></li> </ul> <p class="codepen" data-height="600" data-preview="false" data-default-tab="result" data-slug-hash="yyBMGrL" data-user="hexagoncircle"> <a href="https://codepen.io/hexagoncircle/pen/yyBMGrL"> <svg class="icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512" fill="currentcolor"><path d="M502.285 159.704l-234-156c-7.987-4.915-16.511-4.96-24.571 0l-234 156C3.714 163.703 0 170.847 0 177.989v155.999c0 7.143 3.714 14.286 9.715 18.286l234 156.022c7.987 4.915 16.511 4.96 24.571 0l234-156.022c6-3.999 9.715-11.143 9.715-18.286V177.989c-.001-7.142-3.715-14.286-9.716-18.285zM278 63.131l172.286 114.858-76.857 51.429L278 165.703V63.131zm-44 0v102.572l-95.429 63.715-76.857-51.429L234 63.131zM44 219.132l55.143 36.857L44 292.846v-73.714zm190 229.715L61.714 333.989l76.857-51.429L234 346.275v102.572zm22-140.858l-77.715-52 77.715-52 77.715 52-77.715 52zm22 140.858V346.275l95.429-63.715 76.857 51.429L278 448.847zm190-156.001l-55.143-36.857L468 219.132v73.714z"></path></svg> <span>Open CodePen demo</span> </a> </p> <script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script> <p>I'm being quite facetious about the &quot;holy grail&quot; part, if that's not clear. 😅 This is an initial try on an idea I'll likely experiment more with. I've shared some thoughts on potential <a href="https://ryanmulligan.dev/blog/scrolly-rail/#future-improvements">future improvements</a> at the end of the post. On top of that, after I began experimenting, I discovered the possibility of <a href="https://nerdy.dev/css-wishlist-2025#css-carousel">CSS Carousels</a>. If those scroll enhancing features become a cross browser standard, then we'll be able to generate scroll buttons and more, all with CSS. That's a <em>lot</em> of power.</p> <p>With all of that out of the way, let's explore!</p> <h2 id="the-html">The HTML</h2> <p>Wrap any collection of items with the custom tag:</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>scrolly-rail</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span>1<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span>2<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span>3<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token comment">&lt;!-- and so on--></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>scrolly-rail</span><span class="token punctuation">></span></span></code></pre> <p class="callout">While it is possible to have items without a wrapper element, if the custom element script runs and button controls are connected, <em>sentinel</em> elements are inserted at the start and end bounds of the scroll container. Wrapping the items makes controlling spacing between them much easier, avoiding any undesired gaps appearing due to these sentinels. We'll discover <a href="https://ryanmulligan.dev/blog/scrolly-rail/#observing-inline-scroll-bounds">what the sentinels are for</a> later in the post.</p> <h2 id="the-css">The CSS</h2> <p>Here are the main styles for the component:</p> <pre class="language-scss"><code class="language-scss"><span class="token selector">scrolly-rail </span><span class="token punctuation">{</span> <span class="token property">display</span><span class="token punctuation">:</span> flex<span class="token punctuation">;</span> <span class="token property">overflow-x</span><span class="token punctuation">:</span> auto<span class="token punctuation">;</span> <span class="token property">overscroll-behavior-x</span><span class="token punctuation">:</span> contain<span class="token punctuation">;</span> <span class="token property">scroll-snap-type</span><span class="token punctuation">:</span> x mandatory<span class="token punctuation">;</span> <span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">prefers-reduced-motion</span><span class="token punctuation">:</span> no-preference<span class="token punctuation">)</span></span> <span class="token punctuation">{</span> <span class="token property">scroll-behavior</span><span class="token punctuation">:</span> smooth<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre> <ul> <li>When JavaScript is enabled, sentinel elements are inserted before and after the unordered list (<code>ul</code>) element in the HTML example above. Flexbox ensures that the sentinels are positioned on either side of the element. We'll find out why later in this post.</li> <li>Containing the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/overscroll-behavior#contain">overscroll behavior</a> will prevent us accidentally triggering browser navigation when scrolling beyond either edge of the <code>scrolly-rail</code> container.</li> <li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-snap-type"><code>scroll-snap-type</code></a> enforces mandatory scroll snapping.</li> <li>Smooth scrolling behavior applies when items scroll into view on button click, or if interactive elements (links, buttons, etc.) inside items overflowing the visible scroll area are focused.</li> </ul> <p>Any wrapper element, such as the example <code>ul</code>, will need a flex display to position items in a single row and introduce gap spacing if desired. Then <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-snap-align"><code>scroll-snap-align: start</code></a> is applied to each item. This aligns the targeted snap item to the inline start of the component's scroll snap area. In the HTML example, this would apply to the <code>li</code> elements.</p> <pre class="language-scss"><code class="language-scss"><span class="token selector">scrolly-rail ul </span><span class="token punctuation">{</span> <span class="token property">display</span><span class="token punctuation">:</span> flex<span class="token punctuation">;</span> <span class="token property">gap</span><span class="token punctuation">:</span> 1rem<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">scrolly-rail li </span><span class="token punctuation">{</span> <span class="token property">scroll-snap-align</span><span class="token punctuation">:</span> start<span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>As mentioned earlier, this is everything our component needs for layout, inline scrolling, and scroll snapping. Note that the <a href="https://codepen.io/hexagoncircle/pen/yyBMGrL">CodePen demo</a> takes it a step further with some additional padding and margin styles (check out the demo CSS panel). If we'd like to wire up previous/next controls, we'll need to include the custom element script in our HTML.</p> <h2 id="the-custom-element-script">The custom element script</h2> <p>Add the script file on the page.</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>module<span class="token punctuation">"</span></span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>scrolly-rail.js<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span></code></pre> <p>To connect the previous/next <code>button</code> elements, give each an <code>id</code> value and add these values to the <code>data-control-*</code> attributes on the custom tag.</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>scrolly-rail</span> <span class="token attr-name">data-control-previous</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>btn-previous<span class="token punctuation">"</span></span> <span class="token attr-name">data-control-next</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>btn-next<span class="token punctuation">"</span></span> <span class="token punctuation">></span></span> <span class="token comment">&lt;!-- ... --></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>scrolly-rail</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>btn-previous<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>btn-scrolly-rail<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Previous<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>btn-next<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>btn-scrolly-rail<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Next<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">></span></span></code></pre> <p>Now clicking these buttons will pull the previous or next set of items into view. The amount of items to scroll by is based on how many are fully visible in the scroll container. For example, if we see three visible items, clicking the &quot;next&quot; button will scroll the subsequent three items into view.</p> <h3 id="observing-inline-scroll-bounds">Observing inline scroll bounds</h3> <p>Let's review the demo's top component. As we begin to scroll to the right, the &quot;previous&quot; button appears. Scrolling to the component's end causes the &quot;next&quot; button to disappear. Similarly we can see the bottom component's buttons fade when their respective scroll bound is reached.</p> <p>Recall the sentinels discussed earlier in this post? With a little help from the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API">Intersection Observer API</a>, the component watches for either sentinel intersecting the visible scroll area, indicating that we've reached a boundary. When this happens, a <code>data-bound</code> attribute is toggled on the corresponding <code>button</code> element. This presents an opportunity to alter styles and provide additional visual feedback.</p> <pre class="language-scss"><code class="language-scss"><span class="token selector">.btn-scrolly-rail </span><span class="token punctuation">{</span> <span class="token comment">/** default styles */</span> <span class="token punctuation">}</span> <span class="token selector">.btn-scrolly-rail[data-bound] </span><span class="token punctuation">{</span> <span class="token comment">/* styles to apply to button at boundary */</span> <span class="token punctuation">}</span></code></pre> <h2 id="future-improvements">Future improvements</h2> <p>I'd love to hear from the community most specifically on improving the accessibility story here. Here are some general notes:</p> <ul> <li>I debated if button clicks should pass feedback to screen readers such as &quot;Scrolled next three items into view&quot; or &quot;Reached scroll boundary&quot; but felt unsure if that created unforeseen confusion.</li> <li>For items that contain interactive elements: If a new set of items scroll into view and a user tabs into the item list, should the initial focusable element start at the snap target? This could pair well with <a href="https://codepen.io/hexagoncircle/pen/LYawJVP">navigating the list using keyboard arrow keys</a>.</li> <li>Is it worth authoring intersecting sentinel &quot;enter/leave&quot; events that we can listen for? Something like: Scroll bound reached? Do a thing. Leaving scroll bound? Revert the thing we just did or do another thing. <em>Side note:</em> prevent these events from firing when the component script initializes.</li> <li>How might this code get refactored once <a href="https://developer.chrome.com/blog/scroll-snap-events">scroll snap events</a> are widely available? I imagine we could check for when the first or last element becomes the snap target to handle toggling <code>data-bound</code> attributes. Then we can remove Intersection Observer functionality.</li> </ul> <p>And if any folks have other scroll component solutions to share, please reach out or <a href="https://github.com/hexagoncircle/scrolly-rail/issues">open an issue</a> on the repo.</p> Some Things About Keyframes 2024-12-30T00:00:00Z https://ryanmulligan.dev/blog/some-things-about-keyframes/ <p>Whether you've barely scratched the surface of keyframe animations in CSS or fancy yourself a seasoned pro, I suggest reading <a href="https://www.joshwcomeau.com/animation/keyframe-animations/">An Interactive Guide to Keyframe Animations</a>. Josh (as always) does an impeccable deep dive that includes interactive demos for multi-step animations, loops, setting dynamic values, and more.</p> <p>This is a quick post pointing out some other minor particulars:</p> <ol> <li>Duplicate keyframe properties</li> <li>The order of keyframe rules</li> <li>Custom timing function (easing) values at specific keyframes</li> </ol> <h2 id="duplicate-keyframe-properties">Duplicate keyframe properties</h2> <p>Imagine an &quot;appearance&quot; animation where an element slides down, scales up, and changes color. The starting <code>0%</code> keyframe sets the element's y-axis offset position and scales down the size. When the animation is triggered, the element glides up from the offset to its central position for the full duration of the animation. About halfway through, the element's size begins scaling back up and the background color changes. At first, we might be tempted to duplicate the <code>background-color</code> and <code>scale</code> properties in both <code>0%</code> and <code>50%</code> keyframe blocks.</p> <pre class="language-css"><code class="language-css"><span class="token atrule"><span class="token rule">@keyframes</span> animate</span> <span class="token punctuation">{</span> <span class="token selector">0%</span> <span class="token punctuation">{</span> <span class="token property">background-color</span><span class="token punctuation">:</span> red<span class="token punctuation">;</span> <span class="token property">scale</span><span class="token punctuation">:</span> 0.5<span class="token punctuation">;</span> <span class="token property">translate</span><span class="token punctuation">:</span> 0 100%<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">50%</span> <span class="token punctuation">{</span> <span class="token property">background-color</span><span class="token punctuation">:</span> red<span class="token punctuation">;</span> <span class="token property">scale</span><span class="token punctuation">:</span> 0.5<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">100%</span> <span class="token punctuation">{</span> <span class="token property">background-color</span><span class="token punctuation">:</span> green<span class="token punctuation">;</span> <span class="token property">scale</span><span class="token punctuation">:</span> 1<span class="token punctuation">;</span> <span class="token property">translate</span><span class="token punctuation">:</span> 0 0<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre> <p>Although this functions correctly, it requires us to manage the same property declarations in two locations. Instead of repeating, we can share them in a comma-separated ruleset.</p> <pre class="language-css"><code class="language-css"><span class="token atrule"><span class="token rule">@keyframes</span> animate</span> <span class="token punctuation">{</span> <span class="token selector">0%</span> <span class="token punctuation">{</span> <span class="token property">translate</span><span class="token punctuation">:</span> 0 100%<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">0%, 50%</span> <span class="token punctuation">{</span> <span class="token property">background-color</span><span class="token punctuation">:</span> red<span class="token punctuation">;</span> <span class="token property">scale</span><span class="token punctuation">:</span> 0.5<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">100%</span> <span class="token punctuation">{</span> <span class="token property">background-color</span><span class="token punctuation">:</span> green<span class="token punctuation">;</span> <span class="token property">scale</span><span class="token punctuation">:</span> 1<span class="token punctuation">;</span> <span class="token property">translate</span><span class="token punctuation">:</span> 0 0<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre> <h2 id="keyframe-rules-order">Keyframe rules order</h2> <p>Another semi-interesting quirk is that we can rearrange the keyframe order.</p> <pre class="language-css"><code class="language-css"><span class="token atrule"><span class="token rule">@keyframes</span> animate</span> <span class="token punctuation">{</span> <span class="token selector">0%</span> <span class="token punctuation">{</span> <span class="token property">translate</span><span class="token punctuation">:</span> 0 100%<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">100%</span> <span class="token punctuation">{</span> <span class="token property">background-color</span><span class="token punctuation">:</span> green<span class="token punctuation">;</span> <span class="token property">scale</span><span class="token punctuation">:</span> 1<span class="token punctuation">;</span> <span class="token property">translate</span><span class="token punctuation">:</span> 0 0<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token comment">/* Set and hold values until halfway through animation */</span> <span class="token selector">0%, 50%</span> <span class="token punctuation">{</span> <span class="token property">background-color</span><span class="token punctuation">:</span> red<span class="token punctuation">;</span> <span class="token property">scale</span><span class="token punctuation">:</span> 0.5<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre> <p>Just for kicks, here is a version that swaps <code>0%</code> and <code>100%</code> for their corresponding <code>from</code> and <code>to</code> keyword values.</p> <pre class="language-css"><code class="language-css"><span class="token atrule"><span class="token rule">@keyframes</span> animate</span> <span class="token punctuation">{</span> <span class="token selector">from</span> <span class="token punctuation">{</span> <span class="token property">translate</span><span class="token punctuation">:</span> 0 100%<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">to</span> <span class="token punctuation">{</span> <span class="token property">background-color</span><span class="token punctuation">:</span> green<span class="token punctuation">;</span> <span class="token property">scale</span><span class="token punctuation">:</span> 1<span class="token punctuation">;</span> <span class="token property">translate</span><span class="token punctuation">:</span> 0 0<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token comment">/* Set and hold values until halfway through animation */</span> <span class="token selector">from, 50%</span> <span class="token punctuation">{</span> <span class="token property">background-color</span><span class="token punctuation">:</span> red<span class="token punctuation">;</span> <span class="token property">scale</span><span class="token punctuation">:</span> 0.5<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre> <p>The <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@keyframes#resolving_duplicates">&quot;Resolving Duplicates&quot;</a> section from the MDN docs mentions that <code>@keyframes</code> rules do not cascade, which explains why this order still returns the expected animation. Customizing the order could be useful for grouping property changes within a <code>@keyframes</code> block as an animation becomes more complex.</p> <p>That same section of the MDN docs also points out that cascading <em>does</em> occur when multiple keyframes define the same percentage values. So, in the following <code>@keyframes</code> block, the second <code>translate</code> declaration overrides the first.</p> <pre class="language-css"><code class="language-css"><span class="token atrule"><span class="token rule">@keyframes</span> animate</span> <span class="token punctuation">{</span> <span class="token selector">to</span> <span class="token punctuation">{</span> <span class="token property">translate</span><span class="token punctuation">:</span> 0 100%<span class="token punctuation">;</span> <span class="token property">rotate</span><span class="token punctuation">:</span> 1turn<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">to</span> <span class="token punctuation">{</span> <span class="token property">translate</span><span class="token punctuation">:</span> 0 -100%<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre> <h2 id="keyframe-specific-easing">Keyframe-specific easing</h2> <p>Under <a href="https://www.w3.org/TR/css-animations-1/#timing-functions">&quot;Timing functions for keyframes&quot;</a> from the CSS Animations Level 1 spec, we discover that easing can be adjusted within a keyframe ruleset.</p> <blockquote> <p>A keyframe style rule may also declare the timing function that is to be used as the animation moves to the next keyframe.</p> </blockquote> <p>Toggle open the CSS panel in the ensuing CodePen demo and look for the <code>@keyframes</code> block. Inside one of the percentages, a custom easing is applied using the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function/linear"><code>linear()</code> CSS function</a> to give each element some wobble as it lands.</p> <p class="codepen" data-height="500" data-preview="false" data-default-tab="result" data-slug-hash="yyBozpm" data-user="hexagoncircle"> <a href="https://codepen.io/hexagoncircle/pen/yyBozpm"> <svg class="icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512" fill="currentcolor"><path d="M502.285 159.704l-234-156c-7.987-4.915-16.511-4.96-24.571 0l-234 156C3.714 163.703 0 170.847 0 177.989v155.999c0 7.143 3.714 14.286 9.715 18.286l234 156.022c7.987 4.915 16.511 4.96 24.571 0l234-156.022c6-3.999 9.715-11.143 9.715-18.286V177.989c-.001-7.142-3.715-14.286-9.716-18.285zM278 63.131l172.286 114.858-76.857 51.429L278 165.703V63.131zm-44 0v102.572l-95.429 63.715-76.857-51.429L234 63.131zM44 219.132l55.143 36.857L44 292.846v-73.714zm190 229.715L61.714 333.989l76.857-51.429L234 346.275v102.572zm22-140.858l-77.715-52 77.715-52 77.715 52-77.715 52zm22 140.858V346.275l95.429-63.715 76.857 51.429L278 448.847zm190-156.001l-55.143-36.857L468 219.132v73.714z"></path></svg> <span>Open CodePen demo</span> </a> </p> <script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script> <p>I think that looks quite nice! Adding keyframe-specific easing brings an extra layer of polish and vitality to our animations. One minor snag, though: We can't set a CSS variable as an <code>animation-timing-function</code> value. This unfortunately means we're unable to access shared custom easing values, say from a library or design system.</p> <pre class="language-css"><code class="language-css"><span class="token selector">:root</span> <span class="token punctuation">{</span> <span class="token property">--easeOutCubic</span><span class="token punctuation">:</span> <span class="token function">cubic-bezier</span><span class="token punctuation">(</span>0.33<span class="token punctuation">,</span> 1<span class="token punctuation">,</span> 0.68<span class="token punctuation">,</span> 1<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token atrule"><span class="token rule">@keyframes</span></span> <span class="token punctuation">{</span> <span class="token selector">50%</span> <span class="token punctuation">{</span> <span class="token property">animation-timing-function</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--easeOutCubic<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre> <h2 id="helpful-resources">Helpful resources</h2> <ul> <li><a href="https://www.joshwcomeau.com/animation/keyframe-animations/">An Interactive Guide to Keyframe Animations</a></li> <li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@keyframes">@keyframes on MDN</a></li> <li><a href="https://easings.net/">Easing Functions Cheat Sheet</a></li> <li><a href="https://linear-easing-generator.netlify.app/">Linear easing generator</a></li> <li><a href="https://www.smashingmagazine.com/2023/09/path-css-easing-linear-function/">The Path To Awesome CSS Easing With The <code>linear()</code> Function</a></li> </ul> Blog Questions Challenge 2025-03-25T00:00:00Z https://ryanmulligan.dev/blog/blog-questions-challenge-2025/ <p>Hey there.</p> <p>It has been a minute since my last post. I was semi-recently <a href="https://www.zachleat.com/web/blogging/">tagged by Zach Leatherman</a> to (optionally) participate in this year's <em>Blog Questions Challenge</em>. I had planned on doing it then. But life really hit hard as we entered this year and it has not let up. Energy dedicated to my personal webspace has been non-existent. I am tired. Hopefully this post can help shake off some of the rust, bring me back to writing and sharing with you lovely folks.</p> <p>I won't be tagging anyone to do this challenge. However, if you're inspired to write your own after reading mine, I'd love for you to share it with me.</p> <h2 id="why-did-you-start-blogging-in-the-first-place">Why did you start blogging in the first place?</h2> <p>Blogging has always been a part of my web experience. Earliest I can remember is building my band a <a href="https://en.wikipedia.org/wiki/GeoCities">GeoCities</a> website back in high school. I'd share short passages about new song ideas, how last night's show went, stuff like that. I also briefly had a <a href="https://en.wikipedia.org/wiki/Xanga">Xanga blog</a> running. My memory is totally faded on what exactly I wrote in there—I'm not eager to dig up high school feelings either—but fairly certain all of those entries are just lost digital history. Having an &quot;online journal&quot; was such a fresh idea at the time. Sharing felt more natural and real before the social media platforms took over. [<em>blows raspberry</em>] I've completely dated myself and probably sound like <a href="https://knowyourmeme.com/memes/old-man-yells-at-cloud">&quot;old man yells at cloud&quot;</a> right now.</p> <p>Anyway, I pretty much stopped blogging for a while after high school. I turned my efforts back to pen on paper, keeping journals of lyrics, thoughts, and feelings mostly to myself. My dev-focused blogging that you may be familiar with really only spans the last decade, give or take a couple years.</p> <h2 id="what-platform-are-you-using-to-manage-your-blog-and-why">What platform are you using to manage your blog and why?</h2> <p>At the moment and the foreseeable future, I'm using <a href="https://www.11ty.dev/">11ty</a>. I published a short post about <a href="https://ryanmulligan.dev/blog/migrating-to-11ty/">migrating my site to 11ty</a> back in 2021. I still feel the same sentiments and still admire those same people. And many new community friends as well!</p> <h2 id="have-you-blogged-on-other-platforms-before">Have you blogged on other platforms before?</h2> <p>I've definitely used WordPress but I can't remember what the heck I was even blogging about during that time. Then I switched to just writing posts directly in HTML files and FTP'ing them up to some server somewhere. Pretty silly in retrospect, but boy did I feel alive.</p> <h2 id="how-do-you-write-your-posts">How do you write your posts?</h2> <p>Always via laptop, never on my phone. I manage posts in markdown files, push them up to a GitHub repo and let that automatically redeploy my site on Netlify. Editing content is done in VSCode. I've debated switching to some lightweight CMS, connecting to Notion or Obsidian, but why introduce any more complexity and mess with what works fine for me?</p> <h2 id="when-do-you-feel-most-inspired-to-write">When do you feel most inspired to write?</h2> <p>Typically I'll write up a post about something new I discovered while on my wild coding escapades, whether it's solving an issue at work or exploring new web features in my free time. If I have trouble finding solutions to my particular problem on the world wide net, I'm even more inclined to post about it. Most of my ideas are pursued on weekends, but I've had some early morning or late night weekday sessions.</p> <p>What I'm trying to say is that anytime is a good time for blogging. It's like pizza when it's on a bagel.</p> <h2 id="do-you-publish-immediately-after-writing-or-do-you-let-it-simmer-a-bit-as-a-draft">Do you publish immediately after writing, or do you let it simmer a bit as a draft?</h2> <p>It depends. If I had been writing for a long period of time, I find it best to take a breather before publishing. When I feel ready, I'll post and share with a small group for feedback, find grammatical errors. Then I eventually add it to whatever social channels feel right. Used to be Twitter, but straight up screw that garbage temple. I'll likely post on <a href="https://bsky.app/profile/ryanmulligan.dev">Bluesky</a>, toot on <a href="https://fosstodon.org/@hexagoncircle">Mastodon</a>. Other times I'll slap a new post on this site and not share it on any socials. Let the RSS feeds do their magic.</p> <h2 id="whats-your-favorite-post-on-your-blog">What's your favorite post on your blog?</h2> <p>I don't know if I have a favorite. Can I love them all equally? Well, besides that CSS Marquee one. Damn that blog post for becoming so popular.</p> <h2 id="any-future-plans-for-your-blog">Any future plans for your blog?</h2> <p>Once things settle down in life, I think I'll be ready for a redesign. I had a blast building the <a href="https://ryanmulligan.dev/blog/site-rebuild/">current version inspired by Super Mario Wonder</a>. Until then? More blogging. It won't be super soon, but I do have a few zesty article ideas percolating in this old, tired brain.</p> Transition to the Other Side with Container Query Units 2025-10-11T00:00:00Z https://ryanmulligan.dev/blog/transition-to-the-other-side/ <p>Managing the position of an element as it travels across the length of its parent container can be tricky. Assuming they both have dynamic, responsive dimensions, we might rely on JS to check the width and/or height of each element and do some calculations for a proper end result. The classic <a href="https://ryanmulligan.dev/blog/gsap-flip-cart/">FLIP technique</a> has proven to be a solid solution in the past. For a modern approach, the <a href="https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API">View Transition API</a> can also work well here.</p> <p>I now realize there's a much simpler approach using <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_queries#container_query_length_units">container query units</a> and a dash of CSS wizardry.</p> <h2 id="the-demo">The demo</h2> <p>Select the <code>container query units</code> option if it's not already, then click and hold the container to witness the magic. Try changing the element dimensions or resizing the parent container using its handle on the bottom right.</p> <p class="codepen" data-height="600" data-preview="false" data-default-tab="result" data-slug-hash="gbPxYaW" data-user="hexagoncircle"> <a href="https://codepen.io/hexagoncircle/pen/gbPxYaW"> <svg class="icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512" fill="currentcolor"><path d="M502.285 159.704l-234-156c-7.987-4.915-16.511-4.96-24.571 0l-234 156C3.714 163.703 0 170.847 0 177.989v155.999c0 7.143 3.714 14.286 9.715 18.286l234 156.022c7.987 4.915 16.511 4.96 24.571 0l234-156.022c6-3.999 9.715-11.143 9.715-18.286V177.989c-.001-7.142-3.715-14.286-9.716-18.285zM278 63.131l172.286 114.858-76.857 51.429L278 165.703V63.131zm-44 0v102.572l-95.429 63.715-76.857-51.429L234 63.131zM44 219.132l55.143 36.857L44 292.846v-73.714zm190 229.715L61.714 333.989l76.857-51.429L234 346.275v102.572zm22-140.858l-77.715-52 77.715-52 77.715 52-77.715 52zm22 140.858V346.275l95.429-63.715 76.857 51.429L278 448.847zm190-156.001l-55.143-36.857L468 219.132v73.714z"></path></svg> <span>Open CodePen demo</span> </a> </p> <script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script> <p><a href="https://ryanmulligan.dev/blog/transition-to-the-other-side/#the-solution">Jump down to the solution</a> if you'd like to get right into it. Otherwise, join me on a transformative journey to this final result.</p> <h2 id="transition-exploration">Transition exploration</h2> <p>The following CSS will transition an element smoothly to the right when its parent container is pressed:</p> <pre class="language-scss"><code class="language-scss"><span class="token selector">.element </span><span class="token punctuation">{</span> <span class="token property">transition</span><span class="token punctuation">:</span> transform 200ms ease-out<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">.parent:active .element </span><span class="token punctuation">{</span> <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">translateX</span><span class="token punctuation">(</span>100%<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>Individual transform properties are also available and well supported in modern browsers. I tend to use them more frequently when writing simple transforms like this. In the demo, we'll find the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/translate"><code>translate</code> property</a> is being transitioned. Let's update the above code example to something similar:</p> <pre class="language-scss"><code class="language-scss"><span class="token selector">.element </span><span class="token punctuation">{</span> <span class="token property">transition</span><span class="token punctuation">:</span> translate 200ms ease-out<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">.parent:active .element </span><span class="token punctuation">{</span> <span class="token property">translate</span><span class="token punctuation">:</span> 100%<span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p class="callout">Keep in mind that there's a pre-defined order for independent transform properties. Stefan's article explains <a href="https://www.stefanjudis.com/blog/order-in-css-transformation-transform-functions-vs-individual-transforms/">the fundamental differences between transform functions and individual transforms</a>. Not an issue with our current examples, but something to remember when multiple individual transforms are being applied.</p> <p>Check out <a href="https://ryanmulligan.dev/blog/transition-to-the-other-side/#the-demo">the demo</a> with <code>x</code> checked and the <code>percentage</code> option selected. When we click and hold the container, the element transitions the width of itself to the right. We can see that this percentage is based on the element's dimensions. While handy, it doesn't achieve our goal of moving the element all the way to the opposite side.</p> <h3 id="explicit-dimensions">Explicit dimensions</h3> <p>If we knew the exact dimensions of the parent container, we could declare a <code>calc()</code> function where the element's full percentage is subtracted from the explicit parent size.</p> <pre class="language-scss"><code class="language-scss"><span class="token selector">.parent </span><span class="token punctuation">{</span> <span class="token property">width</span><span class="token punctuation">:</span> 300px<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">.element </span><span class="token punctuation">{</span> <span class="token property">transition</span><span class="token punctuation">:</span> translate 200ms ease-out<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">.parent:active .element </span><span class="token punctuation">{</span> <span class="token property">translate</span><span class="token punctuation">:</span> <span class="token function">calc</span><span class="token punctuation">(</span>300px <span class="token operator">-</span> 100%<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>It works, but it's uncommon to have explicit dimensions declared like that. Our elements need to be flexible and responsive in any context. What can we do instead?</p> <h3 id="position-properties">Position properties</h3> <p>Properties like <code>top</code> and <code>left</code> are available to us. Could we transition the element by doing something like this?</p> <pre class="language-scss"><code class="language-scss"><span class="token selector">.element </span><span class="token punctuation">{</span> <span class="token property">position</span><span class="token punctuation">:</span> relative<span class="token punctuation">;</span> <span class="token property">left</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span> <span class="token property">transition</span><span class="token punctuation">:</span> 200ms ease-out<span class="token punctuation">;</span> <span class="token property">transition-property</span><span class="token punctuation">:</span> translate<span class="token punctuation">,</span> left<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">.parent:active .element </span><span class="token punctuation">{</span> <span class="token property">left</span><span class="token punctuation">:</span> 100%<span class="token punctuation">;</span> <span class="token property">translate</span><span class="token punctuation">:</span> -100%<span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>Seems that we can, at least in the context of the demo. However, animating position properties has a negative impact on layout and creates performance issues. The browser works harder to recalculate element positions, repaint pixels, and then composite the result. This inevitably leads to janky or sluggish animations. GPU-accelerated properties such as <code>transform</code> and <code>translate</code> avoid triggering repaints so animations run buttery-smooth and fluid.</p> <p>Fair enough. We'll focus on moving the element using transform properties. It's time to reveal the strongest solution.</p> <h2 id="the-solution">The solution</h2> <p>In the demo's controls, check that <code>container query units</code> is selected. Click and hold the container. Watch as the element smoothly transitions to the opposite side of the parent container. Try changing the element dimensions using the sliders, or resize the parent with the resize handle on its bottom right. It <em>still</em> works!</p> <p>Here's the gist when we only need to transition to the opposite side of the parent horizontally.</p> <pre class="language-scss"><code class="language-scss"><span class="token selector">.parent </span><span class="token punctuation">{</span> <span class="token property">container-type</span><span class="token punctuation">:</span> inline-size<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">.element </span><span class="token punctuation">{</span> <span class="token property">transition</span><span class="token punctuation">:</span> translate 200ms ease-out<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">.parent:active .element </span><span class="token punctuation">{</span> <span class="token property">translate</span><span class="token punctuation">:</span> <span class="token function">calc</span><span class="token punctuation">(</span>100cqi <span class="token operator">-</span> 100%<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>If we want to transition vertically or in both directions, we'll need the <code>size</code> value for our <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/container-type"><code>container-type</code></a> so that containment is applied to the block and inline directions. The example below translates the element along the y-axis.</p> <pre class="language-scss"><code class="language-scss"><span class="token selector">.parent </span><span class="token punctuation">{</span> <span class="token property">container-type</span><span class="token punctuation">:</span> size<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">.element </span><span class="token punctuation">{</span> <span class="token property">transition</span><span class="token punctuation">:</span> translate 200ms ease-out<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">.parent:active .element </span><span class="token punctuation">{</span> <span class="token property">translate</span><span class="token punctuation">:</span> 0 <span class="token function">calc</span><span class="token punctuation">(</span>100cqb <span class="token operator">-</span> 100%<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>By setting a <code>container-type</code> on the parent, the element is able to access the parent size via <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_queries#container_query_length_units">container query length units</a>:</p> <ul> <li><code>1cqi</code> is 1% of the inline size.</li> <li><code>1cqb</code> is 1% of the block size.</li> </ul> <p class="callout">Notice that we're using logical properties instead of physical ones. If this isn't familiar territory, I recommend Ahmad's excellent <a href="https://ishadeed.com/article/css-logical-properties/">Digging Into CSS Logical Properties</a> article to learn more.</p> <p><code>100cqi</code> is the full inline size of the parent container. We can recall from earlier that a transform percentage reflects the element's dimensions. Once we subtract <code>100%</code> from that container query unit, the element can gracefully transition to its proper position on the opposite side of the container.</p> <p>Take a moment to enjoy the wonder and magic that is modern CSS.</p> <h2 id="helpful-resources">Helpful resources</h2> <ul> <li><a href="https://frontendmasters.com/blog/container-queries-and-units/">Container Queries and Units</a></li> <li><a href="https://web.dev/articles/animations-guide">How to create high-performance CSS animations</a></li> <li><a href="https://www.stefanjudis.com/blog/order-in-css-transformation-transform-functions-vs-individual-transforms/">Order in CSS transformations – transform functions vs individual transforms</a></li> <li><a href="https://ishadeed.com/article/css-logical-properties/">Digging Into CSS Logical Properties</a></li> </ul>