Full Width [alt+shift+f] Shortcuts [alt+shift+k]
Sign Up [alt+shift+s] Log In [alt+shift+l]
16
Hey there. It has been a minute since my last post. I was semi-recently tagged by Zach Leatherman to (optionally) participate in this year's Blog Questions Challenge. I had planned on doing it then. But life really hit hard as we entered this year and it has not let up. Energy dedicated to my personal webspace has been non-existent. I am tired. Hopefully this post can help shake off some of the rust, bring me back to writing and sharing with you lovely folks. I won't be tagging anyone to do this challenge. However, if you're inspired to write your own after reading mine, I'd love for you to share it with me. Why did you start blogging in the first place? Blogging has always been a part of my web experience. Earliest I can remember is building my band a GeoCities website back in high school. I'd share short passages about new song ideas, how last night's show went, stuff like that. I also briefly had a Xanga blog running. My memory is totally faded on what exactly I wrote in there—I'm...
3 weeks ago

Improve your reading experience

Logged in users get linked directly to articles resulting in a better reading experience. Please login for free, it takes less than 1 minute.

More from Ryan Mulligan

Some Things About Keyframes

Whether you've barely scratched the surface of keyframe animations in CSS or fancy yourself as a seasoned pro, I suggest reading An Interactive Guide to Keyframe Animations. Josh (as always) does an impeccable deep dive that includes interactive demos for multi-step animations, loops, setting dynamic values, and more. This is a quick post pointing out some other minor particulars: Duplicate keyframe properties The order of keyframe rules Custom timing function (easing) values at specific keyframes Duplicate keyframe properties Imagine an "appearance" animation where an element slides down, scales up, and changes color. The starting 0% keyframe sets the element's y-axis position and scales down the size. The element glides down to its initial position for the full duration of the animation. About halfway through, the element's size is scaled back up and the background color changes. At first, we might be tempted to duplicate the background-color and scale properties in both 0% and 50% keyframe blocks. @keyframes animate { 0% { background-color: red; scale: 0.5; translate: 0 100%; } 50% { background-color: red; scale: 0.5; } 100% { background-color: green; scale: 1; translate: 0 0; } } Although this functions correctly, it requires us to manage the same property declarations in two locations. Instead of repeating, we can share them in a comma-separated ruleset. @keyframes animate { 0% { translate: 0 100%; } 0%, 50% { background-color: red; scale: 0.5; } 100% { background-color: green; scale: 1; translate: 0 0; } } Keyframe rules order Another semi-interesting qwirk is that we can rearrange the keyframe order. @keyframes animate { 0% { translate: 0 100%; } 100% { background-color: green; scale: 1; translate: 0 0; } /* Set and hold values until halfway through animation */ 0%, 50% { background-color: red; scale: 0.5; } } "Resolving Duplicates" from the MDN docs mentions that @keyframes rules don't cascade, which explains why this order still returns the expected animation. Customizing the order could be useful for grouping property changes within a @keyframes block as an animation becomes more complex. That same section of the MDN docs also points out that cascading does occur when multiple keyframes define the same percentage values. So, in the following @keyframes block, the second translate declaration overrides the first. @keyframes animate { to { translate: 0 100%; rotate: 1turn; } to { translate: 0 -100%; } } Keyframe-specific easing Under "Timing functions for keyframes" from the CSS Animations Level 1 spec, we discover that easing can be adjusted within a keyframe ruleset. A keyframe style rule may also declare the timing function that is to be used as the animation moves to the next keyframe. Toggle open the CSS panel in the ensuing CodePen demo and look for the @keyframes block. Inside one of the percentages, a custom easing is applied using the linear() CSS function to give each element some wobble as it lands. Open CodePen demo I think that looks quite nice! Adding keyframe-specific easing brings an extra layer of polish and vitality to our animations. One minor snag, though: We can't set a CSS variable as an animation-timing-function value. This unfortunately means we're unable to access shared custom easing values, say from a library or design system. :root { --easeOutCubic: cubic-bezier(0.33, 1, 0.68, 1); } @keyframes { 50% { animation-timing-function: var(--easeOutCubic); } } Helpful resources An Interactive Guide to Keyframe Animations @keyframes on MDN Easing Functions Cheat Sheet Linear easing generator The Path To Awesome CSS Easing With The linear() Function

3 months ago 77 votes
Scrolling Rails and Button Controls

Once again, here I am, hackin' away on horizontal scroll ideas. This iteration starts with a custom HTML tag. All the necessities for scroll overflow, scroll snapping, and row layout are handled with CSS. Then, as a little progressive enhancement treat, button elements are connected that scroll the previous or next set of items into view when clicked. Behold! The holy grail of scrolling rails... the scrolly-rail! CodePen demo GitHub repo Open CodePen demo I'm being quite facetious about the "holy grail" part, if that's not clear. 😅 This is an initial try on an idea I'll likely experiment more with. I've shared some thoughts on potential future improvements at the end of the post. With that out of the way, let's explore! The HTML Wrap any collection of items with the custom tag: <scrolly-rail> <ul> <li>1</li> <li>2</li> <li>3</li> <!-- and so on--> </ul> </scrolly-rail> The custom element script checks if the direct child within scrolly-rail is a wrapper element, which is true for the above HTML. While it is possible to have items without a wrapper element, if the custom element script runs and button controls are connected, sentinel elements are inserted at the start and end bounds of the scroll container. Wrapping the items makes controlling spacing between them much easier, avoiding any undesired gaps appearing due to these sentinels. We'll discover what the sentinels are for later in the post. The CSS Here are the main styles for the component: scrolly-rail { display: flex; overflow-x: auto; overscroll-behavior-x: contain; scroll-snap-type: x mandatory; @media (prefers-reduced-motion: no-preference) { scroll-behavior: smooth; } } When JavaScript is enabled, sentinel elements are inserted before and after the unordered list (<ul>) element in the HTML example above. Flexbox ensures that the sentinels are positioned on either side of the element. We'll find out why later in this post. Containing the overscroll behavior will prevent us accidentally triggering browser navigation when scrolling beyond either edge of the scrolly-rail container. scroll-snap-type enforces mandatory scroll snapping. Smooth scrolling behavior applies when items scroll into view on button click, or if interactive elements (links, buttons, etc.) inside items overflowing the visible scroll area are focused. Finally, scroll-snap-align: start should be set on the elements that will snap into place. This snap position aligns an item to the beginning of the scroll snap container. In the above HTML, this would apply to the <li> elements. scrolly-rail li { scroll-snap-align: start; } As mentioned earlier, this is everything our component needs for layout, inline scrolling, and scroll snapping. Note that the CodePen demo takes it a step further with some additional padding and margin styles (check out the demo CSS panel). However, if we'd like to wire up controls, we'll need to include the custom element script in our HTML. The custom element script Include the script file on the page. <script type="module" src="scrolly-rail.js"></script> To connect the previous/next button elements, give each an id value and add these values to the data-control-* attributes on the custom tag. <scrolly-rail data-control-previous="btn-previous" data-control-next="btn-next" > <!-- ... --> </scrolly-rail> <button id="btn-previous" class="btn-scrolly-rail">Previous</button> <button id="btn-next" class="btn-scrolly-rail">Next</button> Now clicking these buttons will pull the previous or next set of items into view. The amount of items to scroll by is based on how many are fully visible in the scroll container. For example, if we see three visible items, clicking the "next" button will scroll the subsequent three items into view. Observing inline scroll bounds Notice that the "previous" button element in the demo's top component. As we begin to scroll to the right, the button appears. Scrolling to the end causes the "next" button to disappear. Similarly, for the bottom component we can see either button fade when their respective scroll bound is reached. Recall the sentinels discussed earlier in this post? With a little help from the Intersection Observer API, the component watches for either sentinel intersecting the visible scroll area, indicating that we've reached a boundary. When this happens, a data-bound attribute is toggled on the respective button. This presents the opportunity to alter styles and provide additional visual feedback. .btn-scrolly-rail { /** default styles */ } .btn-scrolly-rail[data-bound] { /* styles to apply to button at boundary */ } Future improvements I'd love to hear from the community most specifically on improving the accessibility story here. Here are some general notes: I debated if button clicks should pass feedback to screen readers such as "Scrolled next three items into view" or "Reached scroll boundary" but felt unsure if that created unforeseen confusion. For items that contain interactive elements: If a new set of items scroll into view and a user tabs into the item list, should the initial focusable element start at the snap target? This could pair well with navigating the list using keyboard arrow keys. Is it worth authoring intersecting sentinel "enter/leave" events that we can listen for? Something like: Scroll bound reached? Do a thing. Leaving scroll bound? Revert the thing we just did or do another thing. Side note: prevent these events from firing when the component script initializes. How might this code get refactored once scroll snap events are widely available? I imagine we could check for when the first or last element becomes the snap target to handle toggling data-bound attributes. Then we can remove Intersection Observer functionality. And if any folks have other scroll component solutions to share, please reach out or open an issue on the repo.

3 months ago 81 votes
The Shape of Runs to Come

Over the last few months or so, I have been fairly consistent with getting outside for Sunday morning runs. A series of lower body issues had prevented me from doing so for many years, but it was an exercise I had enjoyed back then. It took time to rebuild that habit and muscle but I finally bested the behavior of doing so begrudgingly. Back in the day (what a weird phrase to say, how old am I?) I would purchase digital copies of full albums. I'd use my run time to digest the songs in the order the artist intended. Admittedly, I've become a lazy listener now, relying on streaming services to surface playlists that I mindlessly select to get going. I want to be better than that, but that's a story for another time. These days, my mood for music on runs can vary: Some sessions I'll pop in headphones and throw on some tunes, other times I head out free of devices (besides a watch to track all those sweet, sweaty workout stats) and simply take in the city noise. Before I headed out for my journey this morning, a friend shared a track from an album of song covers in tribute to The Refused's The Shape Of Punk To Come. The original is a treasured classic, a staple LP from my younger years, and I can still remember the feeling of the first time it struck my ears. Its magic is reconjured every time I hear it. When that reverb-soaked feedback starts on Worms of the Senses / Faculties of the Skull, my heart rate begins to ascend. The anticipation builds, my entire body well aware of the explosion of sound imminent. As my run began, I wasn't sure if I had goosebumps from the morning chill or the wall of noise about to ensue. My legs were already pumping. I was fully present, listening intently, ready for the blast. The sound abruptly detonated sending me rocketing down the street towards the rising sun. My current running goal is 4-in-40, traversing four miles under forty minutes. I'm certainly no Prefontaine, but it's a fair enough objective for my age and ability. I'll typically finish my journey in that duration or slightly spill over the forty-minute mark. Today was different. Listening to The Shape Of Punk To Come sent me cruising an extra quarter mile beyond the four before my workout ended. The unstoppable energy from that album is truly pure runner's fuel. There's certainly some layer of nostalgia, my younger spirit awakened and reignited by thrashing guitars and frantic rhythms, but many elements and themes on this record were so innovative at the time it was released. New Noise is a prime example that executes the following feeling flawlessly: Build anticipation, increase the energy level, and then right as the song seems prepped to blast off, switch to something unexpected. In this case, the guitars drop out to make way for some syncopated celestial synths layered over a soft drum rhythm. The energy sits in a holding pattern, unsure whether it should burst or cool down, when suddenly— Can I scream?! Oh my goodness, yes. Yes you can. I quickly morphed into a runner decades younger. I had erupted, my entire being barreling full speed ahead. The midpoint of this track pulls out the same sequence of build up, drop off, and teasing just long enough before unleashing another loud burst of noise, driving to its explosive outro. As the song wraps up, "The New Beat!" is howled repeatedly to a cheering crowd that, I would imagine, had not been standing still. I definitely needed a long stretch after this run.

4 months ago 49 votes
The Pixel Canvas Shimmer Effect

I recently stumbled on a super cool, well-executed hover effect from the clerk.com website where a bloom of tiny pixels light up, their glow staggering from the center to the edges of its container. With some available free time over this Thanksgiving break, I hacked together my own version of a pixel canvas background shimmer. It quickly evolved into a pixel-canvas Web Component that can be enjoyed in the demo below. The component script and demo code have also been pushed up to a GitHub repo. Open CodePen demo Usage Include the component script and then insert a pixel-canvas custom element inside the container it should fill. <script type="module" src="pixel-canvas.js"></script> <div class="container"> <pixel-canvas></pixel-canvas> <!-- other elements --> </div> The pixel-canvas stretches to the edges of the parent container. When the parent is hovered, glimmering pixel fun ensues. Options The custom element has a few optional attributes available to customize the effect. Check out the CodePen demo's html panel to see how each variation is made. data-colors takes a comma separated list of color values. data-gap sets the amount of space between each pixel. data-speed controls the general duration of the shimmer. This value is slightly randomized on each pixel that, in my opinion, adds a little more character. data-no-focus is a boolean attribute that tells the Web Component to not run its animation whenever sibling elements are focused. The animation runs on sibling focus by default. There's likely more testing and tweaking necessary before I'd consider using this anywhere, but my goal was to run with this inspiration simply for the joy of coding. What a mesmerizing concept. I tip my hat to the creative engineers over at Clerk.

4 months ago 56 votes

More in design

Morokanella Fermented in Concrete Egg by Marios Karystios

Makarounas Vineyards Unveils Exclusive Release of Morokanella Fermented in a Concrete Egg. Makarounas Winery presents its latest limited release—an exceptional...

2 days ago 2 votes
Be Mindful of What You Make Easy

Carson Gross has a post about vendoring which brought back memories of how I used to build websites in ye olden days, back in the dark times before npm. “Vendoring” is where you copy dependency source files directly into your project (usually in a folder called /vendor) and then link to them — all of this being a manual process. For example: Find jquery.js or reset.css somewhere on the web (usually from the project’s respective website, in my case I always pulled jQuery from the big download button on jQuery.com and my CSS reset from Eric Meyer’s website). Copy that file into /vendor, e.g. /vendor/jquery@1.2.3.js Pull it in where you need it, e.g. <script src="/vendor/jquery@1.2.3.js"> And don’t get me started on copying your transitive dependencies (the dependencies of your dependencies). That gets complicated when you’re vendoring by hand! Now-a-days package managers and bundlers automate all of this away: npm i what you want, import x from 'pkg', and you’re on your way! It’s so easy (easy to get all that complexity). But, as the HTMX article points out, a strength can also be a weakness. It’s not all net gain (emphasis mine): Because dealing with large numbers of dependencies is difficult, vendoring encourages a culture of independence. You get more of what you make easy, and if you make dependencies easy, you get more of them. I like that — you get more of what you make easy. Therefore: be mindful of what you make easy! As Carson points out, dependency management tools foster a culture of dependence — imagine that! I know I keep lamenting Deno’s move away from HTTP imports by default, but I think this puts a finger on why I’m sad: it perpetuates the status quo, whereas a stance on aligning imports with how the browser works would not perpetuate this dependence on dependency resolution tooling. There’s no package manager or dependency resolution algorithm for the browser. I was thinking about all of this the other day when I then came across this thread of thoughts from Dave Rupert on Mastodon. Dave says: I prefer to use and make simpler, less complex solutions that intentionally do less. But everyone just wants the thing they have now but faster and crammed with more features (which are juxtaposed) He continues with this lesson from his startup Luro: One of my biggest takeaways from Luro is that it’s hard-to-impossible to sell a process change. People will bolt stuff onto their existing workflow (ecosystem) all day long, but it takes a religious conversion to change process. Which really helped me put words to my feelings regarding HTTP imports in Deno: i'm less sad about the technical nature of the feature, and more about what it represented as a potential “religious revival” in the area of dependency management in JS. package.json & dep management has become such an ecosystem unto itself that it seems only a Great Reawakening™️ will change it. I don’t have a punchy point to end this article. It’s just me working through my feelings. Email · Mastodon · Bluesky

3 days ago 5 votes
Developing Digital Disgust

Our world treats information like it’s always good. More data, more content, more inputs — we want it all without thinking twice. To say that the last twenty-five years of culture have centered around info-maximalism wouldn’t be an exaggeration. I hope we’re coming to the end of that phase. More than ever before, it feels like we have to — that we just can’t go on like this. But the solution cannot come from within; it won’t be a better tool or even better information to get us out of this mess. It will be us, feeling and acting differently. Think about this comparison: Information is to wisdom what pornography is to real intimacy. I’m not here to moralize, so I compare to pornography with all the necessary trepidation. Without judgement, it’s my observation that pornography depicts physical connection while creating emotional distance. I think information is like that. There’s a difference between information and wisdom that hinges on volume. More information promises to show us more of reality, but too much of it can easily hide the truth. Information can be pornography — a simulation that, when consumed without limits, can weaken our ability to experience the real thing. When we feel overwhelmed by information — anxious and unable to process what we’ve already taken in — we’re realizing that “more” doesn’t help us find truth. But because we have also established information as a fundamental good in our society, failure to keep up with it, make sense of it, and even profit from it feels like a personal moral failure. There is only one way out of that. We don’t need another filter. We need a different emotional response to information. We should not only question why our accepted spectrum of emotional response to information — in the general sense — is mostly limited to the space between curiosity and desire, but actively develop a capacity for disgust when it becomes too much. And it has become too much. Some people may say that we just need better information skills and tools, not less information. But this misses how fundamentally our minds need space and time to turn information into understanding. When every moment is filled with new inputs, we can’t fully absorb, process, and reflect upon what we’ve consumed. Reflection, not consumptions, creates wisdom. Reflection requires quiet, isolation, and inactivity. Some people say that while technology has expanded over the last twenty-five years, culture hasn’t. If they needed a good defense for that idea, well, I think this is it: A world without idleness is a truly world without creativity. I’m using loaded moral language here for a purpose — to illustrate an imbalance in our information-saturated culture. Idleness is a pejorative these days, though it needn’t be. We don’t refer to compulsive information consumption as gluttony, though we should. And if attention is our most precious resource — as an information-driven economy would imply — why isn’t its commercial exploitation condemned as avarice? As I ask these questions I’m really looking for where individuals like you and me have leverage. If our attention is our currency, then leverage will come with the capacity to not pay it. To not look, to not listen, to not react, to not share. And as has always been true of us human beings, actions are feelings echoed outside the body. We must learn not just to withhold our attention but to feel disgust at ceaseless claims to it.

3 days ago 6 votes
Root Labs by BRIGADE

Challenge Develop strong brand foundations for an international supplement company with a proven product to help them take the US...

4 days ago 5 votes
Some Love For Interoperable Apps

I like to try different apps. What makes trying different apps incredible is a layer of interoperability — standardized protocols, data formats, etc. When I can bring my data from one app to another, that’s cool. Cool apps are interoperable. They work with my data, rather than own it. For example, the other day I was itching to try a new RSS reader. I’ve used Reeder (Classic) for ages. But every once in a while I like to try something different. This is super easy because lots of clients support syncing to Feedbin. It’s worth pointing out: Feedbin has their own app. But they don’t force you to use it. You’re free to use any RSS client you want that supports their service. So all I have to do is download a new RSS client, login to Feedbin, and boom! An experience of my data in a totally different app from a totally different developer. That’s amazing! And you know how long it took? Seconds. No data export. No account migration. Doing stuff with my blog is similar. If I want to try a different authoring experience, all my posts are just plain-text markdown files on disk. Any app that can operate on plain-text files is a potential new app to try. No shade on them, but this why I personally don’t use apps like Bear. Don’t get me wrong, I love so much about Bear. But it wants to keep your data in its own own proprietary, note-keeping safe. You can’t just open your notes in Bear in another app. Importing is required. But there’s a big difference between apps that import (i.e. copy) your existing data and ones that interoperably work with it. Email can also be this way. I use Gmail, which supports IMAP, so I can open my mail in lots of different clients — and believe me, I've tried a lot of email clients over the years. Sparrow Mailbox Spark Outlook Gmail (desktop web, mobile app) Apple Mail Airmail This is why I don’t use un-standardized email features because I know I can’t take them with me. It’s also why I haven’t tried email providers like HEY! Because they don't support open protocols so I can’t swap clients when I want. My email is a dataset, and I want to be able to access it with any existing or future client. I don't want to be stuck with the same application for interfacing with my data forever (and have it tied to a company). I love this way of digital life, where you can easily explore different experiences of your data. I wish it was relevant to other areas of my digital life. I wish I could: Download a different app to view/experience my photos Download a different app to view/experience my music Download a different app to view/read my digital books In a world like this, applications would compete on an experience of my data, rather than on owning it. The world’s a big place. The entire world doesn’t need one singular photo experience to Rule Them All. Let’s have experiences that are as unique and varied as us. Email · Mastodon · Bluesky

5 days ago 8 votes