Web Standards

The State of CSS 2025 Survey is out!

Css Tricks - Thu, 06/05/2025 - 1:13am

The State of CSS 2025 Survey dropped a few days ago, and besides waiting for the results, it’s exciting to see a lot of the new things shipped to CSS over the past year reflected in the questions. To be specific, the next survey covers the following features:

Again, a lot!

However, I think the most important questions (regarding CSS) are asked at the end of each section. I am talking about the “What are your top CSS pain points related to ______?” questions. These sections are optional, but help user agents and the CSS Working Group know what they should focus on next.

By nature of comments, those respondents with strong opinions are most likely to fill them in, skewing data towards issues that maybe the majority doesn’t have. So, even if you don’t have a hard-set view on a CSS pain point, I encourage you to fill them — even with your mild annoyances.

The State of CSS 2025 Survey is out! originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Getting Creative With HTML Dialog

Css Tricks - Tue, 06/03/2025 - 4:39am

Like ’em or loath ’em, whether you’re showing an alert, a message, or a newsletter signup, dialogue boxes draw attention to a particular piece of content without sending someone to a different page. In the past, dialogues relied on a mix of divisions, ARIA, and JavaScript. But the HTML dialog element has made them more accessible and style-able in countless ways.

So, how can you take dialogue box design beyond the generic look of frameworks and templates? How can you style them to reflect a brand’s visual identity and help to tell its stories? Here’s how I do it in CSS using ::backdrop, backdrop-filter, and animations.

Design by Andy Clarke, Stuff & Nonsense. Mike Worth’s website will launch in June 2025, but you can see examples from this article on CodePen.

I mentioned before that Emmy-award-winning game composer Mike Worth hired me to create a highly graphical design. Mike loves ’90s animation, and he challenged me to find ways to incorporate its retro style without making a pastiche. However, I also needed to achieve that retro feel while maintaining accessibility, performance, responsiveness, and semantics.

A brief overview of dialog and ::backdrop

Let’s run through a quick refresher.

Note: While I mostly refer to “dialogue boxes” throughout, the HTML element is spelt dialog.

dialog is an HTML element designed for implementing modal and non-modal dialogue boxes in products and website interfaces. It comes with built-in functionality, including closing a box using the keyboard Esc key, focus trapping to keep it inside the box, show and hide methods, and a ::backdrop pseudo-element for styling a box’s overlay.

The HTML markup is just what you might expect:

<dialog> <h2>Keep me informed</h2> <!-- ... --> <button>Close</button> </dialog>

This type of dialogue box is hidden by default, but adding the open attribute makes it visible when the page loads:

<dialog open> <h2>Keep me informed</h2> <!-- ... --> <button>Close</button> </dialog>

I can’t imagine too many applications for non-modals which are open by default, so ordinarily I need a button which opens a dialogue box:

<dialog> <!-- ... --> </dialog> <button>Keep me informed</button>

Plus a little bit of JavaScript, which opens the modal:

const dialog = document.querySelector("dialog"); const showButton = document.querySelector("dialog + button"); showButton.addEventListener("click", () => { dialog.showModal(); });

Closing a dialogue box also requires JavaScript:

const closeButton = document.querySelector("dialog button"); closeButton.addEventListener("click", () => { dialog.close(); });

Unless the box contains a form using method="dialog", which allows it to close automatically on submit without JavaScript:

<dialog> <form method="dialog"> <button>Submit</button> </form> </dialog>

The dialog element was developed to be accessible out of the box. It traps focus, supports the Esc key, and behaves like a proper modal. But to help screen readers announce dialogue boxes properly, you’ll want to add an aria-labelledby attribute. This tells assistive technology where to find the dialogue box’s title so it can be read aloud when the modal opens.

<dialog aria-labelledby="dialog-title"> <h2 id="dialog-title">Keep me informed</h2> <!-- ... --> </dialog>

Most tutorials I’ve seen include very little styling for dialog and ::backdrop, which might explain why so many dialogue boxes have little more than border radii and a box-shadow applied.

Out-of-the-box dialogue designs

I believe that every element in a design — no matter how small or infrequently seen — is an opportunity to present a brand and tell a story about its products or services. I know there are moments during someone’s journey through a design where paying special attention to design can make their experience more memorable.

Dialogue boxes are just one of those moments, and Mike Worth’s design offers plenty of opportunities to reflect his brand or connect directly to someone’s place in Mike’s story. That might be by styling a newsletter sign-up dialogue to match the scrolls in his news section.

Mike Worth concept design, designed by Andy Clarke, Stuff & Nonsense.

Or making the form modal on his error pages look like a comic-book speech balloon.

Mike Worth concept design, designed by Andy Clarke, Stuff & Nonsense. dialog in action

Mike’s drop-down navigation menu looks like an ancient stone tablet.

Mike Worth, designed by Andy Clarke, Stuff & Nonsense.

I wanted to extend this look to his dialogue boxes with a three-dimensional tablet and a jungle leaf-filled backdrop.

Mike Worth, designed by Andy Clarke, Stuff & Nonsense.

This dialog contains a newsletter sign-up form with an email input and a submit button:

<dialog> <h2>Keep me informed</h2> <form> <label for="email" data-visibility="hidden">Email address</label> <input type="email" id="email" required> <button>Submit</button> </form> <button>x</button> </dialog>

I started by applying dimensions to the dialog and adding the SVG stone tablet background image:

dialog { width: 420px; height: 480px; background-color: transparent; background-image: url("dialog.svg"); background-repeat: no-repeat; background-size: contain; }

Then, I added the leafy green background image to the dialogue box’s generated backdrop using the ::backdrop pseudo element selector:

dialog::backdrop { background-image: url("backdrop.svg"); background-size: cover; } Mike Worth, designed by Andy Clarke, Stuff & Nonsense.

I needed to make it clear to anyone filling in Mike’s form that their email address is in a valid format. So I combined :has and :valid CSS pseudo-class selectors to change the color of the submit button from grey to green:

dialog:has(input:valid) button { background-color: #7e8943; color: #fff; }

I also wanted this interaction to reflect Mike’s fun personality. So, I also changed the dialog background image and applied a rubberband animation to the box when someone inputs a valid email address:

dialog:has(input:valid) { background-image: url("dialog-valid.svg"); animation: rubberBand 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both; } @keyframes rubberBand { from { transform: scale3d(1, 1, 1); } 30% { transform: scale3d(1.25, 0.75, 1); } 40% { transform: scale3d(0.75, 1.25, 1); } 50% { transform: scale3d(1.15, 0.85, 1); } 65% { transform: scale3d(0.95, 1.05, 1); } 75% { transform: scale3d(1.05, 0.95, 1); } to { transform: scale3d(1, 1, 1); } }

Tip: Daniel Eden’s Animate.css library is a fabulous source of “Just-add-water CSS animations” like the rubberband I used for this dialogue box.

Changing how an element looks when it contains a valid input is a fabulous way to add interactions that are, at the same time, fun and valuable for the user.

Mike Worth, designed by Andy Clarke, Stuff & Nonsense.

That combination of :has and :valid selectors can even be extended to the ::backdrop pseudo-class, to change the backdrop’s background image:

dialog:has(input:valid)::backdrop { background-image: url("backdrop-valid.svg"); }

Try it for yourself:

CodePen Embed Fallback Conclusion

We often think of dialogue boxes as functional elements, as necessary interruptions, but nothing more. But when you treat them as opportunities for expression, even the smallest parts of a design can help shape a product or website’s personality.

The HTML dialog element, with its built-in behaviours and styling potential, opens up opportunities for branding and creative storytelling. There’s no reason a dialogue box can’t be as distinctive as the rest of your design.

Andy Clarke

Often 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.

Getting Creative With HTML Dialog originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

The Receding Role of AI Chat

LukeW - Sun, 06/01/2025 - 2:00pm

While chat interfaces to AI models aren't going away anytime soon, the increasing capabilities of AI agents are making the concept of chatting back and forth with an AI model to get things done feel archaic.

Let me first clarify that I don't mean open-ended text fields where people declare their intent are going away. As I wrote recently there will be even more broad input affordances in software whether for text, image, audio, video, or more. When I say chat AIs, I mean applications whose primary mode of getting things done is through a back and forth messaging conversation with an AI model: you type something, the model responds, you type something... and on it goes until you get the output you need.

Anyone that's interacted with an application like this knows that the AI model's responses quickly get lost in conversation threads and producing something from a set of chat replies can be painful. This kind of interface isn't optimal for tasks like authoring a document, writing code, or creating slides. To account for this some applications now include a canvas or artifact area where the output of the AI model's work can go.

In these layouts, the chat interface usually goes from being a single-pane layout to a split-pane layout. Roughly half the UI for input in the form of chat and half of it for output in the form of a canvas or artifact viewer. In these kinds of applications, we already begin to see the prominence of chat receding as people move between providing input and reviewing, editing, or acting on output.

In this model, however, the onus is still on the user to chat back and forth with a model until it produces their desired output in the artifact or canvas pane. Agents (AI models to make use of tools) change this dynamic. People state their objectives and the AI model(s) plans which tools to use and how to accomplish their task.

Instead of each step being a back and forth chat between a person and an AI model, the vast majority, if not all, of the steps are coordinated by the model(s) itself. This again reduces the role of chat. The model(s) takes care of the back and forth and in most cases simply lets people know when its done so they can review and make use of its output.

When agents can use multiple tools, call other agents and run in the background, a person's role moves to kicking things off, clarifying things when needed, and making use of the final output. There's a lot less chatting back and forth. As such, the prominence of the chat interface can recede even further. It's there if you want to check the steps an AI took to accomplish your task. But until then it's out of your way so you can focus on the output.

You can see this UI transition in the AI workspace, Bench. The first version was focused on back and forth instructions with models to get things done: a single-pane AI chat UI. Then a split-paned interface put more emphasis on the results of these instructions with half the screen devoted to an output pane. Today Bench runs and coordinates agents in the background. So the primary interaction is kicking off tasks and reviewing results when they're ready.

In this UI, the chat interface is not only reduced to less than a fourth of the screen but also collapsed by default hiding the model's back and forth conversations with itself unless people want to dig into it.

When working with AI models this way, the process of chatting back and forth to create things within in messaging UI feels dated. AI that takes your instructions, figures out how to get things done using tools, multiple models, changeable plans, and just tells you when it's finished feels a lot more like "the future". Of course I put future in quotes because at the rate AI moves these days the future will be here way sooner than any of us think. So... more UI changes to come!

What We Know (So Far) About CSS Reading Order

Css Tricks - Tue, 05/27/2025 - 3:02am

The reading-flow and reading-order proposed CSS properties are designed to specify the source order of HTML elements in the DOM tree, or in simpler terms, how accessibility tools deduce the order of elements. You’d use them to make the focus order of focusable elements match the visual order, as outlined in the Web Content Accessibility Guidelines (WCAG 2.2).

To get a better idea, let’s just dive in!

(Oh, and make sure that you’re using Chrome 137 or higher.)

reading-flow

reading-flow determines the source order of HTML elements in a flex, grid, or block layout. Again, this is basically to help accessibility tools provide the correct focus order to users.

The default value is normal (so, reading-flow: normal). Other valid values include:

  • flex-visual
  • flex-flow
  • grid-rows
  • grid-columns
  • grid-order
  • source-order

Let’s start with the flex-visual value. Imagine a flex row with five links. Assuming that the reading direction is left-to-right (by the way, you can change the reading direction with the direction CSS property), that’d look something like this:

CodePen Embed Fallback

Now, if we apply flex-direction: row-reverse, the links are displayed 5-4-3-2-1. The problem though is that the focus order still starts from 1 (tab through them!), which is visually wrong for somebody that reads left-to-right.

CodePen Embed Fallback

But if we also apply reading-flow: flex-visual, the focus order also becomes 5-4-3-2-1, matching the visual order (which is an accessibility requirement!):

<div> <a>1</a> <a>2</a> <a>3</a> <a>4</a> <a>5</a> </div> div { display: flex; flex-direction: row-reverse; reading-flow: flex-visual; } CodePen Embed Fallback

To apply the default flex behavior, reading-flow: flex-flow is what you’re looking for. This is very akin to reading-flow: normal, except that the container remains a reading flow container, which is needed for reading-order (we’ll dive into this in a bit).

For now, let’s take a look at the grid-y values. In the grid below, the grid items are all jumbled up, and so the focus order is all over the place.

CodePen Embed Fallback

We can fix this in two ways. One way is that reading-flow: grid-rows will, as you’d expect, establish a row-by-row focus order:

<div> <a>1</a> <a>2</a> <a>3</a> <a>4</a> <a>5</a> <a>6</a> <a>7</a> <a>8</a> <a>9</a> <a>10</a> <a>11</a> <a>12</a> </div> div { display: grid; grid-template-columns: repeat(4, 1fr); grid-auto-rows: 100px; reading-flow: grid-rows; a:nth-child(2) { grid-row: 2 / 4; grid-column: 3; } a:nth-child(5) { grid-row: 1 / 3; grid-column: 1 / 3; } } CodePen Embed Fallback

Or, reading-flow: grid-columns will establish a column-by-column focus order:

CodePen Embed Fallback

reading-flow: grid-order will give us the default grid behavior (i.e., the jumbled up version). This is also very akin to reading-flow: normal (except that, again, the container remains a reading flow container, which is needed for reading-order).

There’s also reading-flow: source-order, which is for flex, grid, and block containers. It basically turns containers into reading flow containers, enabling us to use reading-order. To be frank, unless I’m missing something, this appears to make the flex-flow and grid-order values redundant?

reading-order

reading-order sort of does the same thing as reading-flow. The difference is that reading-order is for specific flex or grid items, or even elements in a simple block container. It works the same way as the order property, although I suppose we could also compare it to tabindex.

Note: To use reading-order, the container must have the reading-flow property set to anything other than normal.

I’ll demonstrate both reading-order and order at the same time. In the example below, we have another flex container where each flex item has the order property set to a different random number, making the order of the flex items random. Now, we’ve already established that we can use reading-flow to determine focus order regardless of visual order, but in the example below we’re using reading-order instead (in the exact same way as order):

div { display: flex; reading-flow: source-order; /* Anything but normal */ /* Features at the end because of the higher values */ a:nth-child(1) { /* Visual order */ order: 567; /* Focus order */ reading-order: 567; } a:nth-child(2) { order: 456; reading-order: 456; } a:nth-child(3) { order: 345; reading-order: 345; } a:nth-child(4) { order: 234; reading-order: 234; } /* Features at the beginning because of the lower values */ a:nth-child(5) { order: -123; reading-order: -123; } } CodePen Embed Fallback

Yes, those are some rather odd numbers. I’ve done this to illustrate how the numbers don’t represent the position (e.g., order: 3 or reading-order: 3 doesn’t make it third in the order). Instead, elements with lower numbers are more towards the beginning of the order and elements with higher numbers are more towards the end. The default value is 0. Elements with the same value will be ordered by source order.

In practical terms? Consider the following example:

div { display: flex; reading-flow: source-order; a:nth-child(1) { order: 1; reading-order: 1; } a:nth-child(5) { order: -1; reading-order: -1; } } CodePen Embed Fallback

Of the five flex items, the first one is the one with order: -1 because it has the lowest order value. The last one is the one with order: 1 because it has the highest order value. The ones with no declaration default to having order: 0 and are thus ordered in source order, but otherwise fit in-between the order: -1 and order: 1 flex items. And it’s the same concept for reading-order, which in the example above mirrors order.

However, when reversing the direction of flex items, keep in mind that order and reading-order work a little differently. For example, reading-order: -1 would, as expected, and pull a flex item to the beginning of the focus order. Meanwhile, order: -1 would pull it to the end of the visual order because the visual order is reversed (so we’d need to use order: 1 instead, even if that doesn’t seem right!):

div { display: flex; flex-direction: row-reverse; reading-flow: source-order; a:nth-child(5) { /* Because of row-reverse, this actually makes it first */ order: 1; /* However, this behavior doesn’t apply to reading-order */ reading-order: -1; } } CodePen Embed Fallback

reading-order overrides reading-flow. If we, for example, apply reading-flow: flex-visual, reading-flow: grid-rows, or reading-flow: grid-columns (basically, any declaration that does in fact change the reading flow), reading-order overrides it. We could say that reading-order is applied after reading-flow.

What if I don’t want to use flexbox or grid layout?

Well, that obviously rules out all of the flex-y and grid-y reading-flow values; however, you can still set reading-flow: source-order on a block element and then manipulate the focus order with reading-order (as we did above).

How does this relate to the tabindex HTML attribute?

They’re not equivalent. Negative tabindex values make targets unfocusable and values other than 0 and -1 aren’t recommended, whereas a reading-order declaration can use any number as it’s only contextual to the reading flow container that contains it.

For the sake of being complete though, I did test reading-order and tabindex together and reading-order appeared to override tabindex.

Going forward, I’d only use tabindex (specifically, tabindex="-1") to prevent certain targets from being focusable (the disabled attribute will be more appropriate for some targets though), and then reading-order for everything else.

Closing thoughts

Being able to define reading order is useful, or at least it means that the order property can finally be used as intended. Up until now (or rather when all web browsers support reading-flow and reading-order, because they only work in Chrome 137+ at the moment), order hasn’t been useful because we haven’t been able to make the focus order match the visual order.

What We Know (So Far) About CSS Reading Order originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Ask LukeW: Generation Model Testing

LukeW - Sat, 05/24/2025 - 2:00pm

The last two weeks featured a flurry of new AI model announcements. Keeping up with these changes can be hard without some kind of personal benchmark. For me, that's been my personal AI feature, Ask LukeW, which allows me to both quickly try and put new models into production.

To start... what were all these announcements? On May 14th, OpenAI released three new models in their GPT-4.1 series. On May 20th at I/O, Google updated Gemini 2.5 Pro. On May 22nd, Anthropic launched Claude Opus 4 and Claude Sonnet 4. So clearly high-end model releases aren't slowing down anytime soon.

Many AI-powered applications develop and use their own benchmarks to evaluate new models when they become available. But there's still nothing quite like trying an AI model yourself in a domain or problem space you know very well to gauge its strengths and weaknesses.

To do this more easily, I added the ability to quickly test new models on the Ask LukeW feature of this site. Because Ask LukeW works with the thousands of articles I've written and hundreds of presentations I've given, it's a really effective way for me to see what's changed. Essentially, I know what good looks like because I know what the answers should be.

The Ask LukeW system retrieves as much relevant content as possible before asking a large language model (LLM) to generate an answer to someone's question (as seen in the system diagram). As a result, the LLM can have lots of content to make sense of when things get to the generation part of the pipeline.

Previously this resulted in a lot of "kitchen sink" style bullet point answers as frontier models mostly leaned toward including as much information as possible. These kinds of replies ended up using lots of words without clearly getting to the point. After some testing, I found Anthropic's Claude Opus 4 is much better at putting together responses that feel like they understood the essence of a question. You can see the difference in the before and after examples in this article. The responses to questions with lots of content to synthesize feel more coherent and concise.

It's worth noting I'm only using Opus 4 is for the generation part of the Ask LukeW pipeline which uses AI models to not only generate but also transform, clean, embed, retrieve, and rank content. So there's many other parts of the pipeline where testing new models matters but in the final generation step at the end, Opus 4 wins. For now...

Better CSS Shapes Using shape() — Part 1: Lines and Arcs

Css Tricks - Fri, 05/23/2025 - 3:02am

Creating CSS Shapes is a classic and one of my favorite exercise. Indeed, I have one of the biggest collections of CSS Shapes from where you can easily copy the code of any shape. I also wrote an extensive guide on how to create them: The Modern Guide For Making CSS Shapes.

Even if I have detailed most of the modern techniques and tricks, CSS keeps evolving, and new stuff always emerges to simplify our developer life. Recently, clip-path was upgraded to have a new shape() value. A real game changer!

Better CSS Shapes Using shape()
  1. Lines and Arcs (you are here!)
  2. More on Arcs
  3. Curves

Before we jump in, it’s worth calling out that the shape() function is currently only supported in Chrome 137+ and Safari 18.4+ as I’m writing this in May 2025.

What is shape()?

Let me quote the description from the official specification:

While the path() function allows reuse of the SVG path syntax to define more arbitrary shapes than allowed by more specialized shape functions, it requires writing a path as a single string (which is not compatible with, for example, building a path piecemeal with var()), and inherits a number of limitations from SVG, such as implicitly only allowing the px unit.

The shape() function uses a set of commands roughly equivalent to the ones used by path(), but does so with more standard CSS syntax, and allows the full range of CSS functionality, such as additional units and math functions.

In other words, we have the SVG features in the CSS side that we can combine with existing features such as var(), calc(), different units, etc. SVG is already good at drawing complex shapes, so imagine what is possible with something more powerful.

If you keep reading the spec, you will find:

In that sense, shape() is a superset of path(). A path() can be easily converted to a shape(), but to convert a shape() back to a path() or to SVG requires information about the CSS environment.

And guess what? I already created an online converter from SVG to CSS. Save this tool because it will be very handy. If you are already good at creating SVG shapes or you have existing codes, no need to reinvent the wheel. You paste your code in the generator, and you get the CSS code that you can easily tweak later.

Let’s try with the CSS-Tricks logo. Here is the SVG I picked from the website:

<svg width="35px" height="35px" viewBox="0 0 362.62 388.52" > <path d="M156.58,239l-88.3,64.75c-10.59,7.06-18.84,11.77-29.43,11.77-21.19,0-38.85-18.84-38.85-40C0,257.83,14.13,244.88,27.08,239l103.6-44.74L27.08,148.34C13,142.46,0,129.51,0,111.85,0,90.66,18.84,73,40,73c10.6,0,17.66,3.53,28.25,11.77l88.3,64.75L144.81,44.74C141.28,20,157.76,0,181.31,0s40,18.84,36.5,43.56L206,149.52l88.3-64.75C304.93,76.53,313.17,73,323.77,73a39.2,39.2,0,0,1,38.85,38.85c0,18.84-12.95,30.61-27.08,36.5L231.93,194.26,335.54,239c14.13,5.88,27.08,18.83,27.08,37.67,0,21.19-18.84,38.85-40,38.85-9.42,0-17.66-4.71-28.26-11.77L206,239l11.77,104.78c3.53,24.72-12.95,44.74-36.5,44.74s-40-18.84-36.5-43.56Z"></path> </svg>

You take the value inside the d attribute, paste it in the converter, and boom! You have the following CSS:

.shape { aspect-ratio: 0.933; clip-path: shape(from 43.18% 61.52%,line by -24.35% 16.67%,curve by -8.12% 3.03% with -2.92% 1.82%/-5.2% 3.03%,curve by -10.71% -10.3% with -5.84% 0%/-10.71% -4.85%,curve to 7.47% 61.52% with 0% 66.36%/3.9% 63.03%,line by 28.57% -11.52%,line to 7.47% 38.18%,curve to 0% 28.79% with 3.59% 36.67%/0% 33.33%,curve to 11.03% 18.79% with 0% 23.33%/5.2% 18.79%,curve by 7.79% 3.03% with 2.92% 0%/4.87% 0.91%,line by 24.35% 16.67%,line to 39.93% 11.52%,curve to 50% 0% with 38.96% 5.15%/43.51% 0%,smooth by 10.07% 11.21% with 11.03% 4.85%,line to 56.81% 38.48%,line by 24.35% -16.67%,curve to 89.29% 18.79% with 84.09% 19.7%/86.36% 18.79%,arc by 10.71% 10% of 10.81% 10.09% small cw,curve by -7.47% 9.39% with 0% 4.85%/-3.57% 7.88%,line to 63.96% 50%,line to 92.53% 61.52%,curve by 7.47% 9.7% with 3.9% 1.51%/7.47% 4.85%,curve by -11.03% 10% with 0% 5.45%/-5.2% 10%,curve by -7.79% -3.03% with -2.6% 0%/-4.87% -1.21%,line to 56.81% 61.52%,line by 3.25% 26.97%,curve by -10.07% 11.52% with 0.97% 6.36%/-3.57% 11.52%,smooth by -10.07% -11.21% with -11.03% -4.85%,close); }

Note that you don’t need to provide any viewBox data. The converter will automatically find the smallest rectangle for the shape and will calculate the coordinates of the points accordingly. No more viewBox headaches and no need to fight with overflow or extra spacing!

CodePen Embed Fallback

Here is another example where I am applying the shape to an image element. I am keeping the original SVG so you can compare both shapes.

CodePen Embed Fallback When to use shape()

I would be tempted to say “all the time” but in reality, not. In my guide, I distinguish between two types of shapes: The ones with only straight lines and the ones with curves. Each type can either have repetition or not. In the end, we have four categories of shapes.

If we don’t have curves and we don’t have repetition (the easiest case), then clip-path: polygon() should do the job. If we have a repetition (with or without curves), then mask is the way to go. With mask, we can rely on gradients that can have a specific size and repeat, but with clip-path we don’t have such options.

If you have curves and don’t have a repetition, the new shape() is the best option. Previously, we had to rely on mask since clip-path is very limited, but that’s no longer the case. Of course, these are not universal rules, but my own way to identify which option is the most suitable. At the end of the day, it’s always a case-by-case basis as we may have other things to consider, such as the complexity of the code, the flexibility of the method, browser support, etc.

Let’s draw some shapes!

Enough talking, let’s move to the interesting part: drawing shapes. I will not write a tutorial to explain the “complex” syntax of shape(). It will be boring and not interesting. Instead, we will draw some common shapes and learn by practice!

Rectangle

Take the following polygon:

clip-path: polygon( 0 0, 100% 0, 100% 100%, 0 100% );

Technically, this will do nothing since it will draw a rectangle that already follows the element shape which is a rectangle, but it’s still the perfect starting point for us.

Now, let’s write it using shape().

clip-path: shape( from 0 0, line to 100% 0, line to 100% 100%, line to 0 100% );

The code should be self-explanatory and we already have two commands. The from command is always the first command and is used only once. It simply specifies the starting point. Then we have the line command that draws a line to the next point. Nothing complex so far.

We can still write it differently like below:

clip-path: shape( from 0 0, hline to 100%, vline to 100%, hline to 0 );

Between the points 0 0 and 100% 0, only the first value is changing which means we are drawing a horizontal line from 0 0 to 100% 0, hence the use of hline to 100% where you only need to specify the horizontal offset. It’s the same logic using vline where we draw a vertical line between 100% 0 and 100% 100%.

I won’t advise you to draw your shape using hline and vline because they can be tricky and are a bit difficult to read. Always start by using line and then if you want to optimize your code you can replace them with hline or vline when applicable.

We have our first shape and we know the commands to draw straight lines:

CodePen Embed Fallback Circular Cut-Out

Now, let’s try to add a circular cut-out at the top of our shape:

For this, we are going to rely on the arc command, so let’s understand how it works.

If we have two points, A and B, there are exactly two circles with a given radius that intersect with both points like shown in the figure. The intersection gives us four possible arcs we can draw between points A and B. Each arc is defined by a size and a direction.

There is also the particular case where the radius is equal to half the distance between A and B. In this case, only two arcs can be drawn and the direction will decide which one.

The syntax will look like this:

clip-path: shape( from Xa Ya, arc to Xb Yb of R [large or small] [cw or ccw] );

Let’s add this to our previous shape. No need to think about the values. Instead, let’s use random ones and see what happens:

clip-path: shape( from 0 0, arc to 40% 0 of 50px, line to 100% 0, line to 100% 100%, line to 0 100% ); CodePen Embed Fallback

Not bad, we can already see the arc between 0 0 and 40% 0. Notice how I didn’t define the size and direction of the arc. By default, the browser will use small and ccw.

Let’s explicitly define the size and direction to see the four different cases:

CodePen Embed Fallback

Hmm, it’s working for the first two blocks but not the other ones. Quite strange, right?

Actually, everything is working fine. The arcs are drawn outside the element area so nothing is visible. If you add some box-shadow, you can see them:

CodePen Embed Fallback

Arcs can be tricky due to the size and direction thing, so get ready to be confused. If that happens, remember that you have four different cases, and trying all of them will help you find which one you need.

Now let’s try to be accurate and draw half a circle with a specific radius placed at the center:

We can define the radius as a variable and use what we have learned so far:

.shape { --r: 50px; clip-path: shape( from 0 0, line to calc(50% - var(--r)) 0, arc to calc(50% + var(--r)) 0 of var(--r), line to 100% 0, line to 100% 100%, line to 0 100% ); } CodePen Embed Fallback

It’s working fine, but the code can still be optimized. We can replace all the line commands with hline and vline like below:

.shape { --r: 50px; clip-path: shape(from 0 0, hline to calc(50% - var(--r)), arc to calc(50% + var(--r)) 0 of var(--r), hline to 100%, vline to 100%, hline to 0 ); }

We can also replace the radius with 1%:

.shape { --r: 50px; clip-path: shape(from 0 0, hline to calc(50% - var(--r)), arc to calc(50% + var(--r)) 0 of 1%, hline to 100%, vline to 100%, hline to 0 ); }

When you define a small radius (smaller than half the distance between both points), no circle can meet the condition we explained earlier (an intersection with both points), so we cannot draw an arc. This case falls within an error handling where the browser will scale the radius until we can have a circle that meets the condition. Instead of considering this case as invalid, the browser will fix “our mistake” and draw an arc.

This error handling is pretty cool as it allows us to simplify our shape() function. Instead of specifying the exact radius, I simply put a small value and the browser will do the job for me. This trick only works when the arc we want to draw is half a circle. Don’t try to apply it with any arc command because it won’t always work.

Another optimization is to update the following:

arc to calc(50% + var(--r)) 0 of 1%,

…with this:

arc by calc(2 * var(--r)) 0 of 1%,

Almost all the commands can either use a to directive or a by directive. The first one defines absolute coordinates like the one we use with polygon(). It’s the exact position of the point we are moving to. The second defines relative coordinates which means we need to consider the previous point to identify the coordinates of the next point.

In our case, we are telling the arc to consider the previous point (50% - R) 0 and move by 2*R 0, so the final point will be (50% - R + 2R) (0 + 0), which is the same as (50% + R) 0.

.shape { --r: 50px; clip-path: shape(from 0 0, hline to calc(50% - var(--r)), arc by calc(2 * var(--r)) 0 of 1px, hline to 100%, vline to 100%, hline to 0 ); }

This last optimization is great because if we want to move the cutout from the center, we only need to update one value: the 50%.

.shape { --r: 50px; --p: 40%; clip-path: shape( from 0 0, hline to calc(var(--p) - var(--r)), arc by calc(2 * var(--r)) 0 of 1px, hline to 100%, vline to 100%, hline to 0 ); } CodePen Embed Fallback

How would you adjust the above to have the cut-out at the bottom, left, or right? That’s your first homework assignment! Try to do it before moving to the next part.

I will give my implementation so that you can compare with yours, but don’t cheat! If you can do this without referring to my work, you will be able to do more complex shapes more easily.

CodePen Embed Fallback Rounded Tab

Enough cut-out, let’s try to create a rounded tab:

Can you see the puzzle of this one? Similar to the previous shape, it’s a bunch of arc and line commands. Here is the code:

.shape { --r: 26px; clip-path: shape( /* left part */ from 0 100%, arc by var(--r) calc(-1 * var(--r)) of var(--r), vline to var(--r), arc by var(--r) calc(-1 * var(--r)) of var(--r) cw, /* right part */ hline to calc(100% - 2 * var(--r)), arc by var(--r) var(--r) of var(--r) cw, vline to calc(100% - var(--r)), arc by var(--r) var(--r) of var(--r) ); }

It looks a bit scary, but if you follow it command by command, it becomes a lot clearer to see what’s happening. Here is a figure to help you visualize the left part of it.

All the arc commands are using the by directive because, in all the cases, I always need to move by an offset equal to R, meaning I don’t have to calculate the coordinates of the points. And don’t try to replace the radius by 1% because it won’t work since we are drawing a quarter of a circle rather than half of a circle.

CodePen Embed Fallback

From this, we can easily achieve the left and right variations:

CodePen Embed Fallback

Notice how I am only using two arc commands instead of three. One rounded corner can be done with a classic border radius, so this can help us simplify the shape.

Inverted Radius

One last shape, the classic inner curve at the corner also called an inverted radius. How many arc commands do we need for this one? Check the figure below and think about it.

If your answer is six, you have chosen the difficult way to do it. It’s logical to think about six arcs since we have six curves, but three of them can be done with a simple border radius, so only three arcs are needed. Always take the time to analyze the shape you are creating. Sometimes, basic CSS properties can help with creating the shape.

What are you waiting for? This is your next homework and I won’t help you with a figure this time. You have all that you need to easily create it. If you are struggling, give the article another read and try to study the previous shapes more in depth.

Here is my implementation of the four variations:

CodePen Embed Fallback Conclusion

That’s all for this first part. You should have a good overview of the shape() function. We focused on the line and arc commands which are enough to create most of the common shapes.

Don’t forget to bookmark the SVG to CSS converter and keep an eye on my CSS Shape collection where you can find the code of all the shapes I create. And here is a last shape to end this article.

CodePen Embed Fallback Better CSS Shapes Using shape()
  1. Lines and Arcs (you are here!)
  2. More on Arcs
  3. Curves

Better CSS Shapes Using shape() — Part 1: Lines and Arcs originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

You can style alt text like any other text

Css Tricks - Thu, 05/22/2025 - 4:43am

Clever, clever that Andy Bell. He shares a technique for displaying image alt text when the image fails to load. Well, more precisely, it’s a technique to apply styles to the alt when the image doesn’t load, offering a nice UI fallback for what would otherwise be a busted-looking error.

The recipe? First, make sure you’re using alt in the HTML. Then, a little JavaScript snippet that detects when an image fails to load:

const images = document.querySelectorAll("img"); if (images) { images.forEach((image) => { image.onerror = () => { image.setAttribute("data-img-loading-error", ""); }; }); }

That slaps an attribute on the image — data-img-loading-error — that is selected in CSS:

img[data-img-loading-error] { --img-border-style: 0.25em solid color-mix(in srgb, currentColor, transparent 75%); --img-border-space: 1em; border-inline-start: var(--img-border-style); border-block-end: var(--img-border-style); padding-inline-start: var(--img-border-space); padding-block: var(--img-border-space); max-width: 42ch; margin-inline: auto; }

And what you get is a lovely little presentation of the alt that looks a bit like a blockquote and is is only displayed when needed.

CodePen Embed Fallback

Andy does note, however, that Safari does not render alt text if it goes beyond a single line, which &#x1f937;‍♂️.

You can style alt text like any other text originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

MCP: Model-Context-Protocol

LukeW - Wed, 05/21/2025 - 2:00pm

In his AI Speaker Series presentation at Sutter Hill Ventures, David Soria Parra of Anthropic, shared insights on the Model-Context-Protocol (MCP), an open protocol designed to standardize how AI applications interact with external data sources and tools. Here's my notes from his talk:

  • Models are only as good as the context provided to them, making it crucial to ensure they have access to relevant information for specific tasks
  • MCP standardizes how AI applications interact with external systems, similar to how the Language Server Protocol (LSP) standardized development tools
  • MCP is not a protocol between models and external systems, but between AI applications that use LLMs and external systems
  • Without MCP, AI development is fragmented with every application building custom implementations, custom prompts, and custom tool calls
  • MCP separates the concerns of providing data access from building applications
  • This separation allows application developers to focus on building better applications while data providers can focus on exposing their data effectively

How MCP Works
  • Two major components exist in an MCP system: client (implemented by the application using the LLM) and server (serves context to the client)
  • MCP servers offer: Tools (functions that perform actions), Resources (raw data content exposed by the server), Prompts (show how tools should be invoked)
  • Application developers can connect their apps to any MCP server in the ecosystem
  • API developers can expose their data to multiple AI applications by implementing an MCP server once
  • Allows different organizations within large companies to build components independently that work together through the protocol
Writing Good Tools for MCP
  • Tools should be simple and focused on specific tasks
  • Comprehensive descriptions help models understand when and how to use the tools
  • Error messages should be in natural language to facilitate better interactions
  • The goal is to create tools that are intuitive for both models and users
Future Directions for MCP
  • Remote MCP servers with proper authorization mechanisms
  • An official MCP registry to discover available servers and tools
  • Asynchronous execution for long-running tasks
  • Streaming data capabilities from servers to clients
  • Namespacing to organize tools and resources
  • Improved elicitation techniques for better interactions
  • There's a need for a structure to manage the protocol as it grows

SVG to CSS Shape Converter

Css Tricks - Wed, 05/21/2025 - 5:09am

Shape master Temani Afif has what might be the largest collection of CSS shapes on the planet with all the tools to generate them on the fly. There’s a mix of clever techniques he’s typically used to make those shapes, many of which he’s covered here at CSS-Tricks over the years.

Some of the more complex shapes were commonly clipped with the path() function. That makes a lot of sense because it literally accepts SVG path coordinates that you can draw in an app like Figma and export.

But Temani has gone all-in on the newly-released shape() function which recently rolled out in both Chromium browsers and Safari. That includes a brand-new generator that converts path() shapes in shape() commands instead.

So, if we had a shape that was originally created with an SVG path, like this:

.shape { clip-path: path( M199.6,18.9 c-4.3-8.9-12.5-16.4-22.3-17.8 c-11.9-1.7-23.1,5.4-32.2,13.2 c-9.1,7.8-17.8,16.8-29.3,20.3 c-20.5,6.2-41.7-7.4-63.1-7.5 c38.7,27,24.8,33,15.2,43.3 c-35.5,38.2-0.1,99.4,40.6,116.2 c32.8,13.6,72.1,5.9,100.9-15 c27.4-19.9,44.3-54.9,47.4-88.6 c0.2-2.7,0.4-5.3,0.5-7.9 c204.8,38,203.9,27.8,199.6,18.9 z ); }

…the generator will spit this out:

.shape { clip-path: shape( from 97.54% 10.91%, curve by -10.93% -10.76% with -2.11% -5.38%/-6.13% -9.91%, curve by -15.78% 7.98% with -5.83% -1.03%/-11.32% 3.26%, curve by -14.36% 12.27% with -4.46% 4.71%/-8.72% 10.15%, curve by -30.93% -4.53% with -10.05% 3.75%/-20.44% -4.47%, curve to 7.15% 25.66% with 18.67% 15.81%/11.86% 19.43%, curve by 19.9% 70.23% with -17.4% 23.09%/-0.05% 60.08%, curve by 49.46% -9.07% with 16.08% 8.22%/35.34% 3.57%, curve by 23.23% -53.55% with 13.43% -12.03%/21.71% -33.18%, curve by 0.25% -4.77% with 0.1% -1.63%/0.2% -3.2%, curve to 97.54% 10.91% with 100.09% 22.46%/99.64% 16.29%, close ); }

Pretty cool!

CodePen Embed Fallback

Honestly, I’m not sure how often I’ll need to convert path() to shape(). Seems like a stopgap sorta thing where the need for it dwindles over time as shape() is used more often — and it’s not like the existing path() function is broken or deprecated… it’s just different. But still, I’m using the generator a LOT as I try to wrap my head around shape() commands. Seeing the commands in context is invaluable which makes it an excellent learning tool.

SVG to CSS Shape Converter originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

A Reader’s Question on Nested Lists

Css Tricks - Mon, 05/19/2025 - 2:32am

A couple of days back, among the tens of crypto-scams that flood our contact inbox, we found an interesting question on nested lists from one of our readers.

I have a problem (related to list-numbering) that seems commonplace, but I can’t seem to solve it or find any solution for. If any of your geniuses can answer this, I’m sure there are going to be a lot of people interested.

Styling lists? Enough to catch my attention. After all, I just completed an entire guide about CSS counters. The message continues:

Here’s the problem. It’s a routine numbering sequence, of different levels, found in (for example) [government], legislation, and in my case, condominium bylaws. I have five levels represented by the first number at each level of 1., (1), (a) (lower-alpha), (i) (lower-roman), (A) (upper-alpha). Of course, I have 5 levels here, but if you could demonstrate a solution for 3 levels.

Fair enough! So, what we are looking to achieve is a nested list, where each sublist marker/counter is of a different kind. The example linked in the message is the following:

8 The strata corporation must repair and maintain all of the following: (a) common assets of the strata corporation; (b) common property that has not been designated as limited common property; (c) limited common property, but the duty to repair and maintain it is restricted to (i) repair and maintenance that in the ordinary course of events occurs less often than once a year, and (ii) the following, no matter how often the repair or maintenance ordinarily occurs: (A) the structure of a building; (B) the exterior of a building; (C) chimneys, stairs, balconies and other things attached to the exterior of a building; (D) doors, windows and skylights on the exterior of a building or that front on the common property;

While simple at first glance, it still has some nuance, so let’s try to come up with the most maintainable solution here.

The ugly way

My first approach to this problem was no approach at all; I just opened CodePen, wrote up the HTML, and tried to get my CSS to work towards the final result. After translating the Markdown into ol and li elements, and with no special styling on each list, the base list would look like the following:

CodePen Embed Fallback

Once there, my first instinct was to select each ol element and then change its list-style-type to the desired one. To target each level, I selected each ol depending on its number of ol ancestors, then let the specificity handle the rest:

ol { list-style-type: decimal; /* Unnecessary; just for demo */ } ol ol { list-style-type: lower-alpha; } ol ol ol { list-style-type: lower-roman; } ol ol ol ol { list-style-type: upper-alpha; }

And as you can see, this works… But we can agree it’s an ugly way to go about it.

CodePen Embed Fallback Nesting to the rescue

Luckily, CSS nesting has been baseline for a couple of years now, so we could save ourselves a lot of ol selectors by just nesting each element inside the next one.

ol { list-style-type: decimal; ol { list-style-type: lower-alpha; ol { list-style-type: lower-roman; ol { list-style-type: upper-alpha; } } } }

While too much nesting is usually frowned upon, I think that, for this case in particular, it makes the CSS clearer on what it intends to do — especially since the CSS structure matches the HTML itself, and it also keeps all the list styles in one place. All to the same result:

CodePen Embed Fallback It’s legal

I don’t know anything about legal documents, nor do I intend to learn about them. However, I do know the law, and by extension, lawyers are finicky about how they are formatted because of legal technicalities and whatnot. The point is that for a legal document, those parentheses surrounding each list marker — like (A) or (ii) — are more than mere decoration and have to be included in our lists, which our current solution doesn’t.

A couple of years back, we would have needed to set a counter for each list and then include the parentheses along the counter() output; repetitive and ugly. Nowadays, we can use the @counter-style at rule, which as its name implies, allows us to create custom counter styles that can be used (among other places) in the list-style-type property.

In case you’re unfamiliar with the @counter-style syntax, what we need to know is that it can be used to extend predefined counter styles (like decimal or upper-alpha), and attach to them a different suffix or prefix. For example, the following counter style extends the common decimal style and adds a dash (-) as a prefix and a colon (:) as a suffix.

@counter-style my-counter-style { system: extends decimal; prefix: "- "; suffix: ": "; } ol { list-style-type: my-counter-style; } CodePen Embed Fallback

In our case, we’ll need four counter styles:

  • A decimal marker, without the ending dot. The initial submission doesn’t make it clear if it’s with or without the dot, but let’s assume it’s without.
  • A lower alpha marker, enclosed in parentheses.
  • A lower Roman marker, also enclosed in parentheses.
  • An upper alpha marker, enclosed in parentheses as well.

All these would translate to the following @counter-style rules:

@counter-style trimmed-decimal { system: extends decimal; suffix: " "; } @counter-style enclosed-lower-alpha { system: extends lower-alpha; prefix: "("; suffix: ") "; } @counter-style enclosed-lower-roman { system: extends lower-roman; prefix: "("; suffix: ") "; } @counter-style enclosed-upper-alpha { system: extends upper-alpha; prefix: "("; suffix: ") "; }

And then, we just gotta replace each with its equivalent in our initial ol declarations:

ol { list-style-type: trimmed-decimal; ol { list-style-type: enclosed-lower-alpha; ol { list-style-type: enclosed-lower-roman; ol { list-style-type: enclosed-upper-alpha; } } } } CodePen Embed Fallback It should work without CSS!

Remember, though, it’s a legal document, so what happens if the internet is weak enough so that only the HTML loads correctly, or if someone checks the page from an old browser that doesn’t support nesting or @counter-style?

Thinking only about the list, in most websites, it would be a mild annoyance where the markers go back to decimal, and you have to go by padding to know where each sublist starts. However, in a legal document, it can be a big deal. How big? I am no lawyer, so it beats me, but we still can make sure the list keeps its original numbering even without CSS.

For the task, we can use the HTML type attribute. It’s similar to CSS list-style-type but with its own limited uses. First, its use with ul elements is deprecated, while it can be used in ol elements to keep the lists correctly numbered even without CSS, like in legal or technical documents such as ours. It has the following values:

  • "1" for decimal numbers (default)
  • "a" for lowercase alphabetic
  • "A" for uppercase alphabetic
  • "i" for lowercase Roman numbers
  • "I" for uppercase Roman numbers

Inside our HTML list, we would assign the correct numbering for each ol level:

CodePen Embed Fallback

Depending on how long the document is, it may be more the hassle than the benefit, but it is still good to know. Although this kind of document doesn’t change constantly, so it wouldn’t hurt to add this extra safety net.

Welp, that was kinda too much for a list! But that’s something intrinsic to legal documents. Still, I think it’s the simplest way to achieve the initial reader’s goal. Let me know in the comments if you think this is overengineered or if there is an easier way.

More on lists! Almanac on Apr 23, 2021 list-style ul { list-style: square outside none; } Sara Cope Almanac on Jan 28, 2025 @counter-style @counter-style apple-counter { ... } Juan Diego Rodríguez Article on May 30, 2025 Styling Counters in CSS Juan Diego Rodríguez

A Reader’s Question on Nested Lists originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Background Agents Reduce Context Window Issues

LukeW - Sun, 05/18/2025 - 2:00pm

Anyone that's gotten into a long chat with an AI model has likely noticed things slow down and results get worse the longer a conversation continues. Many chat interfaces will let people know when they've hit this point but background agents make the issue much less likely to happen.

Across all our AI-first companies, whether coding, engineering simulation, or knowledge work, a subset of people stay in one long chat session with AI models and never bother to create a new session when moving on to a new task. But... why does this matter? Long chat sessions mean lots of context which adds up to more tokens for AI models to process. The more tokens, the more time, the more cost, and eventually, the more degraded results get.

At the heart of this issue is a technical constraint called the context window. The context window refers to the amount of text, measured in tokens, that a large language model can consider or "remember" at one time. It functions as the AI's working memory, determining how long of a conversation an AI model can sustain without losing track of earlier details.

Starting a new chat session creates a new context window which helps a lot with this issue. So to encourage new sessions, many AI products will pop up a warning suggesting people to move on to a new chat when things start to bog down. Here's an example from Anthropic's Claude.

Warning messages like this aren't ideal but the alternative is inadvertently raking up costs and getting worse results when models try to makes sense of a long thread with many different topics. While AI systems can implement selective memory that prioritizes keeping the most relevant parts of the conversation, some things will need to get dropped to keep context windows manageable. And yes, bigger context windows can help but only to a point.

Background agents can help. AI products that make use of background agents encourage people to kick off a different agent for each of their discrete tasks. The mental model of "tell an agent to do something and come back to check its work" naturally guides people toward keeping distinct tasks separate and, as a result, does a lot to mitigate the context window issue.

The interface for our agent workspace for teams, Bench, illustrates this model. There's an input field to start new tasks and a list showing tasks that are still running, tasks awaiting review, and tasks that are complete. In this user interface model people are much more likely to kick off a new agent for each new task they need done.

Does this completely eliminate context window issues? Not entirely because agents can still fill a context window with the information they collect and use. People can also always give more and more instructions to an agent. But we've definitely seen that moving to a background agent UI model impacts how people approach working with AI models. People go from staying in one long chat session covering lots of different topics to firing off new agents for each distinct tasks they want to get done. And that helps a lot with context widow issues.

Enhancing Prompts with Contextual Retrieval

LukeW - Fri, 05/16/2025 - 2:00pm

AI models are much better at writing prompts for AI models than people are. Which is why several of our AI-first companies rewrite people's initial prompts to produce better outcomes. Last week our AI for code company, Augment launched a similar approach that's significantly improved through its real time codebase understanding.

Since AI-powered agents can accomplish a lot more through the use of tools, guiding them effectively is critical. But most developers using AI for coding products write incomplete or vague prompts, which leads to incorrect or suboptimal outputs.

The Prompt Enhancer feature in Augment automatically pulls relevant context from a developer's codebase using Augment's real-time codebase index and the developer's current coding session. Augment uses its codebase understanding to rewrite the initial prompt, incorporating the gathered context and filling in missing details like files and symbols from the codebase. In many cases, the system knows what's in a large codebase better than a developer simply because it can keep it all "in its head" and track changes happening in real time.

Developers can review the enhanced prompt and edit it before executing. This gives them a chance to see how the system interpreted their request and make any necessary corrections.

As developers use this feature, they regularly learn what's possible with AI, what Augment understands and can do with its codebase understanding, and how to get the most out of both of these systems. It serves as an educational tool, helping developers become more proficient at working with AI coding tools over time.

We've used similar approaches in our image generation and knowledge agent products as well. By transforming vague or incomplete instructions into detailed, optimized prompts written by the systems that understand what's possible, we can make powerful AI tools more accessible and more effective.

HTML Email Accessibility Report 2025

Css Tricks - Fri, 05/16/2025 - 4:38am

Some weekend reading on the heels of Global Accessibility Awareness Day (GAADM), which took place yesterday. The Email Markup Consortium (EMC) released its 2025 study on the accessibility in HTML emails, and the TL;DR is not totally dissimilar from what we heard from WebAIM’s annual web report:

This is the third full year for this report and we are disappointed to see the same issues as we have in previous years. The top 10 issues haven’t changed order since last year, apart from the addition of color contrast, which can be put down to a change in the testing and reporting.

The results come from an analysis of 443,585 emails collected from the past year. According to EMC, only 21 emails passed all accessibility checks — and they were all written by the same author representing two different brands. And, further, that author represents one of the companies that not only sponsors the study, but develops the automated testing tool powering the analysis.

Automated testing is the key here. That’s needed for a project looking at hundreds of thousands of emails, but it won’t surface everything, as noted:

Email that pass our checks may still have accessibility issues that we cannot pick up through automated testing. For example, we check if an alt attribute is present on an image, but we do not check if the text is suitable for that image in the context of that message.

The most common issues relate to internationalization, like leaving out the lang (96% of emails) and dir (98% of emails) attributes. But you’ll be familiar with most of what rounds up the top 10, because it lines up with WebAIM’s findings:

  • Links must have discernible text
  • Element has insufficient color contrast
  • Images must have alternate text
  • Link text should be descriptive
  • Links must be distinguishable without relying on color

I appreciate that the report sheds a light on what accessibility features are supported by specific email clients, such as Gmail. The report outlines a set of 20 HTML, CSS, and ARIA features they look for and found that only one email client (SFR Mail?) of the 44 evaluated supports all of the features. Apple Mail and Samsung Email are apparently close behind, but the other 41? Not so much.

So, yeah. Email has a ways to go, like a small microcosm of the web itself.

HTML Email Accessibility Report 2025 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Scroll-Driven Animations Inside a CSS Carousel

Css Tricks - Thu, 05/15/2025 - 2:30am

I was reflecting on what I learned about CSS Carousels recently. There’s a lot they can do right out of the box (and some things they don’t) once you define a scroll container and hide the overflow.

Hey, isn’t there another fairly new CSS feature that works with scroll regions? Oh yes, that’s Scroll-Driven Animations. Shouldn’t that mean we can trigger an animation while scrolling through the items in a CSS carousel?

Why yes, that’s exactly what it means. At least in Chrome at the time I’m playing with this:

CodePen Embed Fallback

It’s as straightforward as you might expect: define your keyframes and apply them on the carousel items:

@keyframes foo { from { height: 0; } to { height: 100%; font-size: calc(2vw + 1em); } } .carousel li { animation: foo linear both; animation-timeline: scroll(inline); }

There are more clever ways to animate these things of course. But what’s interesting to me is that this demo now combines CSS Carousels with Scroll-Driven Animations. The only rub is that the demo also slaps CSS Scroll Snapping in there with smooth scrolling, which is effectively wiped out when applying the scroll animation.

I thought I might work around that with a view() timeline instead. That certainly makes for a smoother animation that is applied to each carousel item as they scroll into view, but no dice on smooth scrolling.

CodePen Embed Fallback

Scroll-Driven Animations Inside a CSS Carousel originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

This Isn’t Supposed to Happen: Troubleshooting the Impossible

Css Tricks - Wed, 05/14/2025 - 4:01am

I recently rebuilt my portfolio (johnrhea.com). After days and days of troubleshooting and fixing little problems on my local laptop, I uploaded my shiny new portfolio to the server — and triumphantly watched it not work at all…

The browser parses and runs JavaScript, right? Maybe Chrome will handle something a little different from Firefox, but if the same code is on two different servers it should work the same in Chrome (or Firefox) no matter which server you look at, right? Right?

First, the dynamically generated stars wouldn’t appear and when you tried to play the game mode, it was just blank. No really terrible website enemies appeared, nor could they shoot any bad experience missiles at you, at least, not in the game mode, but I guess my buggy website literally sent a bad experience missile at you. Over on the page showing my work, little cars were supposed to zoom down the street, but they didn’t show up, either.

Let me tell you, there was no crying or tears of any kind. I was very strong and thrilled, just thrilled, to accept the challenge of figuring out what was going on. I frantically googled things like “What could cause JavaScript to act differently on two servers?”, “Why would a server change how JavaScript works?”, and “Why does everyone think I’m crying when I’m clearly not?” But to no avail.

There were some errors in the console, but not ones that made sense. I had an SVG element that we’ll call car (because that’s what I named it). I created it in vanilla JavaScript, added it to the page, and zoomed it down the gray strip approximating a street. (It’s a space theme where you can explore planets. It’s really cool. I swear.) I was setting transforms on car using car.style.transform and it was erroring out. car.style was undefined.

I went back to my code on my laptop. Executes flawlessly. No errors.

To get past the initial error, I switched it from car.style to using setAttribute e.g. car.setAttribute('style', 'transform: translate(100px, 200px)');. This just got me to the next error. car was erroring out on some data-* attributes I was using to hold information about the car, e.g. car.dataset.xspeed would also come back undefined when I tried to access them. This latter technology has been supported in SVG elements since 2015, yet it was not working on the server, and worked fine locally. What the Hoobastank could be happening? (Yes, I’m referencing the 1990s band and, no, they have nothing to do with the issue. I just like saying… errr… writing… their name.)

With search engines not being much help (mostly because the problem isn’t supposed to exist), I contacted my host thinking maybe some kind of server configuration was the issue. The very polite tech tried to help, checking for server errors and other simple misconfigurations, but there were no issues he could find. After reluctantly serving as my coding therapist and listening to my (tearless) bemoaning of ever starting a career in web development, he basically said they support JavaScript, but can’t really go into custom code, so best of luck. Well, thanks for nothing, person whom I will call Truckson! (That’s not his real name, but I thought “Carson” was too on the nose.)

Next, and still without tears, I tried to explain my troubles to ChatGPT with the initial prompt: “Why would JavaScript on two different web servers act differently?” It was actually kind of helpful with a bunch of answers that turned out to be very wrong.

  • Maybe there was an inline SVG vs SVG in an img issue? That wasn’t it.
  • Could the browser be interpreting the page as plain text instead of HTML through some misconfiguration? Nope, it was pulling down HTML, and the headers were correct.
  • Maybe the browser is in quirks mode? It wasn’t.
  • Could the SVG element be created incorrectly? You can’t create an SVG element in HTML using document.createElement('svg') because SVG actually has a different namespace. Instead, you have to use document.createElementNS("http://www.w3.org/2000/svg", 'svg'); because SVG and HTML use similar, but very different, standards. Nope, I’d used the createElementNS function and the correct namespace.

Sidenote: At several points during the chat session, ChatGPT started replies with, “Ah, now we’re getting spicy &#x1f525;” as well as, “Ah, this is a juicy one. &#x1f347;” (emojis included). It also used the word “bulletproof” a few times in what felt like a tech-bro kind of way. Plus there was a “BOOM. &#x1f4a5; That’s the smoking gun right there”, as well as an “Ahhh okay, sounds like there’s still a small gremlin in the works.” I can’t decide whether I find this awesome, annoying, horrible, or scary. Maybe all four?

Next, desperate, I gave our current/future robot overlord some of my code to give it context and show it that none of these were the issue. It still harped on the misconfiguration and kept having me output things to check if the car element was an SVG element. Again, locally it was an SVG element, but on the server it came back that it wasn’t.

  • Maybe using innerHTML to add some SVG elements to the car element garbled the car element into not being an SVG element? ChatGPT volunteered to rewrite a portion of code to fix this. I put the new code into my system. It worked locally! Then I uploaded it to the server and… no dice. Same error was still happening.

I wept openly. I mean… I swallowed my emotions in a totally healthy and very manly way. And that’s the end of the article, no redemption, no solution, no answer. Just a broken website and the loud sobs of a man who doesn’t cry… ever…

…You still here?

Okay, you’re right. You know I wouldn’t leave you hanging like that. After the non-existent sob session, I complained to ChatGPT, it again gave me some console logs including having the car element print out its namespace and that’s when the answer came to me. You see the namespace for an SVG is this:

http://www.w3.org/2000/svg

What it actually printed was this:

https://www.w3.org/2000/svg

One letter. That’s the difference.

Normally you want everything to be secure, but that’s not really how namespaces work. And while the differences between these two strings is minimal, I might as well have written document.createElementNS("Gimme-them-SVGzers", "svg");. Hey, W3C, can I be on the namespace committee?

But why was it different? You’d be really mad if you read this far and it was just a typo in my code. Right?

You’ve invested some time into this article, and I already did the fake-out of having no answer. So, having a code typo would probably lead to riots in the streets and hoards of bad reviews.

Don’t worry. The namespace was correct in my code, so where was that errant “s” coming from?

I remembered turning on a feature in my host’s optimization plugin: automatically fix insecure pages. It goes through and changes insecure links to secure ones. In 99% of cases, it’s the right choice. But apparently it also changes namespace URLs in JavaScript code.

I turned that feature off and suddenly I was traversing the galaxy, exploring planets with cars zooming down gray pseudo-elements, and firing lasers at really terrible websites instead of having a really terrible website. There were no tears (joyful or otherwise) nor were there celebratory and wildly embarrassing dance moves that followed.

Have a similar crazy troubleshooting issue? Have you solved an impossible problem? Let me know in the comments.

This Isn’t Supposed to Happen: Troubleshooting the Impossible originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Using Pages CMS for Static Site Content Management

Css Tricks - Mon, 05/12/2025 - 2:42am

Friends, I’ve been on the hunt for a decent content management system for static sites for… well, about as long as we’ve all been calling them “static sites,” honestly.

I know, I know: there are a ton of content management system options available, and while I’ve tested several, none have really been the one, y’know? Weird pricing models, difficult customization, some even end up becoming a whole ‘nother thing to manage.

Also, I really enjoy building with site generators such as Astro or Eleventy, but pitching Markdown as the means of managing content is less-than-ideal for many “non-techie” folks.

A few expectations for content management systems might include:

  • Easy to use: The most important feature, why you might opt to use a content management system in the first place.
  • Minimal Requirements: Look, I’m just trying to update some HTML, I don’t want to think too much about database tables.
  • Collaboration: CMS tools work best when multiple contributors work together, contributors who probably don’t know Markdown or what GitHub is.
  • Customizable: No website is the same, so we’ll need to be able to make custom fields for different types of content.

Not a terribly long list of demands, I’d say; fairly reasonable, even. That’s why I was happy to discover Pages CMS.

According to its own home page, Pages CMS is the “The No-Hassle CMS for Static Site Generators,” and I’ll to attest to that. Pages CMS has largely been developed by a single developer, Ronan Berder, but is open source, and accepting pull requests over on GitHub.

Taking a lot of the “good parts” found in other CMS tools, and a single configuration file, Pages CMS combines things into a sleek user interface.

Pages CMS includes lots of options for customization, you can upload media, make editable files, and create entire collections of content. Also, content can have all sorts of different fields, check the docs for the full list of supported types, as well as completely custom fields.

There isn’t really a “back end” to worry about, as content is stored as flat files inside your git repository. Pages CMS provides folks the ability to manage the content within the repo, without needing to actually know how to use Git, and I think that’s neat.

User Authentication works two ways: contributors can log in using GitHub accounts, or contributors can be invited by email, where they’ll receive a password-less, “magic-link,” login URL. This is nice, as GitHub accounts are less common outside of the dev world, shocking, I know.

Oh, and Pages CMS has a very cheap barrier for entry, as it’s free to use.

Pages CMS and Astro content collections

I’ve created a repository on GitHub with Astro and Pages CMS using Astro’s default blog starter, and made it available publicly, so feel free to clone and follow along.

I’ve been a fan of Astro for a while, and Pages CMS works well alongside Astro’s content collection feature. Content collections make globs of data easily available throughout Astro, so you can hydrate content inside Astro pages. These globs of data can be from different sources, such as third-party APIs, but commonly as directories of Markdown files. Guess what Pages CMS is really good at? Managing directories of Markdown files!

Content collections are set up by a collections configuration file. Check out the src/content.config.ts file in the project, here we are defining a content collection named blog:

import { glob } from 'astro/loaders'; import { defineCollection, z } from 'astro:content'; const blog = defineCollection({ // Load Markdown in the `src/content/blog/` directory. loader: glob({ base: './src/content/blog', pattern: '**/*.md' }), // Type-check frontmatter using a schema schema: z.object({ title: z.string(), description: z.string(), // Transform string to Date object pubDate: z.coerce.date(), updatedDate: z.coerce.date().optional(), heroImage: z.string().optional(), }), }); export const collections = { blog };

The blog content collection checks the /src/content/blog directory for files matching the **/*.md file type, the Markdown file format. The schema property is optional, however, Astro provides helpful type-checking functionality with Zod, ensuring data saved by Pages CMS works as expected in your Astro site.

Pages CMS Configuration

Alright, now that Astro knows where to look for blog content, let’s take a look at the Pages CMS configuration file, .pages.config.yml:

content: - name: blog label: Blog path: src/content/blog filename: '{year}-{month}-{day}-{fields.title}.md' type: collection view: fields: [heroImage, title, pubDate] fields: - name: title label: Title type: string - name: description label: Description type: text - name: pubDate label: Publication Date type: date options: format: MM/dd/yyyy - name: updatedDate label: Last Updated Date type: date options: format: MM/dd/yyyy - name: heroImage label: Hero Image type: image - name: body label: Body type: rich-text - name: site-settings label: Site Settings path: src/config/site.json type: file fields: - name: title label: Website title type: string - name: description label: Website description type: string description: Will be used for any page with no description. - name: url label: Website URL type: string pattern: ^(https?:\/\/)?(www\.)?[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(\/[^\s]*)?$ - name: cover label: Preview image type: image description: Image used in the social preview on social networks (e.g. Facebook, Twitter...) media: input: public/media output: /media

There is a lot going on in there, but inside the content section, let’s zoom in on the blog object.

- name: blog label: Blog path: src/content/blog filename: '{year}-{month}-{day}-{fields.title}.md' type: collection view: fields: [heroImage, title, pubDate] fields: - name: title label: Title type: string - name: description label: Description type: text - name: pubDate label: Publication Date type: date options: format: MM/dd/yyyy - name: updatedDate label: Last Updated Date type: date options: format: MM/dd/yyyy - name: heroImage label: Hero Image type: image - name: body label: Body type: rich-text

We can point Pages CMS to the directory we want to save Markdown files using the path property, matching it up to the /src/content/blog/ location Astro looks for content.

path: src/content/blog

For the filename we can provide a pattern template to use when Pages CMS saves the file to the content collection directory. In this case, it’s using the file date’s year, month, and day, as well as the blog item’s title, by using fields.title to reference the title field. The filename can be customized in many different ways, to fit your scenario.

filename: '{year}-{month}-{day}-{fields.title}.md'

The type property tells Pages CMS that this is a collection of files, rather than a single editable file (we’ll get to that in a moment).

type: collection

In our Astro content collection configuration, we define our blog collection with the expectation that the files will contain a few bits of meta data such as: title, description, pubDate, and a few more properties.

We can mirror those requirements in our Pages CMS blog collection as fields. Each field can be customized for the type of data you’re looking to collect. Here, I’ve matched these fields up with the default Markdown frontmatter found in the Astro blog starter.

fields: - name: title label: Title type: string - name: description label: Description type: text - name: pubDate label: Publication Date type: date options: format: MM/dd/yyyy - name: updatedDate label: Last Updated Date type: date options: format: MM/dd/yyyy - name: heroImage label: Hero Image type: image - name: body label: Body type: rich-text

Now, every time we create a new blog item in Pages CMS, we’ll be able to fill out each of these fields, matching the expected schema for Astro.

Aside from collections of content, Pages CMS also lets you manage editable files, which is useful for a variety of things: site wide variables, feature flags, or even editable navigations.

Take a look at the site-settings object, here we are setting the type as file, and the path includes the filename site.json.

- name: site-settings label: Site Settings path: src/config/site.json type: file fields: - name: title label: Website title type: string - name: description label: Website description type: string description: Will be used for any page with no description. - name: url label: Website URL type: string pattern: ^(https?:\/\/)?(www\.)?[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(\/[^\s]*)?$ - name: cover label: Preview image type: image description: Image used in the social preview on social networks (e.g. Facebook, Twitter...)

The fields I’ve included are common site-wide settings, such as the site’s title, description, url, and cover image.

Speaking of images, we can tell Pages CMS where to store media such as images and video.

media: input: public/media output: /media

The input property explains where to store the files, in the /public/media directory within our project.

The output property is a helpful little feature that conveniently replaces the file path, specifically for tools that might require specific configuration. For example, Astro uses Vite under the hood, and Vite already knows about the public directory and complains if it’s included within file paths. Instead, we can set the output property so Pages CMS will only point image path locations starting at the inner /media directory instead.

To see what I mean, check out the test post in the src/content/blog/ folder:

--- title: 'Test Post' description: 'Here is a sample of some basic Markdown syntax that can be used when writing Markdown content in Astro.' pubDate: 05/03/2025 heroImage: '/media/blog-placeholder-1.jpg' ---

The heroImage now property properly points to /media/... instead of /public/media/....

As far as configurations are concerned, Pages CMS can be as simple or as complex as necessary. You can add as many collections or editable files as needed, as well as customize the fields for each type of content. This gives you a lot of flexibility to create sites!

Connecting to Pages CMS

Now that we have our Astro site set up, and a .pages.config.yml file, we can connect our site to the Pages CMS online app. As the developer who controls the repository, browse to https://app.pagescms.org/ and sign in using your GitHub account.

You should be presented with some questions about permissions, you may need to choose between giving access to all repositories or specific ones. Personally, I chose to only give access to a single repository, which in this case is my astro-pages-cms-template repo.

After providing access to the repo, head on back to the Pages CMS application, where you’ll see your project listed under the “Open a Project” headline.

Clicking the open link will take you into the website’s dashboard, where we’ll be able to make updates to our site.

Creating content

Taking a look at our site’s dashboard, we’ll see a navigation on the left side, with some familiar things.

  • Blog is the collection we set up inside the .pages.config.yml file, this will be where we we can add new entries to the blog.
  • Site Settings is the editable file we are using to make changes to site-wide variables.
  • Media is where our images and other content will live.
  • Settings is a spot where we’ll be able to edit our .pages.config.yml file directly.
  • Collaborators allows us to invite other folks to contribute content to the site.

We can create a new blog post by clicking the Add Entry button in the top right

Here we can fill out all the fields for our blog content, then hit the Save button.

After saving, Pages CMS will create the Markdown file, store the file in the proper directory, and automatically commit the changes to our repository. This is how Pages CMS helps us manage our content without needing to use git directly.

Automatically deploying

The only thing left to do is set up automated deployments through the service provider of your choice. Astro has integrations with providers like Netlify, Cloudflare Pages, and Vercel, but can be hosted anywhere you can run node applications.

Astro is typically very fast to build (thanks to Vite), so while site updates won’t be instant, they will still be fairly quick to deploy. If your site is set up to use Astro’s server-side rendering capabilities, rather than a completely static site, the changes might be much faster to deploy.

Wrapping up

Using a template as reference, we checked out how Astro content collections work alongside Pages CMS. We also learned how to connect our project repository to the Pages CMS app, and how to make content updates through the dashboard. Finally, if you are able, don’t forget to set up an automated deployment, so content publishes quickly.

Using Pages CMS for Static Site Content Management originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

UXPA: Using AI to Streamline Persona & Journey Map Creation

LukeW - Thu, 05/08/2025 - 2:00pm

In her Using AI to Streamline Personas and Journey Map Creation talk at UXPA Boston, Kyle Soucy shared how UX researchers can effectively use AI for personas and journey maps while maintaining research integrity. Here are my notes from her talk:

  • Proto-personas help teams align on assumptions before research. Calling them "assumptions-based personas" helps teams understand research is still needed
  • For proto-personas, use documented assumptions, anecdotal evidence, and market research
  • Research-based personas are based on actual ethnographic research and insights from transcripts, surveys, analytics, etc.
  • Decide on persona sections yourself - this is the researcher's job, not AI's. every element should have a purpose and be relevant to understanding the user
  • Upload data to your Gen AI tool - most tools accept various file formats
  • Different AI tools have different security levels. Be aware of your organization's stance on data privacy
  • Use behavior prompts to get richer information about users, such as "When users encounter X, what do they typically do?"
  • For proto-personas: Ask AI to generate research questions to validate assumptions
  • For research-based personas: Request day-in-the-life narratives
  • Every element on a persona should have a purpose. If it's not helping your design team understand or empathize with users better, it doesn't belong
  • Researchers determine journey map elements (stages, information needed)
  • AI helps fill in the content based on research data
  • Include clear definitions of terms in your prompts (e.g., "jobs to be done")
  • Ask AI to label assumptions when data is incomplete to identify research gaps
  • Don't rely on AI for generating opportunities, this requires team effort
  • AI is a tool for efficiency, not a replacement for UX researchers. The only way to keep AI from taking your job is to use it to do your job better
  • Garbage in, garbage out - biases in your data will be amplified
  • AI tools hallucinate information - know your data well enough to spot inaccuracies
  • Don't use AI for generating opportunities or solutions - this requires team expertise

UXPA: Designing Humane Experiences

LukeW - Thu, 05/08/2025 - 2:00pm

In his Designing Humane Experiences: 5 Lessons from History's Greatest Innovation talk at UXPA Boston, Darrell Penta explored how the Korean alphabet (Hangul), created by King Sejong 600 years ago, exemplifies humane, user-centered design principles that remain relevant today. Here's my notes from his talk:

  • Humane design shows compassion, kindness, and a concern for the suffering or well-being of others, even when such behavior is neither required nor expected
  • When we approach design with compassion and concern for others' well-being, we unlock our ability to create innovative experiences
  • In 15th century Korea (and most historical societies), literacy was restricted to elites
  • Learning to read and write Chinese characters (used in Korea at that time) took years of dedicated study something common people couldn't afford
  • King Sejong created an entirely new alphabet rather than adapting an existing one. There's ben only four instances in history of writing systems were invented independently. most are adaptations of existing systems
Korean Alphabet Innovations
  • Letters use basic geometric forms (lines, circles, squares) making them visually distinct and easier to learn
  • Consonants and vowels have clearly different visual treatments, unlike in English where nothing in the letter shapes indicates their class
  • The shapes of consonants reflect how the mouth forms those sounds: the shape of closed lips, the tongue position behind teeth, etc.
  • Sound features are mapped to visual features in a consistent way. base shapes represent basic sounds. Additional strokes represent additional sound features
  • Letters are arranged in syllable blocks, making the syllable count visible
  • Alphabet was designed for the technology of the time (brush and ink)
  • Provided comprehensive documentation explaining the system
  • Created with flexibility to be written in multiple directions (horizontally or vertically)
  • 5 Lessons for Designers
    1. Be Principled and Predictable: Develop clear, consistent design principles and apply them systematically
    2. Prioritize Information Architecture: Don't treat it as an afterthought
    3. Embrace Constraints: View limitations as opportunities for innovation
    4. Design with Compassion: Consider the broader social impact of your design
    5. Empower Users: Create solutions that provide access and opportunity

UXPA: Bridging AI and Human Expertise

LukeW - Thu, 05/08/2025 - 2:00pm

In his presentation Bridging AI and Human Expertise at UXPA Boston 2025, Stewart Smith shared insights on designing expert systems that effectively bridge artificial intelligence and human expertise. Here are my notes from his talk:

  • Expert systems simulate human expert decision-making to solve complex problems like GPS routing and supply chain planning
  • Key components include knowledge base, inference engine, user interface, explanation facility, and knowledge acquisition
  • Traditional systems were rule-based, but AI is transforming them with machine learning for pattern recognition
  • The explanation facility justifies conclusions by answering "why" and "how" questions
  • Trust is the cornerstone of system adoption. if people don't trust your system, they won't use it
  • Explainability must be designed into the system from the beginning to trace key decisions
  • The "black box problem" occurs when you know inputs and outputs but can't see inner workings
  • High-stakes domains like finance or healthcare require greater explainability
  • Aim for balance between under-reliance (missed opportunities) and over-reliance (atrophied skills) on AI
  • Over-reliance creates false security when users habitually approve system recommendations
  • Human experts remain essential for catching bad data feeds or biased data
  • Present AI as augmentation to decision-making, not replacement
  • Provide confidence scores or indicators of the system's certainty level
  • Ensure users can adjust and override AI recommendations where necessary
  • Present AI insights within existing workflows that match expert mental models
  • Clearly differentiate between human and AI-generated insights
  • Training significantly increases AI literacy—people who haven't used AI often underestimate it
  • Highlight success stories and provide social proof of AI's benefits
  • Focus on automating routine decisions to give people more time for complex tasks
  • Trust is the foundation of AI adoption.
  • Explainability is a spectrum and must be balanced with performance.
  • UX plays a critical role in bridging AI capabilities and human expertise.

Orbital Mechanics (or How I Optimized a CSS Keyframes Animation)

Css Tricks - Thu, 05/08/2025 - 2:33am

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 Fallback

My 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 Fallback

One 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 Fallback

Lastly, 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 Fallback

And 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.

Syndicate content
©2003 - Present Akamai Design & Development.