More from Ryan Mulligan
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
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.
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.
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.
More in design
This brand creation for Southwestern Distillery is born from the legend of two nineteenth century Cornish brothers who spent all...
There’s a psychological burden of digital life even heavier than distraction. When the iPhone was first introduced in 2007, the notion of an “everything device” was universally celebrated. A single object that could serve as phone, camera, music player, web browser, and so much more promised unprecedented convenience and connectivity. It was, quite literally, the dream of the nineties. But the better part of twenty years later, we’ve gained enough perspective to recognize that this revolutionary vision came with costs we did not anticipate. Distraction, of course, is the one we can all relate to first. An everything device has the problem of being useful nearly all the time, and when in use, all consuming. When you use it to do one thing, it pushes you toward others. In order to avoid this, you must disable functions. That’s an interesting turn of events, isn’t it? We have made a thing that does more than we need, more often than we desire. Because system-wide, duplicative notifications are enabled by default, the best thing you could say about the device’s design is that it lacks a point of view toward a prioritization of what it does. The worst thing you could say is that it is distracting by design. (I find it fascinating how many people – myself included — attempt to reduce the features of their smartphone to the point of replicating a “dumbphone” experience in order to save ourselves from distraction, but don’t actually go so far as to use a lesser-featured phone because a few key features are just too good to give up. A dumbphone is less distracting, but a nightmare for text messaging and a lousy camera. It turns out I don’t want a phone at all, but a camera that texts — and ideally one smaller than anything on the market now. I know I’m not alone, and yet this product will not be made. ) This kind of distraction is direct distraction. It’s the kind we are increasingly aware of, and as its accumulating stress puts pressure on our inner and outer lives, we can combat it with various choices and optimizations. But there is another kind of distraction that is less direct, though just as cumulative and, I believe, just as toxic. I’ve come to think of it as the “digital echo.” On a smartphone, every single thing it is used to do generates information that goes elsewhere. The vast majority of this is unseen — though not unfelt — by us. Everyone knows that there is no privacy within a digital device, nor within its “listening” range. We are all aware that as much information as smartphone provides to us, exponentially more is generated for someone else — someone watching, listening, measuring, and monetizing. The “digital echo” is more than just the awareness of this; it is the cognitive burden of knowing that our actions generate data elsewhere. The echo exists whenever we use connected technology, creating a subtle but persistent awareness that what we do isn’t just our own. A device like a smartphone has always generated a “digital echo”, but many others are as well. Comparing two different motor vehicles illustrates this well. In a car like a Tesla, which we might think of as a “smartcar” since it’s a computer you can drive, every function produces a digital signal. Adjusting the air conditioning, making a turn, opening a door — the car knows and records it all, transmitting this information to distant servers. By contrast, my 15-year-old Honda performs all of its functions without creating these digital echoes. The operations remain private, existing only in the moment they occur. In our increasingly digital world, I have begun to feel the SCIF-like isolation of the cabin of my car, and I like it. (The “smartcar”, of course, won’t remain simply a computer you can drive. The penultimate “smartcar” drives itself. The self-driving car represents perhaps the most acute expression of how digital culture values attention and convenience above all else, especially control and ownership. As a passenger of a self-driving car, you surrender control over the vehicle’s operation in exchange for the “freedom” to direct your attention elsewhere, most likely to some digital signal either on your own device or on screens within the vehicle. I can see the value in this; driving can be boring and most times I am behind the wheel I’d rather be doing something else. But currently, truly autonomous vehicles are service-enabling products like Waymo, meaning we also relinquish ownership. The benefits of that also seem obvious: no insurance premiums, no maintenance costs. But not every advantage is worth its cost. The economics of self-driving cars are not clear-cut. There’s a real debate to be had about attention, convenience, and ownership that I hope will play out before we have no choice but to be a passenger in someone else’s machine.) When I find myself looking for new ways to throttle my smartphone’s functions, or when I sit in the untapped isolation of my car, I often wonder about the costs of the “digital echo.” What is the psychological cost of knowing that your actions aren’t just your own, but create information that can be observed and analyzed by others? As more aspects of our lives generate digital echoes, they force an ambient awareness of being perpetually witnessed rather than simply existing. This transforms even solitary activities into implicit social interactions. It forces us to maintain awareness of our “observed self” alongside our “experiencing self,” creating a kind of persistent self-consciousness. We become performers in our own lives rather than merely participants. I think this growing awareness contributes to a growing interest in returning to single-focus devices and analog technologies. Record players and film cameras aren’t experiencing resurgence merely from nostalgia, but because they offer fundamentally different relationships with media — relationships characterized by intention, presence, and focus. In my own life, this recognition has led to deliberate choices about which technologies to embrace and which to avoid. Here are three off the top of my head: Replacing streaming services with owned media formats (CDs, Blu-rays) that remain accessible on my terms, not subject to platform changes or content disappearance Preferring printed books while using dedicated e-readers for digital texts — in this case, accepting certain digital echoes when the benefits (in particular, access to otherwise unavailable material) outweigh the costs Rejecting smart home devices entirely, recognizing that their convenience rarely justifies the added complexity and surveillance they introduce You’ve probably made similarly-motivated decisions, perhaps in other areas of your life or in relation to other things entirely. What matters, I think, is that these choices aren’t about rejecting technology but about creating spaces for more intentional engagement. They represent a search for balance in a world that increasingly defaults to maximum connectivity. I had a conversation recently with a friend who mused, “What are these the early days of?” What a wonderful question that is; we are, I hope, always living in the early days of something. Perhaps now, we’re witnessing the beginning of a new phase in our relationship with technology. The initial wave of digital transformation prioritized connecting everything possible; the next wave may be more discriminating about what should be connected and what’s better left direct and immediate. I hope to see operating systems truly designed around focus rather than multitasking, interfaces that respect attention rather than constantly competing for it, and devices that serve discrete purposes exceptionally well instead of performing multiple functions adequately. The digital echoes of our actions will likely continue to multiply, but we can choose which echoes we’re willing to generate and which activities deserve to remain ephemeral — to exist only in the moment they occur and then in the memories of those present. What looks like revision or retreat may be the next wave of innovation, borne out of having learned the lessons of the last few decades and desiring better for the next.
01 Intro Conversational interfaces are a bit of a meme. Every couple of years a shiny new AI development emerges and people in tech go “This is it! The next computing paradigm is here! We’ll only use natural language going forward!”. But then nothing actually changes and we continue using computers the way we always […]
Mid-Autumn Festival is not only an occasion for people to gather and enjoy delicious mooncakes, but also a moment to...