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
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.
So there I was, experimenting with HTML password inputs and Web Components. I'm not sure why the idea even came up but it quickly snowballed into a curious expedition. The result from the journey was a set of custom elements that provide extra functionality and information about the text being typed into a password input field. I shared my CodePen demo in a Mastodon post and soon after decided to push these scripts up to a GitHub repo. Open CodePen demo Get started The repo includes two Web Component scripts. They operate independent of one another. I recommend reading through the repo documentation but here's a rundown of what's included. <password-rules> adds an input event listener to capture when a list of rules (password length, includes an uppercase letter, etc.) are matched as the user is typing in their new password. <password-toggle> shows and hides the password input value on click. To get started, add the scripts to a project and include them on the page. <script type="module" src="path/to/password-rules.js"></script> <script type="module" src="path/to/password-toggle.js"></script> Below is an example of using both custom elements with a password input. <label for="new-password">Password</label> <input type="password" id="new-password" /> <div id="status" aria-live="polite"></div> <password-toggle data-input-id="new-password" data-status-id="status"> <button type="button">Toggle password visibility</button> </password-toggle> <password-rules data-input-id="new-password" data-rules=".{9}, [A-Z], .*\d"> <ul> <li data-rule-index="0">Longer than 8 characters</li> <li data-rule-index="1">Includes an uppercase letter</li> <li data-rule-index="2">Includes a number</li> </ul> </password-rules> Password toggle password-toggle expects a button element to be inside it. This button will be augmented with the ability to toggle the visibility of the input field's value. When the toggle button is clicked, the "status" element containing the aria-live attribute will send a notification to screen readers that the password value is currently visible or hidden. For instance, when clicking for the first time, the string "Password is visible" is inserted into the container and announced by a screen reader. We can also style the toggle button when it enters its pressed or "visible password" state. In the CodePen demo, this is how the eye icon (aye aye!) is being swapped. button svg:last-of-type { display: none; } button[aria-pressed="true"] { svg:first-of-type { display: none; } svg:last-of-type { display: block; } } Targeting the [aria-pressed] attribute selector ensures that our styles stay in sync with their accessibility counterpart. It also means that we don't need to manage a semantic attribute value as well as some generic class selector like "is-active". Ben Myers shares great knowledge on this subject in Style with stateful, semantic selectors. A must-have in the bookmarks 🏆 Password rules The password-rules element is passed a comma-separated list of regular expression strings, each related to a specific rule. We also have the option to connect any child element to a rule by passing the index of that string to a data-rule-index attribute. The placement or type of element doesn't matter as long as it's contained within the password-rules. Here's an alternate version to drive that point home: <password-rules data-input-id="new-password" data-rules=".{8}, [A-Z], .*\d"> <div class="one-column" data-rule-index="0"> Longer than 8 characters </div> <div class="two-columns"> <span data-rule-index="2">Includes a number</span> <span data-rule-index="1">Includes an uppercase letter</span> </div> </password-rules> Check it off When a rule is met that matches the data-rule-index value on an element, an is-match class gets added to the element. The demo styles use this selector to add a checkmark emoji when present. .password-rules__checklist .is-match::before { content: "✅"; } score/total The current password "score" and rules "total" are passed to the custom element as data attributes and CSS variables. The score value updates as rules are met. This allows us to do some fancy things like change the colors in a score meter and present the current tally. All of it done with CSS. /** Incrementally adjust background colors */ password-rules[data-score="1"] .password-rules__meter :first-child, password-rules[data-score="2"] .password-rules__meter :nth-child(-n + 2), password-rules[data-score="3"] .password-rules__meter :nth-child(-n + 3), password-rules[data-score="4"] .password-rules__meter :nth-child(-n + 4) { background-color: dodgerblue; } /** When all rules are met, swap to a new color for each meter element */ password-rules[data-score="5"] .password-rules__meter :nth-child(-n + 5) { background-color: mediumseagreen; } CSS variables are passed into a CSS counter() to render the current score and total. .password-rules__score::before { counter-reset: score var(--score, 0) total var(--total, 5); content: counter(score) "/" counter(total); } I added fallback values to the CSS variables when I realized that the --total value, specifically, renders as 0 on page load and doesn't update until we begin typing in the input field. I did discover that we could skip the fallback by registering the custom property. This ensures the total is correctly reflected when the component initializes. But, to be honest, this feels unnecessary when the fallback here will suffice. @property --total { syntax: "<number>"; initial-value: 0; inherits: true; } If this @property stuff is unfamiliar, Stephanie Eckles has got you covered in Providing Type Definitions for CSS with @property. Another one to bookmark! I've also recently spent time with this newly supported at-rule in CSS @property and the New Style. Progressively enhanced for the win I believe this tells a fairly nice progressive enhancement story. Without JavaScript, the password input still works as expected. But when these scripts run, users get additional feedback and interactivity. Developers get access to extra selectors that can be useful for styling state changes. And listen, I get it–there are better ways to handle client-side form validation, but this was a fun exploration nonetheless.
More in design
This by no means a definitive list, so don’t @ me! AI is an inescapable subject. There’s obviously an incredible headwind behind the computing progress of the last handful of years — not to mention the usual avarice — but there has also been nearly a century of thought put toward artificial intelligence. If you want to have a more robust understanding of what is at work beneath, say, the OpenAI chat box, pick any one of these texts. Each one would be worth a read — even a skim (this is by no means light reading). At the very least, familiarizing yourself with the intellectual path leading to now will help you navigate the funhouse of overblown marketing bullshit filling the internet right now, especially as it pertains to AGI. Read what the heavyweights had to say about it and you’ll see how many semantic games are being played while also moving the goalposts. Steps to an Ecology of Mind (1972) — Gregory Bateson. Through imagined dialogues with his daughter, Bateson explores how minds emerge from systems of information and communication, providing crucial insights for understanding artificial intelligence. The Sciences of the Artificial (1969) — Herbert Simon examines how artificial systems, including AI, differ from natural ones and introduces key concepts about bounded rationality. The Emperor’s New Mind (1989) — Roger Penrose. While arguing against strong AI, provides valuable insights into consciousness and computation that remain relevant to current AI discussions. Gödel, Escher, Bach: An Eternal Golden Braid (1979) — Douglas Hofstadter weaves together mathematics, art, and music to explore consciousness, self-reference, and emergent intelligence. Though not explicitly about AI, it provides fundamental insights into how complex cognition might emerge from simple rules and patterns. Perceptrons (1969) — Marvin Minsky & Seymour Papert. This controversial critique of neural networks temporarily halted research in the field but ultimately helped establish its theoretical foundations. Minsky and Papert’s mathematical analysis revealed both the limitations and potential of early neural networks. The Society of Mind (1986) — Marvin Minsky proposes that intelligence emerges from the interaction of simple agents working together, rather than from a single unified system. This theoretical framework remains relevant to understanding both human cognition and artificial intelligence. Computers and Thought (1963) — Edward Feigenbaum & Julian Feldman (editors) This is the first collection of articles about artificial intelligence, featuring contributions from pioneers like Herbert Simon and Allen Newell. It captures the foundational ideas and optimism of early AI research. Artificial Intelligence: A Modern Approach (1995) — Stuart Russell & Peter Norvig. This comprehensive textbook defined how AI would be taught for decades. It presents AI as rational agent design rather than human intelligence simulation, a framework that still influences the field. Computing Machinery and Intelligence (1950) — Alan Turing’s paper introduces the Turing Test and addresses fundamental questions about machine intelligence that we’re still grappling with today. It’s remarkable how many current AI debates were anticipated in this work. Cybernetics: Or Control and Communication in the Animal and the Machine (1948) — Norbert Wiener established the theoretical groundwork for understanding control systems in both machines and living things. His insights about feedback loops and communication remain crucial to understanding AI systems.
My thoughts about the Zettelkasten (Slip box) note taking methodology invented by the German sociologist Niklas Luhmann.
Chinese interior studio Various Associates has completed an irregular pyramid-shaped flagship store for drone brand DJI in Shenzhen, China. Located...
John Gruber has a post about how Google’s search results now require JavaScript[1]. Why? Here’s Google: the change is intended to “better protect” Google Search against malicious activity, such as bots and spam Lol, the irony. Let’s turn to JavaScript for protection, as if the entire ad-based tracking/analytics world born out of JavaScript’s capabilities isn’t precisely what led to a less secure, less private, more exploited web. But whatever, “the web” is Google’s product so they can do what they want with it — right? Here’s John: Old original Google was a company of and for the open web. Post 2010-or-so Google is a company that sees the web as a de facto proprietary platform that it owns and controls. Those who experience the web through Google Chrome and Google Search are on that proprietary not-closed-per-se-but-not-really-open web. Search that requires JavaScript won’t cause the web to die. But it’s a sign of what’s to come (emphasis mine): Requiring JavaScript for Google Search is not about the fact that 99.9 percent of humans surfing the web have JavaScript enabled in their browsers. It’s about taking advantage of that fact to tightly control client access to Google Search results. But the nature of the true open web is that the server sticks to the specs for the HTTP protocol and the HTML content format, and clients are free to interpret that as they see fit. Original, novel, clever ways to do things with website output is what made the web so thrilling, fun, useful, and amazing. This JavaScript mandate is Google’s attempt at asserting that it will only serve search results to exactly the client software that it sees fit to serve. Requiring JavaScript is all about control. The web was founded on the idea of open access for all. But since that’s been completely and utterly abused (see LLM training datasets) we’re gonna lose it. The whole “freemium with ads” model that underpins the web was exploited for profit by AI at an industrial scale and that’s causing the “free and open web” to become the “paid and private web”. Universal access is quickly becoming select access — Google search results included. If you want to go down a rabbit hole of reading more about this, there’s the TechCrunch article John cites, a Hacker News thread, and this post from a company founded on providing search APIs. ⏎ Email :: Mastodon :: Bluesky #generalNotes
Kedrovka is a brand of plant-based milk crafted for those who care about their health, value natural ingredients, and seek...