Developer News
Orbital Mechanics (or How I Optimized a CSS Keyframes Animation)
I recently updated my portfolio at johnrhea.com. (If you’re looking to add a CSS or front-end engineer with storytelling and animation skills to your team, I’m your guy.) I liked the look of a series of planets I’d created for another personal project and decided to reuse them on my new site. Part of that was also reusing an animation I’d built circa 2019, where a moon orbited around the planet.
Initially, I just plopped the animations into the new site, only changing the units (em units to viewport units using some complicated math that I was very, very proud of) so that they would scale properly because I’m… efficient with my time. However, on mobile, the planet would move up a few pixels and down a few pixels as the moons orbited around it. I suspected the plopped-in animation was the culprit (it wasn’t, but at least I got some optimized animation and an article out of the deal).
Here’s the original animation:
CodePen Embed FallbackMy initial animation for the moon ran for 60 seconds. I’m folding it inside a disclosure widget because, at 141 lines, it’s stupid long (and, as we’ll see, emphasis on the stupid). Here it is in all its “glory”:
Open code #moon1 { animation: moon-one 60s infinite; } @keyframes moon-one { 0% { transform: translate(0, 0) scale(1); z-index: 2; animation-timing-function: ease-in; } 5% { transform: translate(-3.51217391vw, 3.50608696vw) scale(1.5); z-index: 2; animation-timing-function: ease-out; } 9.9% { z-index: 2; } 10% { transform: translate(-5.01043478vw, 6.511304348vw) scale(1); z-index: -1; animation-timing-function: ease-in; } 15% { transform: translate(1.003478261vw, 2.50608696vw) scale(0.25); z-index: -1; animation-timing-function: ease-out; } 19.9% { z-index: -1; } 20% { transform: translate(0, 0) scale(1); z-index: 2; animation-timing-function: ease-in; } 25% { transform: translate(-3.51217391vw, 3.50608696vw) scale(1.5); z-index: 2; animation-timing-function: ease-out; } 29.9% { z-index: 2; } 30% { transform: translate(-5.01043478vw, 6.511304348vw) scale(1); z-index: -1; animation-timing-function: ease-in; } 35% { transform: translate(1.003478261vw, 2.50608696vw) scale(0.25); z-index: -1; animation-timing-function: ease-out; } 39.9% { z-index: -1; } 40% { transform: translate(0, 0) scale(1); z-index: 2; animation-timing-function: ease-in; } 45% { transform: translate(-3.51217391vw, 3.50608696vw) scale(1.5); z-index: 2; animation-timing-function: ease-out; } 49.9% { z-index: 2; } 50% { transform: translate(-5.01043478vw, 6.511304348vw) scale(1); z-index: -1; animation-timing-function: ease-in; } 55% { transform: translate(1.003478261vw, 2.50608696vw) scale(0.25); z-index: -1; animation-timing-function: ease-out; } 59.9% { z-index: -1; } 60% { transform: translate(0, 0) scale(1); z-index: 2; animation-timing-function: ease-in; } 65% { transform: translate(-3.51217391vw, 3.50608696vw) scale(1.5); z-index: 2; animation-timing-function: ease-out; } 69.9% { z-index: 2; } 70% { transform: translate(-5.01043478vw, 6.511304348vw) scale(1); z-index: -1; animation-timing-function: ease-in; } 75% { transform: translate(1.003478261vw, 2.50608696vw) scale(0.25); z-index: -1; animation-timing-function: ease-out; } 79.9% { z-index: -1; } 80% { transform: translate(0, 0) scale(1); z-index: 2; animation-timing-function: ease-in; } 85% { transform: translate(-3.51217391vw, 3.50608696vw) scale(1.5); z-index: 2; animation-timing-function: ease-out; } 89.9% { z-index: 2; } 90% { transform: translate(-5.01043478vw, 6.511304348vw) scale(1); z-index: -1; animation-timing-function: ease-in; } 95% { transform: translate(1.003478261vw, 2.50608696vw) scale(0.25); z-index: -1; animation-timing-function: ease-out; } 99.9% { z-index: -1; } 100% { transform: translate(0, 0) scale(1); z-index: 2; animation-timing-function: ease-in; } }If you look at the keyframes in that code, you’ll notice that the 0% to 20% keyframes are exactly the same as 20% to 40% and so on up through 100%. Why I decided to repeat the keyframes five times infinitely instead of just repeating one set infinitely is a decision lost to antiquity, like six years ago in web time. We can also drop the duration to 12 seconds (one-fifth of sixty) if we were doing our due diligence.
I could thus delete everything from 20% on, instantly dropping the code down to 36 lines. And yes, I realize gains like this are unlikely to be possible on most sites, but this is the first step for optimizing things.
#moon1 { animation: moon-one 12s infinite; } @keyframes moon-one { 0% { transform: translate(0, 0) scale(1); z-index: 2; animation-timing-function: ease-in; } 5% { transform: translate(-3.51217391vw, 3.50608696vw) scale(1.5); z-index: 2; animation-timing-function: ease-out; } 9.9% { z-index: 2; } 10% { transform: translate(-5.01043478vw, 6.511304348vw) scale(1); z-index: -1; animation-timing-function: ease-in; } 15% { transform: translate(1.003478261vw, 2.50608696vw) scale(0.25); z-index: -1; animation-timing-function: ease-out; } 19.9% { z-index: -1; } 20% { transform: translate(0, 0) scale(1); z-index: 2; animation-timing-function: ease-in; } }Now that we’ve gotten rid of 80% of the overwhelming bits, we can see that there are five main keyframes and two additional ones that set the z-index close to the middle and end of the animation (these prevent the moon from dropping behind the planet or popping out from behind the planet too early). We can change these five points from 0%, 5%, 10%, 15%, and 20% to 0%, 25%, 50%, 75%, and 100% (and since the 0% and the former 20% are the same, we can remove that one, too). Also, since the 10% keyframe above is switching to 50%, the 9.9% keyframe can move to 49.9%, and the 19.9% keyframe can switch to 99.9%, giving us this:
#moon1 { animation: moon-one 12s infinite; } @keyframes moon-one { 0%, 100% { transform: translate(0, 0) scale(1); z-index: 2; animation-timing-function: ease-in; } 25% { transform: translate(-3.51217391vw, 3.50608696vw) scale(1.5); z-index: 2; animation-timing-function: ease-out; } 49.9% { z-index: 2; } 50% { transform: translate(-5.01043478vw, 6.511304348vw) scale(1); z-index: -1; animation-timing-function: ease-in; } 75% { transform: translate(1.003478261vw, 2.50608696vw) scale(0.25); z-index: -1; animation-timing-function: ease-out; } 99.9% { z-index: -1; } }Though I was very proud of myself for my math wrangling, numbers like -3.51217391vw are really, really unnecessary. If a screen was one thousand pixels wide, -3.51217391vw would be 35.1217391 pixels. No one ever needs to go down to the precision of a ten-millionth of a pixel. So, let’s round everything to the tenth place (and if it’s a 0, we’ll just drop it). We can also skip z-index in the 75% and 25% keyframes since it doesn’t change.
Here’s where that gets us in the code:
#moon1 { animation: moon-one 12s infinite; } @keyframes moon-one { 0%, 100% { transform: translate(0, 0) scale(1); z-index: 2; animation-timing-function: ease-in; } 25% { transform: translate(-3.5vw, 3.5vw) scale(1.5); z-index: 2; animation-timing-function: ease-out; } 49.9% { z-index: 2; } 50% { transform: translate(-5vw, 6.5vw) scale(1); z-index: -1; animation-timing-function: ease-in; } 75% { transform: translate(1vw, 2.5vw) scale(0.25); z-index: -1; animation-timing-function: ease-out; } 99.9% { z-index: -1; } }After all our changes, the animation still looks pretty close to what it was before, only way less code:
CodePen Embed FallbackOne of the things I don’t like about this animation is that the moon kind of turns at its zenith when it crosses the planet. It would be much better if it traveled in a straight line from the upper right to the lower left. However, we also need it to get a little larger, as if the moon is coming closer to us in its orbit. Because both translation and scaling were done in the transform property, I can’t translate and scale the moon independently.
If we skip either one in the transform property, it resets the one we skipped, so I’m forced to guess where the mid-point should be so that I can set the scale I need. One way I’ve solved this in the past is to add a wrapping element, then apply scale to one element and translate to the other. However, now that we have individual scale and translate properties, a better way is to separate them from the transform property and use them as separate properties. Separating out the translation and scaling shouldn’t change anything, unless the original order they were declared on the transform property was different than the order of the singular properties.
#moon1 { animation: moon-one 12s infinite; } @keyframes moon-one { 0%, 100% { translate: 0 0; scale: 1; z-index: 2; animation-timing-function: ease-in; } 25% { translate: -3.5vw 3.5vw; z-index: 2; animation-timing-function: ease-out; } 49.9% { z-index: 2; } 50% { translate: -5vw 6.5vw; scale: 1; z-index: -1; animation-timing-function: ease-in; } 75% { translate: 1vw 2.5vw; scale: 0.25; animation-timing-function: ease-out; } 99.9% { z-index: -1; } }Now that we can separate the scale and translate properties and use them independently, we can drop the translate property in the 25% and 75% keyframes because we don’t want them placed precisely in that keyframe. We want the browser’s interpolation to take care of that for us so that it translates smoothly while scaling.
#moon1 { animation: moon-one 12s infinite; } @keyframes moon-one { 0%, 100% { translate: 0 0; scale: 1; z-index: 2; animation-timing-function: ease-in; } 25% { scale: 1.5; animation-timing-function: ease-out; } 49.9% { z-index: 2; } 50% { translate: -5vw 6.5vw; scale: 1; z-index: -1; animation-timing-function: ease-in; } 75% { scale: 0.25; animation-timing-function: ease-out; } 99.9% { z-index: -1; } } CodePen Embed FallbackLastly, those different timing functions don’t make a lot of sense anymore because we’ve got the browser working for us, and if we use an ease-in-out timing function on everything, then it should do exactly what we want.
#moon1 { animation: moon-one 12s infinite ease-in-out; } @keyframes moon-one { 0%, 100% { translate: 0 0; scale: 1; z-index: 2; } 25% { scale: 1.5; } 49.9% { z-index: 2; } 50% { translate: -5vw 6.5vw; scale: 1; z-index: -1; } 75% { scale: 0.25; } 99.9% { z-index: -1; } } CodePen Embed FallbackAnd there you go: 141 lines down to 28, and I think the animation looks even better than before. It will certainly be easier to maintain, that’s for sure.
But what do you think? Was there an optimization step I missed? Let me know in the comments.
Orbital Mechanics (or How I Optimized a CSS Keyframes Animation) originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Why is Nobody Using the hwb() Color Function?
Okay, nobody is an exaggeration, but have you seen the stats for hwb()? They show a steep decline, and after working a lot on color in the CSS-Tricks almanac, I’ve just been wondering why that is.
hwb() is a color function in the sRGB color space, which is the same color space used by rgb(), hsl() and the older hexadecimal color format (e.g. #f8a100). hwb() is supposed to be more intuitive and easier to work with than hsl(). I kinda get why it’s considered “easier” since you specify how much black or white you want to add to a given color. But, how is hwb() more intuitive than hsl()?
hwb() accepts three values, and similar to hsl(), the first value specifies the color’s hue (between 0deg–360deg), while the second and third values add whiteness (0 – 100) and blackness (0 – 100) to the mix, respectively.
According to Google, the term “intuitive” means “what one feels to be true even without conscious reasoning; instinctive.” As such, it does truly seem that hwb() is more intuitive than hsl(), but it’s only a slight notable difference that makes that true.
Let’s consider an example with a color. We’ll declare light orange in both hsl() and hwb():
/* light orange in hsl */ .element-1 { color: hsl(30deg 100% 75%); } /* light orange in hwb() */ .element-2 { color: hwb(30deg 50% 0%); }These two functions produce the exact same color, but while hwb() handles ligthness with two arguments, hsl() does it with just one, leaving one argument for the saturation. By comparison, hwb() provides no clear intuitive way to set just the saturation. I’d argue that makes the hwb() function less intuitive than hsl().
I think another reason that hsl() is generally more intuitive than hwb() is that HSL as a color model was created in the 1970s while HWB as a color model was created in 1996. We’ve had much more time to get acquainted with hsl() than we have hwb(). hsl() was implemented by browsers as far back as 2008, Safari being the first and other browsers following suit. Meanwhile, hwb() gained support as recently as 2021! That’s more than a 10-year gap between functions when it comes to using them and being familiar with them.
There’s also the fact that other color functions that are used to represent colors in other color spaces — such as lab(), lch(), oklab(), and oklch() — offer more advantages, such as access to more colors in the color gamut and perceptual uniformity. So, maybe being intuitive is coming at the expense of having a more robust feature set, which could explain why you might go with a less intuitive function that doesn’t use sRGB.
Look, I can get around the idea of controlling how white or black you want a color to look based on personal preferences, and for designers, it’s maybe easier to mix colors that way. But I honestly would not opt for this as my go-to color function in the sRGB color space because hsl() does something similar using the same hue, but with saturation and lightness as the parameters which is far more intuitive than what hwb() offers.
I see our web friend, Stefan Judis, preferring hsl() over hwb() in his article on hwb().
Lea Verou even brought up the idea of removing hwb() from the spec in 2022, but a decision was made to leave it as it was since browsers were already implementing the function. And although,I was initially pained by the idea of keeping hwb() around, I also quite understand the feeling of working on something, and then seeing it thrown in the bin. Once we’ve introduced something, it’s always tough to walk it back, especially when it comes to maintaining backwards compatibility, which is a core tenet of the web.
I would like to say something though: lab(), lch(), oklab(), oklch() are already here and are better color functions than hwb(). I, for one, would encourage using them over hwb() because they support so many more colors that are simply missing from the hsl() and hwb() functions.
I’ve been exploring colors for quite some time now, so any input would be extremely helpful. What color functions are you using in your everyday website or web application, and why?
More on color Almanac on Feb 22, 2025 hsl() .element { color: hsl(90deg, 50%, 50%); } color Sunkanmi Fafowora Almanac on Mar 4, 2025 lab() .element { color: lab(50% 50% 50% / 0.5); } color Sunkanmi Fafowora Almanac on Mar 12, 2025 lch() .element { color: lch(10% 0.215 15deg); } color Sunkanmi Fafowora Almanac on Apr 29, 2025 oklab() .element { color: oklab(25.77% 25.77% 54.88%; } color Sunkanmi Fafowora Almanac on May 10, 2025 oklch() .element { color: oklch(70% 0.15 240); } color Gabriel ShoyomboWhy is Nobody Using the hwb() Color Function? originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
GSAP is Now Completely Free, Even for Commercial Use!
Back in October, the folks behind the GreenSock Animation Platform (GSAP) joined forces with Webflow, the visual website builder. Now, the team’s back with another announcement: Along with the version 3.13 release, GSAP, and all its awesome plugins, are now freely available to everyone.
Thanks to Webflow GSAP is now 100% free including all of the bonus plugins like SplitText, MorphSVG, and all the others that were exclusively available to Club GSAP members. That’s right, the entire GSAP toolset is free, even for commercial use! 🤯
Webflow is celebrating over on their blog as well:
With Webflow’s support, the GSAP team can continue to lead the charge in product and industry innovation while allowing even more developers the opportunity to harness the full breadth of GSAP-powered motion.
Check out the GSAP blog to read more about the announcement, then go animate something awesome and share it with us!
GSAP is Now Completely Free, Even for Commercial Use! originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Modern Scroll Shadows Using Scroll-Driven Animations
Using scroll shadows, especially for mobile devices, is a subtle bit of UX that Chris has covered before (indeed, it’s one of his all-time favorite CSS tricks), by layering background gradients with different attachments, we can get shadows that are covered up when you’ve scrolled to the limits of the element.
Geoff covered a newer approach that uses the animation-timeline property. Using animation-timeline, we can tie CSS animation to the scroll position. His example uses pseudo-elements to render the scroll shadows, and animation-range to animate the opacity of the pseudo-elements based on scroll.
Here’s yet another way. Instead of using shadows, let’s use a CSS mask to fade out the edges of the scrollable element. This is a slightly different visual metaphor that works great for horizontally scrollable elements — places where your scrollable element doesn’t have a distinct border of its own. This approach still uses animation-timeline, but we’ll use custom properties instead of pseudo-elements. Since we’re fading, the effect also works regardless of whether we’re on a dark or light background.
Getting started with a scrollable elementFirst, we’ll define our scrollable element with a mask that fades out the start and end of the container. For this example, let’s consider the infamous table that can’t be responsive and has to be horizontally scrollable on mobile.
Let’s add the mask. We can use the shorthand and find the mask as a linear gradient that fades out on either end. A mask lets the table fade into the background instead of overlaying a shadow, but you could use the same technique for shadows.
CodePen Embed Fallback .scrollable { mask: linear-gradient(to right, #0000, #ffff 3rem calc(100% - 3rem), #0000); } Defining the custom properties and animationNext, we need to define our custom properties and the animation. We’ll define two separate properties, --left-fade and --right-fade, using @property. Using @property is necessary here to specify the syntax of the properties so that browsers can animate the property’s values.
@property --left-fade { syntax: "<length>"; inherits: false; initial-value: 0; } @property --right-fade { syntax: "<length>"; inherits: false; initial-value: 0; } @keyframes scrollfade { 0% { --left-fade: 0; } 10%, 100% { --left-fade: 3rem; } 0%, 90% { --right-fade: 3rem; } 100% { --right-fade: 0; } }Instead of using multiple animations or animation-range, we can define a single animation where --left-fade animates from 0 to 3rem between 0-10%, and --right-fade animates from 3rem to 0 between 90-100%. Now we update our mask to use our custom properties and tie the scroll-timeline of our element to its own animation-timeline.
Putting it all togetherPutting it all together, we have the effect we’re after:
CodePen Embed FallbackWe’re still waiting for some browsers (Safari) to support animation-timeline, but this gracefully degrades to simply not fading the element at all.
Wrapping upI like this implementation because it combines two newer bits of CSS — animating custom properties and animation-timeline — to achieve a practical effect that’s more than just decoration. The technique can even be used with scroll-snap-based carousels or cards:
CodePen Embed FallbackIt works regardless of content or background and doesn’t require JavaScript. It exemplifies just how far CSS has come lately.
Modern Scroll Shadows Using Scroll-Driven Animations originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Make the AI Models do the Prompting
Despite all the mind-blowing advances in AI models over the past few years, they still face a massive obstacle to achieving their potential: people don't know what AI can do nor how to guide it. One of the ways we've been addressing this is by having LLMs rewrite people's prompts.
Prompt Writing & EditingThe preview release of Reve's (our AI for creative tooling company) text to image model helps people get better image generation results by re-writing their prompts in several ways.
Reve's enhance feature (on by default) takes someone's image prompt and re-writes it in a way that optimizes for a better result but also teaches people about the image model's capabilities. Reve is especially strong at adhering to very detailed prompts but many people's initial instructions are short and vague. To get to a better result, the enhance feature drafts a much comprehensive prompt which not only makes Reve's strengths clear but also teaches people how to get the most of the model.
The enhance feature also harmonizes prompts when someone make changes. For instance, if the prompt includes several mentions of the main subject, like a horse, and you change one of them to a cow, the enhance feature will make sure to harmonize all the "horse" mentions to "cow" for you.
But aren't these long prompts too complicated for most people to edit? This is why the default mode in Reve is instruct and prompt editing is one click away. Through natural language instructions, people can edit any image they create without having to dig through a wall of prompt text.
Even better, though, is starting an image generation with an image. In this approach you simply upload an image and Reve writes a comprehensive prompt for it. From there you can either use the instruct mode to make changes or dive into the full prompt to make edits.
Plan Creation & Tool UseAs if it wasn't hard enough to prompt an AI model to do what you want, things get even harder with agentic interfaces. When AI models can make use of tools to get things done in addition to using their own built-in capabilities, people now have to know not only what AI models can do but what the tools they have access to can do as well.
In response to an instruction in Bench (our AI for knowledge work company), the system uses an AI model to plan an appropriate set of actions in response. This plan includes not only the tools (search, browse, fact check, create PowerPoint, etc.) that make the most sense to complete the task but also their settings. Since people don't know what tools Bench can use nor what parameters the tools accept, once again an AI model rewrites people's prompts for them into something much more effective.
For instance, when using the search tool, Bench will not only decide on and execute the most relevant search queries but also set parameters like date range or site-specific constraints. In most cases, people don't need to worry about these parameters. In fact, we put them all behind a little settings icon so people can focus on the results of their task and let Bench do the thinking. But in cases where people want to make modifications to the choices Bench made, they can.
Behind the scenes in Bench, the system not only re-writes people's instructions to pick and make effective use of tools but it also decides which AI models to call and when. How much of that should be exposed to people so they can both modify it if needed and understand how things work has been a topic of debate. There's clearly a tradeoff with doing everything for people automatically and giving them more explicit (but more complicated) controls.
At a high level, though, AI models are much better at writing prompts for AI models than most people are. So the approach we've continued to take is letting the AI models rewrite and optimize people's initial prompts for the best possible outcome.
CSS shape() Commands
The CSS shape() function recently gained support in both Chromium and WebKit browsers. It’s a way of drawing complex shapes when clipping elements with the clip-path property. We’ve had the ability to draw basic shapes for years — think circle, ellipse(), and polygon() — but no “easy” way to draw more complex shapes.
Well, that’s not entirely true. It’s true there was no “easy” way to draw shapes, but we’ve had the path() function for some time, which we can use to draw shapes using SVG commands directly in the function’s arguments. This is an example of an SVG path pulled straight from WebKit’s blog post linked above:
<svg viewBox="0 0 150 100" xmlns="http://www.w3.org/2000/svg"> <path fill="black" d="M0 0 L 100 0 L 150 50 L 100 100 L 0 100 Q 50 50 0 0 z " /> </svg>Which means we can yank those <path> coordinates and drop them into the path() function in CSS when clipping a shape out of an element:
.clipped { clip-path: path("M0 0 L 100 0 L 150 50 L 100 100 L 0 100 Q 50 50 0 0 z"); }I totally understand what all of those letters and numbers are doing. Just kidding, I’d have to read up on that somewhere, like Myriam Frisano’s more recent “Useful Recipes For Writing Vectors By Hand” article. There’s a steep learning curve to all that, and not everyone — including me — is going down that nerdy, albeit interesting, road. Writing SVG by hand is a niche specialty, not something you’d expect the average front-ender to know. I doubt I’m alone in saying I’d rather draw those vectors in something like Figma first, export the SVG code, and copy-paste the resulting paths where I need them.
The shape() function is designed to be more, let’s say, CSS-y. We get new commands that tell the browser where to draw lines, arcs, and curves, just like path(), but we get to use plain English and native CSS units rather than unreadable letters and coordinates. That opens us up to even using CSS calc()-ulations in our drawings!
Here’s a fairly simple drawing I made from a couple of elements. You’ll want to view the demo in either Chrome 135+ or Safari 18.4+ to see what’s up.
CodePen Embed FallbackSo, instead of all those wonky coordinates we saw in path(), we get new terminology. This post is really me trying to wrap my head around what those new terms are and how they’re used.
In short, you start by telling shape() where the starting point should be when drawing. For example, we can say “from top left” using directional keywords to set the origin at the top-left corner of the element. We can also use CSS units to set that position, so “from 0 0” works as well. Once we establish that starting point, we get a set of commands we can use for drawing lines, arcs, and curves.
I figured a table would help.
CommandWhat it meansUsageExampleslineA line that is drawn using a coordinate pairThe by keyword sets a coordinate pair used to determine the length of the line.line by -2px 3pxvlineVertical lineThe to keyword indicates where the line should end, based on the current starting point.The by keyword sets a coordinate pair used to determine the length of the line.vline to 50pxhlineHorizontal lineThe to keyword indicates where the line should end, based on the current starting point.
The by keyword sets a coordinate pair used to determine the length of the line.hline to 95%arcAn arc (oh, really?!). An elliptical one, that is, sort of like the rounded edges of a heart shape.The to keyword indicates where the arc should end.
The with keyword sets a pair of coordinates that tells the arc how far right and down the arc should slope.
The of keyword specifies the size of the ellipse that the arc is taken from. The first value provides the horizontal radius of the ellipse, and the second provides the vertical radius. I’m a little unclear on this one, even after playing with it.arc to 10% 50% of 1%curveA curved lineThe to keyword indicates where the curved line should end.
The with keyword sets “control points” that affect the shape of the curve, making it deep or shallow.curve to 0% 100% with 50% 0%smoothAdds a smooth Bézier curve command to the list of path data commandsThe to keyword indicates where the curve should end.
The by keyword sets a coordinate pair used to determine the length of the curve.
The with keyword specifies control points for the curve.smooth by 50% 50% with 50% 5%
The spec is dense, as you might expect with a lot of moving pieces like this. Again, these are just my notes, but let me know if there’s additional nuance you think would be handy to include in the table.
Oh, another fun thing: you can adjust the shape() on hover/focus. The only thing is that I was unable to transition or animate it, at least in the current implementation.
CodePen Embed FallbackCSS shape() Commands originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
State of Devs: A Survey for Every Developer
I don’t know if I should say this on a website devoted to programming, but I sometimes feel like *lowers voice* coding is actually the least interesting part of our lives.
After all, last time I got excited meeting someone at a conference it was because we were both into bouldering, not because we both use React. And The Social Network won an Oscar for the way it displayed interpersonal drama, not for its depiction of Mark Zuckerberg’s PHP code.
Yet for the past couple years, I’ve been running developer surveys (such as the State of JS and State of CSS) that only ask about code. It was time to fix that.
A new kind of surveyThe State of Devs survey is now open to participation, and unlike previous surveys it covers everything except code: career, workplace, but also health, hobbies, and more.
I’m hoping to answer questions such as:
- What are developers’ favorite recent movies and video games?
- What kind of physical activity do developers practice?
- How much sleep are we all getting?
But also address more serious topics, including:
- What do developers like about their workplace?
- What factors lead to workplace discrimination?
- What global issues are developers most concerned with?
Another benefit from branching out into new topics is the chance to reach out to new audiences.
It’s no secret that people who don’t fit the mold of the average developer (whether because of their gender, race, age, disabilities, or a myriad of other factors) often have a harder time getting involved in the community, and this also shows up in our data.
In the past, we’ve tried various outreach strategies to help address these imbalances in survey participation, but the results haven’t always been as effective as we’d hoped.
So this time, I thought I’d try something different and have the survey itself include more questions relevant to under-represented groups, asking about workplace discrimination:
As well as actions taken in response to said discrimination:
Yet while obtaining a more representative data sample as a result of this new focus would be ideal, it isn’t the only benefit.
The most vulnerable among us are often the proverbial canaries in the coal mine, suffering first from issues or policies that will eventually affect the rest of the community as well, if left unchecked.
So, facing these issues head-on is especially valuable now, at a time when “DEI” is becoming a new taboo, and a lot of the important work that has been done to make things slightly better over the past decade is at risk of being reversed.
The big questionsFinally, the survey also tries to go beyond work and daily life to address the broader questions that keep us up at night:
There’s been talk in recent years about keeping the workplace free of politics. And why I can certainly see the appeal in that, in 2025, it feels harder than ever to achieve that ideal. At a time when people are losing rights and governments are sliding towards authoritarianism, should we still pretend that everything is fine? Especially when you factor in the fact that the tech community is now a major political player in its own right…
So while I didn’t push too far in that direction for this first edition of the survey, one of my goals for the future is to get a better grasp of where exactly developers stand in terms of ideology and worldview. Is this a good idea, or should I keep my distance from any hot-button issues? Don’t hesitate to let me know what you think, or suggest any other topic I should be asking about next time.
In the meantime, go take the survey, and help us get a better picture of who exactly we all are!
State of Devs: A Survey for Every Developer originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Revisiting Image Maps
I mentioned last time that I’ve been working on a new website for Emmy-award-winning game composer Mike Worth. He hired me to create a highly graphical design that showcases his work.
Mike loves ’90s animation, particularly Disney’s Duck Tales and other animated series. He challenged me to find a way to incorporate their retro ’90s style into his design without making it a pastiche. But that wasn’t my only challenge. I also needed to achieve that ’90s feel by using up-to-the-minute code to maintain accessibility, performance, responsiveness, and semantics.
Designing for Mike was like a trip back to when mainstream website design seemed more spontaneous and less governed by conventions and best practices. Some people describe these designs as “whimsical”:
adjective
- spontaneously fanciful or playful
- given to whims; capricious
- quaint, unusual, or fantastic
— Collins English Dictionary
But I’m not so sure that’s entirely accurate. “Playful?” Definitely. “Fanciful?” Possibly. But “fantastic?” That depends. “Whimsy” sounds superfluous, so I call it “expressive” instead.
Studying design from way back, I remembered how websites often included graphics that combined branding, content, and navigation. Pretty much every reference to web design in the ’90s — when I designed my first website — talks about Warner Brothers’ Space Jam from 1996.
Warner Brothers’ Space Jam (1996)So, I’m not going to do that.
Brands like Nintendo used their home pages to direct people to their content while making branded visual statements. Cheestrings combined graphics with navigation, making me wonder why we don’t see designs like this today. Goosebumps typified this approach, combining cartoon illustrations with brightly colored shapes into a functional and visually rich banner, proving that being useful doesn’t mean being boring.
Left to right: Nintendo, Cheestrings, Goosebumps.In the ’90s, when I developed graphics for websites like these, I either sliced them up and put their parts in tables or used mostly forgotten image maps.
A brief overview of properties and valuesLet’s run through a quick refresher. Image maps date all the way back to HTML 3.2, where, first, server-side maps and then client-side maps defined clickable regions over an image using map and area elements. They were popular for graphics, maps, and navigation, but their use declined with the rise of CSS, SVG, and JavaScript.
<map> adds clickable areas to a bitmap or vector image.
<map name="projects"> ... </map>That <map> is linked to an image using the usemap attribute:
<img usemap="#projects" ...>Those elements can have separate href and alt attributes and can be enhanced with ARIA to improve accessibility:
<map name="projects"> <area href="" alt="" … /> ... </map>The shape attribute specifies an area’s shape. It can be a primitive circle or rect or a polygon defined by a set of absolute x and y coordinates:
<area shape="circle" coords="..." ... /> <area shape="rect" coords="..." ... /> <area shape="poly" coords="..." ... />Despite their age, image maps still offer plenty of benefits. They’re lightweight and need (almost) no JavaScript. More on that in just a minute. They’re accessible and semantic when used with alt, ARIA, and title attributes. Despite being from a different era, even modern mobile browsers support image maps.
Design by Andy Clarke, Stuff & Nonsense. Mike Worth’s website will launch in April 2025, but you can see examples from this article on CodePen.My design for Mike Worth includes several graphic navigation elements, which made me wonder if image maps might still be an appropriate solution.
Image maps in actionMike wants his website to showcase his past work and the projects he’d like to do. To make this aspect of his design discoverable and fun, I created a map for people to explore by pressing on areas of the map to open modals. This map contains numbered circles, and pressing one pops up its modal.
My first thought was to embed anchors into the external map SVG:
<img src="projects.svg" alt="Projects"> <svg ...> ... <a href="..."> <circle cx="35" cy="35" r="35" fill="#941B2F"/> <path fill="#FFF" d="..."/> </a> </svg>This approach is problematic. Those anchors are only active when SVG is inline and don’t work with an <img> element. But image maps work perfectly, even though specifying their coordinates can be laborious. Fortunately, plenty of tools are available, which make defining coordinates less tedious. Upload an image, choose shape types, draw the shapes, and copy the markup:
<img src="projects.svg" usemap="#projects-map.svg"> <map name="projects-map.svg"> <area href="" alt="" coords="..." shape="circle"> <area href="" alt="" coords="..." shape="circle"> ... </map>Image maps work well when images are fixed sizes, but flexible images present a problem because map coordinates are absolute, not relative to an image’s dimensions. Making image maps responsive needs a little JavaScript to recalculate those coordinates when the image changes size:
function resizeMap() { const image = document.getElementById("projects"); const map = document.querySelector("map[name='projects-map']"); if (!image || !map || !image.naturalWidth) return; const scale = image.clientWidth / image.naturalWidth; map.querySelectorAll("area").forEach(area => { if (!area.dataset.originalCoords) { area.dataset.originalCoords = area.getAttribute("coords"); } const scaledCoords = area.dataset.originalCoords .split(",") .map(coord => Math.round(coord * scale)) .join(","); area.setAttribute("coords", scaledCoords); }); } ["load", "resize"].forEach(event => window.addEventListener(event, resizeMap) );I still wasn’t happy with this implementation as I wanted someone to be able to press on much larger map areas, not just the numbered circles.
Every <path> has coordinates which define how it’s drawn, and they’re relative to the SVG viewBox:
<svg width="1024" height="1024"> <path fill="#BFBFBF" d="…"/> </svg>On the other hand, a map’s <area> coordinates are absolute to the top-left of an image, so <path> values need to be converted. Fortunately, Raphael Monnerat has written PathToPoints, a tool which does precisely that. Upload an SVG, choose the point frequency, copy the coordinates for each path, and add them to a map area’s coords:
<map> <area href="" shape="poly" coords="..."> <area href="" shape="poly" coords="..."> <area href="" shape="poly" coords="..."> ... </map> More issues with image mapsImage maps are hard-coded and time-consuming to create without tools. Even with tools for generating image maps, converting paths to points, and then recalculating them using JavaScript, they could be challenging to maintain at scale.
<area> elements aren’t visible, and except for a change in the cursor, they provide no visual feedback when someone hovers over or presses a link. Plus, there’s no easy way to add animations or interaction effects.
But the deal-breaker for me was that an image map’s pixel-based values are unresponsive by default. So, what might be an alternative solution for implementing my map using CSS, HTML, and SVG?
Anchors positioned absolutely over my map wouldn’t solve the pixel-based positioning problem or give me the irregular-shaped clickable areas I wanted. Anchors within an external SVG wouldn’t work either.
But the solution was staring me in the face. I realized I needed to:
- Create a new SVG path for each clickable area.
- Make those paths invisible.
- Wrap each path inside an anchor.
- Place the anchors below other elements at the end of my SVG source.
- Replace that external file with inline SVG.
I created a set of six much larger paths which define the clickable areas, each with its own fill to match its numbered circle. I placed each anchor at the end of my SVG source:
<svg … viewBox="0 0 1024 1024"> <!-- Visible content --> <g>...</g> <!-- Clickable areas -->` <g id="links">` <a href="..."><path fill="#B48F4C" d="..."/></a>` <a href="..."><path fill="#6FA676" d="..."/></a>` <a href="..."><path fill="#30201D" d="..."/></a>` ... </g> </svg>Then, I reduced those anchors’ opacity to 0 and added a short transition to their full-opacity hover state:
#links a { opacity: 0; transition: all .25s ease-in-out; } #links a:hover { opacity: 1; }While using an image map’s <area> sadly provides no visual feedback, embedded anchors and their content can respond to someone’s action, hint at what’s to come, and add detail and depth to a design.
I might add gloss to those numbered circles to be consistent with the branding I’ve designed for Mike. Or, I could include images, titles, or other content to preview the pop-up modals:
<g id="links"> <a href="…"> <path fill="#B48F4C" d="..."/> <image href="..." ... /> </a> </g>Try it for yourself:
CodePen Embed Fallback Expressive design, modern techniquesDesigning Mike Worth’s website gave me a chance to blend expressive design with modern development techniques, and revisiting image maps reminded me just how important a tool image maps were during the period Mike loves so much.
Ultimately, image maps weren’t the right tool for Mike’s website. But exploring them helped me understand what I really needed: a way to recapture the expressiveness and personality of ’90s website design using modern techniques that are accessible, lightweight, responsive, and semantic. That’s what design’s about: choosing the right tool for a job, even if that sometimes means looking back to move forward.
Biography: Andy ClarkeOften referred to as one of the pioneers of web design, Andy Clarke has been instrumental in pushing the boundaries of web design and is known for his creative and visually stunning designs. His work has inspired countless designers to explore the full potential of product and website design.
Andy’s written several industry-leading books, including Transcending CSS, Hardboiled Web Design, and Art Direction for the Web. He’s also worked with businesses of all sizes and industries to achieve their goals through design.
Visit Andy’s studio, Stuff & Nonsense, and check out his Contract Killer, the popular web design contract template trusted by thousands of web designers and developers.
Revisiting Image Maps originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Open Up With Brad Frost, Episode 2
Brad Frost is running this new little podcast called Open Up. Folks write in with questions about the “other” side of web design and front-end development — not so much about tools and best practices as it is about the things that surround the work we do, like what happens if you get laid off, or AI takes your job, or something along those lines. You know, the human side of what we do in web design and development.
Well, it just so happens that I’m co-hosting the show. In other words, I get to sprinkle in a little advice on top of the wonderful insights that Brad expertly doles out to audience questions.
Our second episode just published, and I thought I’d share it. We’re finding our sea legs with this whole thing and figuring things out as we go. We’ve opened things up (get it?!) to a live audience and even pulled in one of Brad’s friends at the end to talk about the changing nature of working on a team and what it looks like to collaborate in a remote-first world.
https://www.youtube.com/watch?v=bquVF5CibawOpen Up With Brad Frost, Episode 2 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
