Ryan MulliganBlogging general thoughts and rambles, code snippets, and front-end
web dev discoveries2024-03-13T00:00:00Zhttps://ryanmulligan.devRyan Mulliganhey@ryanmulligan.devStyle Review2021-11-05T00:00:00Zhttps://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" /><source type="image/jpeg" 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" /><img alt="Stock photo of a fancy chocolate cake" src="https://ryanmulligan.dev/images/Tkx7mmWXl7-100.jpeg" width="800" height="588" /></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 Eleventy2021-11-08T00:00:00Zhttps://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="what-s-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 Navigation2021-11-15T00:00:00Zhttps://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/mailto:h%65y@%72%79%61%6E%6D%75%6Clig%61n.dev?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 "skip to next section" 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"><</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"><!-- section content --></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>section</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</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"></</span>a</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</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"><!-- 40+ links --></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>ul</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</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"></</span>a</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</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"><!-- section content --></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</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="0"</code> is applied to the project list. This adds it as a focusable element in the document's source order.</li>
<li><code>tabindex="-1"</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" /><source type="image/jpeg" 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" /><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" /></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="what-s-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/mailto:h%65y@%72%79%61%6E%6D%75%6Clig%61n.dev?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 GSAP2022-01-07T00:00:00Zhttps://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 "Usage" 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 "Usage" 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 Schemes2022-02-01T00:00:00Zhttps://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><html></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="5"</code> and then get updated to match <code>data-theme="1"</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&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="300" data-default-tab="result" 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>
&<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">&</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="-1"</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="status"</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&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 Container2022-03-11T00:00:00Zhttps://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&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 "full-bleed" 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 "full-bleed" 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 "Show line names" 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" /><source type="image/jpeg" 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" /><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" /></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&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 Breakpoints2022-07-05T00:00:00Zhttps://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 "desktop" menu (imagine a horizontal list of navigation items) that converts into its "mobile" 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 < 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 Marquee2022-08-06T00:00:00Zhttps://ryanmulligan.dev/blog/css-marquee/<h2 id="the-deprecated-tag">The deprecated tag</h2>
<p>The HTML <code><marquee></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><marquee></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><marquee></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"><</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"><</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"><</span>li</span><span class="token punctuation">></span></span>Item 1<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span><span class="token punctuation">></span></span>Item 2<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span><span class="token punctuation">></span></span>Item 3<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>ul</span><span class="token punctuation">></span></span>
<span class="token comment"><!-- Mirrors the content above --></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</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"><</span>li</span><span class="token punctuation">></span></span>Item 1<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span><span class="token punctuation">></span></span>Item 2<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span><span class="token punctuation">></span></span>Item 3<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>ul</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span></code></pre>
<aside class="callout"><p>Be sure to set <code>aria-hidden="true"</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><marquee></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><marquee></code> with TailwindCSS</a></li>
</ul>
Layout Breakouts with CSS Grid2022-10-07T00:00:00Zhttps://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 "show grid lines" 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 > *</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 "Losing Floats" 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 Time2023-01-20T00:00:00Zhttps://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 Elements2023-02-14T00:00:00Zhttps://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 "How grid-auto works" 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 "Hide elements" 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 Scroll2023-04-02T00:00:00Zhttps://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 "intercept"—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"><</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"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</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"></</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="result" data-preview="400" 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 Viewports2023-05-20T00:00:00Zhttps://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"><</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"><</span>table</span><span class="token punctuation">></span></span>
<span class="token comment"><!-- ... --></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>table</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</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 Values2023-07-14T00:00:00Zhttps://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"><!-- inline style --></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</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"><</span>li</span><span class="token punctuation">></span></span>Item 1<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span><span class="token punctuation">></span></span>Item 2<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span><span class="token punctuation">></span></span>Item 3<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>ul</span><span class="token punctuation">></span></span>
<span class="token comment"><!-- custom ruleset --></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</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"></</span>style</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</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"><</span>li</span><span class="token punctuation">></span></span>Item 1<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span><span class="token punctuation">></span></span>Item 2<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span><span class="token punctuation">></span></span>Item 3<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</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"><</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"><</span>li</span><span class="token punctuation">></span></span>Item 1<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span><span class="token punctuation">></span></span>Item 2<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span><span class="token punctuation">></span></span>Item 3<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</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 "stacks" 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"><</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"><</span>li</span><span class="token punctuation">></span></span>Item 1<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span><span class="token punctuation">></span></span>Item 2<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span><span class="token punctuation">></span></span>
Item 3
<span class="token comment"><!-- This <ul> will also have a 2rem gap --></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</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"><</span>li</span><span class="token punctuation">></span></span>Item 1<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span><span class="token punctuation">></span></span>Item 2<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span><span class="token punctuation">></span></span>Item 3<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>ul</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</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 "reset" 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"><</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"><</span>li</span><span class="token punctuation">></span></span>Item 1<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span><span class="token punctuation">></span></span>Item 2<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span><span class="token punctuation">></span></span>
Item 3
<span class="token comment"><!-- Revert the value on this element --></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</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"><</span>li</span><span class="token punctuation">></span></span>Item 1<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span><span class="token punctuation">></span></span>Item 2<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span><span class="token punctuation">></span></span>Item 3<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>ul</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</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 CSS2023-08-21T00:00:00Zhttps://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 "develop" 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 "developing" 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"><</span>figure</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</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"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>figcaption</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>figcaption</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</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><axis></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><view-timeline-inset></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 "developing" 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><length-percentage></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 Component2023-10-07T00:00:00Zhttps://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 "scrollspy" 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 "active" 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="what-s-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:00Zhttps://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><scroll-pen></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><theme-machine></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>..."Toot at me" may not be the best way to word that.</p>
We can :has it all2023-12-19T00:00:00Zhttps://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"><</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"><!-- card contents --></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>article</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</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"><!-- card contents --></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>article</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</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"><!-- card contents --></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</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="filter"]</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 Component2023-12-22T00:00:00Zhttps://ryanmulligan.dev/blog/target-toggler/<p>There are very rare occasions that I want <code><details></code> element disclosure widget-style funtionality but would like to have the <code><summary></code> element detached or live outside of it's related <code><details></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><button></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"><</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"></</span>script</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</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"><</span>button</span><span class="token punctuation">></span></span>Show more info<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>button</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>target-toggler</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>section</span><span class="token punctuation">></span></span>A special announcement<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>section</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</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"><!-- some additional information --></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</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"><</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"><</span>button</span><span class="token punctuation">></span></span>Show more info<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>button</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</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 Spark2024-01-02T00:00:00Zhttps://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><click-spark></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"><</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"></</span>click-spark</span><span class="token punctuation">></span></span></code></pre>
<p><strong>Updated on January 5, 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"><</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"></</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 Options2024-01-18T00:00:00Zhttps://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 "today I learned", 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 "issue chooser" 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" /><source type="image/jpeg" 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" /><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" /></picture></p>
<p>The <code>blank_issues_enabled: false</code> line in the config code hides the "open a blank issue" 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 Queries2024-01-27T00:00:00Zhttps://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, "toggling" 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 Overflow2024-02-19T00:00:00Zhttps://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 "header" and "footer" 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"><</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"><</span>section</span><span class="token punctuation">></span></span>
<span class="token comment"><!-- this content controls overall height --></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>section</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</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"><!-- overflowing section content --></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>section</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</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 "magic number" 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 Great2024-03-13T00:00:00Zhttps://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, "It's bedtime, you two, come along!" 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>