Web Standards

We Might Need Something Between Root and Relative CSS Units for “Base Elements”

Css Tricks - Wed, 08/13/2025 - 3:24am

CSS provides us with root and relative values.

  • Root values are like rem and rlh — they’re tied to the values written in the :root selector (the most common one would be the html element).
  • Relative values are like em, lh, ch and various others — they’re tied to the font-size in that specific element.

I’ve come to realize that perhaps we need to have a unit between root and relative values. Having such a unit allows us to size things without complex em or lh calculations.

Let me give you an example: Prose

Earlier this year, Jen Simmons wrote about the using the lh unit to style margin and padding for better typographical vertical rhythm.

p { margin-block: 1lh; }

We can expand the concept a little further to include all other spaces around the text. One way of doing this is the “Lobotomized Owl” technique that Heydon Pickering popularized a while ago.

* + * { margin-top: 1lh; }

Today, we can also use the :not(:first-child) to achieve the same effect — and that might be a tad more readable.

*:not(:first-child) { margin-top: 1lh; }

Often, we need to constrain these selectors so they don’t spill everywhere and break the rest of the page. One great class for this is .prose.

.prose { *:not(:first-child) { margin-top: 1lh; } }

This is simple and good — but what happens if you include typography of other sizes…? You’ll see this break down incredibly quickly (because 1lh of a <h2> element can be incredibly big).

CodePen Embed Fallback

One way around this issue is to use Flexbox on the parent element. By doing so, we can set gap to 1lh and we don’t have to deal with the value of 1lh changing on the h2 element. (Bonus, we also don’t have to deal with margin collapse.)

.prose { display: flex; flex-direction: column; gap: 1lh; } CodePen Embed Fallback

But we introduce a new problem here: proximity confusion.

Content below <h2> belongs within the <h2>. But content above the <h2> belongs with the previous section header. We should, ideally, make the spacing different to clarify their relationship.

The simplest way is to add a little margin above the <h2>.

But we can’t add margin above <h2> with lh since the lh value on <h2> will be different from that of the surrounding elements.

CodePen Embed Fallback

We have to use a little CSS trickery and margin-bottom (or the logical equivalent) on the element above the <h2>. Here, we simply set margin-bottom to 1lh since we use Flexbox and don’t have to deal with margin collapse. (If you had to deal with margin collapse, you’d have to set margin-bottom to 2lh.)

CodePen Embed Fallback

Is there a better way? Well, that’s what this article is about!

But before we go there, let’s consider a different UI that has similar problems so you can begin to see the greater ramifications of this problem (and the importance of the solution).

Here’s a second example: Card component

Now let’s say we have a card component that’s divided into two parts, header and content.

In these kind of components, the header is often styled with a different font-size than the content.

To create such a card, the simplest markup may be:

<div class="card"> <h2 class="title">Card Title</h2> <div class="content">Card Content</div> </div>

Unfortunately, we cannot use the lh unit to create the padding within the card — doing so causes the margin on the <h2> element to blow (incredibly) out of proportion!

CodePen Embed Fallback

There are, of course, many ways to handle this type of situation.

One possible way is to change the markup such that the <h2> resides in a <header> element. When we do this, we can apply the padding on the <header>, bypassing the enlarged 1lh problem.

<div class="card"> <header class="title"> <h2>Card Title</h2> </header> <div class="content">Card Content</div> </div> CodePen Embed Fallback

While changing the markup solves the problem, it’s not ideal — since we probably don’t want to create an extra header element unless it’s necessary…

Well, another possible method is to use a root value like rlh. This allows <h2> and content to use the same base unit, and therefore, create the same padding.

CodePen Embed Fallback

But we still run into problems if the .card needs to scale to different font-size values. Imagine you want to make a smaller card — now 1rlh isn’t going to look right since the padding value becomes too big in proportion to the content.

CodePen Embed Fallback

What can we do?

A simple solution is to change the padding value according to the supported variants of the component — but this kinda thing is sorta hard-coded and not very friendly…

.card-sm { --padding: 0.75rlh; } .card-md { --padding: 1rlh; } .card-lg { --padding: 1.25rlh; } CodePen Embed Fallback

What’s the alternative?

This is where an intermediary between root and relative units might come in handy.

The handy in-between unit

This section is purely speculative CSS to illustrate a point. We’ll follow up with a simple way to actually do this in practice today in a later section, so hang tight and follow along conceptually for now.

Let’s say we have a unit that takes it’s reference value from a specified element. We’ll call this a base unit, for lack of a better name.

  • So, 1 base font-size unit could be 1bem.
  • And 1 base line-height unit could be 1blh.

Pretty easy at this point.

Imagine we can style the cards with this base unit. Then we can simply use 1blh to quantify the padding and everything else would be sized appropriately:

.card { > * { padding: 1blh; } } .card-sm { font-size: 0.8em; } .card-md { font-size: 1em; } .card-lg { font-size: 1.2em; }

Hurrah?

Tying this back to the .prose example earlier, it could very well resolve the proximity confusion issue without complicating our selectors:

.prose { h2:not(:first-child) { margin-top: 2blh; } } How might this work?

For this function to be added easily to modern CSS, I could think of two possible ways:

  1. Attach that to container queries.
  2. Define a syntax similar to anchor positioning.
The container query method

We already have stuff like cqw and cqh to denote container width and container height values. It’s not too far of a cry to say we could have a cqem (container-query em) unit or cqlh (container-query line-height).

There are downsides to this approach.

First, containers need to be defined in a parent element. This requires more markup and makes the code somewhat complex and unintuitive. This code below might be a possible implementation:

<div class="container"> <div class="card"> <h2 class="title">Card Title</h2> <div class="content">Card Content</div> </div> </div> .container { container-type: inline-size; } .card { > * { padding: 1cqbl; } }

Dealing with nested containers isn’t much a problem, because we can always set the container-name we want to inherit from. But, there might be a collision if we want to use different container references for cqbl and cqw.

Imagine this:

<!-- We might want to inherit the cqem or cqbl from here --> <div class="container-base"> <!-- But we might need cqw or cqh from here --> <div class="two-column-grid"> <div class="card">...</div> <div class="card">...</div> </div> </div>

Kinda sucks to be limited by container collisions.

Anchor positioning syntax

In this case, we first decide the base we want to inherit from. We can call this a base-anchor, or something similar.

Here, we can explicitly set a base anchor name — or perhaps even leave it as none if we don’t wanna name it. Then the rest of the elements within could inherit from this base value immediately:

.card { base-anchor: --card; /* or perhaps none */ > * { padding: 1blh; } }

If we need to refer to this anchor from a completely unrelated component, we can leverage the anchor name and simply do this:

.far-away-comp { base-name: --card; /* Then use blh from here */ } Double anchor

One fascinating aspect I can think of is a potential double-anchor use case where the base component is able to inherit its font-size or value from yet another base or its parent element.

This flexibility lets us create component variations based on font sizes incredibly easily without needing to rely on complex em calculations.

Here’s an example of what I’m talking about:

.prose { base-anchor: --prose; font-size: 1em; line-height: 1.5; } /* Inherits font-size from .prose */ /* This is automatic if base-name is not provided */ .card { base-anchor: none; base-name: --prose; /* In this case, 1blh could be 1.5em */ > * { padding: 1blh; } } /* After inheriting the font size, since we have a base-anchor in the card, we adjust the font-size value accordingly, so: - 1bem would mean 0.8em further in the card - 1blh could then mean 0.8 * 1.5em = 1.2em */ .card.card-sm { font-size: 0.8em; }

Fascinating, yeah? This brings about a whole new possibility when creating reusable components.

Putting it into practice today

Let me preface this section with the fact that bem and blh does not exist today. So whatever implementation I can come up with is simply an imperfect stop-gap measure.

Today, we are certain that we can use the em unit for such a purpose — but this requires a little bit more calculation, since em is a relative, not a base unit.

The first step is to determine the base element — and the base font size — which we can do by setting the base-size property:

.card { --base-size: 1em; font-size: var(--base-size); } .card-sm { --base-size: 0.8em; }

We can then simulate the bem (base em) unit by dividing the intended font-size with the base-size:

.card { h2 { --font-size: 2em; font-size: calc(var(--font-size) / var(--base-size)); } }

Unfortunately, the above code won’t work because we can’t perform a calc() division with a unit-ed value. So the best we can do to remove the units from --base-size.

When we do this, we need to perform another calc() on the base element to create the actual font-size property:

.card { --base-size: 1; font-size: calc(var(--base-size) * 1em); }

Then we perform the same calc() in the <h2> to create its font size:

.card { h2 { --font-size: 2; font-size: calc(var(--font-size) / var(--base-size) * 1em); } }

This is all starting to get a little “ugh”.

Nobody wants to all these boilerplate code. So, this is best abstracted away with a mixin, or perhaps even a function. If you use Sass, you might imagine something like this:

@mixin base-anchor() { font-size: calc(var(--base-size) * 1em); }

If you use Tailwind, perhaps you can imagine the Tailwind utility to do the same. After all, Tailwind utilities can be seen as convenient Sass mixins.

@utility base-anchor { font-size: calc(var(--base-size) * 1em); }

We can then apply this utility into the base element. The code looks a little bit cleaner:

.card { @apply base-anchor; --base-size: 1; } .card-sm { --base-size: 0.8; }

For the <h2>, we can create another utility to perform the calculation for us. It’ll look something like this:

@utility text-relative { font-size: calc(var(--text-size) / var(--base-size) * 1em); }

We can then use the utility like this:

.card .title { @apply text-relative; --text-size: 2; }

Now, to calculate the padding of the card for the .title element, we need to reverse the font-size to get the base-size value. This is best done with a CSS function, which is not widely supported today, but hopefully, soon!

@function --bem(--multiplier) { result: calc(var(--text-size / var(--base-size) * 1em * --multiplier)); }

We can then use --bem to calculate the padding on the card title:

.card .title { /* ... */ padding-block: --bem(0.5); padding-inline: --bem(1); }

We mentioned above that the lh value works better for margin and padding since it preserves vertical rhythm. So, why not create a --blh function too?

In this case, we can add a --leading variable that the function can inherit from:

@function --blh(--multiplier, --lh-multiplier) { result: calc( var( --text-size / var(--base-size) * 1em * --multiplier * var(--lh-multiplier, var(--leading)) ) ); }

Then we can use --blh like this:

.card .title { /* ... */ padding-block: --blh(0.5); padding-inline: --blh(1); } In the spirit of today

We can’t use --bem and --blh in production because CSS Functions are not available all browsers yet. In the spirit of making bem work right now, we can create a utility, that calculates the --base-font-size from the --font-size.

Notice this new variable is called --base-font-size, not --base-size, since --base-size is already used. (We cannot overwrite the CSS variable.)

/* I multiplied the value by 1em here to make it easy for you to use the value */ @utility base-font-size { --base-font-size: calc(var(--base-size) / var(--font-size) * 1em); }

We can also create a utility called base-line-height to get the value of the line-height. When we do this, it’s much easier if we also pass in a --leading variable:

@utility base-line-height { --base-leading: calc(var(--base-font-size)* var(--leading)); }

Then we can use calc on --base-leading to get the values we want:

.card .title { @apply text-relative; @apply base-font-size; @apply base-line-height; --font-size: 2; padding-inline: var(--base-line-height); padding-block: calc(var(--base-line-height) * 0.5); } Putting it all together

Let’s first put together the necessary utilities and functions to make this happen today:

/* The necessary utilities */ @utility base-anchor { font-size: calc(var(--base-size) * 1em); } @utility text-relative { font-size: calc(var(--font-size) / var(--base-size) * 1em); } /* To use this today */ @utility base-font-size { --base-font-size: calc(var(--base-size) / var(--font-size) * 1em); } @utility base-line-height { --base-line-height: calc(var(--base-font-size)* var(--leading)); } /* Easier usage when CSS Functions become available */ @function --bem(--multiplier) { result: calc(var(--font-size / var(--base-size) * 1em * --multiplier)); } @function --blh(--multiplier, --lh-multiplier) { result: calc( var( --font-size / var(--base-size) * 1em * --multiplier * var(--lh-multiplier, var(--leading)) ) ); }

Now here’s the .card code to achieve the functionality in Tailwind we were talking about. You can see it at work here.

/* What we can actually use today */ .card { @apply base-anchor; --base-size: 1; --leading: 1.5; > * { padding: 1lh; } .title { @apply text-relative; @apply base-font-size; @apply base-line-height; --font-size: 2; padding-inline: var(--base-line-height); padding-block: calc(var(--base-line-height) * 0.5); } } .card-sm { --base-size: 0.8; .title { --font-size: 1.2; } } /* What we can use when CSS Functions are available */ .card { @apply base-anchor; --base-size: 1; > * { padding: calc(--blh(1)); } .title { @apply text-relative; --text-size: 2; padding-block: calc(--blh(0.5)); } }

It’s still not as pretty as the bem and blh versions I’ve shown you above, but at the very least, we achieve some sort of functionality, yeah? And it doesn’t look half bad!

Using this with Splendid Labz today

Splendid Styles — the branch of Splendid Labz that handles design and styles — contains the code you can use today.

We also included the --bem and --blh versions if you wanna play with them as well.

To use Splendid Styles, just download the library, import the base-font-size file, and do what you’ve just seen the above!

npm i @splendidlabz/styles @import '@splendidlabz/styles/typography/base-font-size.css'

That’s it!

Now, if you’re interested in all of the tools I’m been cooking up to make web development simpler, you can grab an early bird discount for the Splendid Pro package today — this is available to all CSS-Tricks readers!

(I might add a lifetime option to the Styles package as it evolves to sufficiently. But it might be a year or so before that happens.)

Alright, enough promotion. Let’s come back here.

What do you think about this unit between root and relative values?

I hesitate to call it “base” em because “base” can mean so many things. But it also sounds right at the same time.

  • Does bem and blh make sense to you?
  • Do you think I’m thinking a wee bit too much for this design aspect?
  • Maybe you’ve got a better name for this?

I’d love to hear from you so please feel free to share your thoughts below!

We Might Need Something Between Root and Relative CSS Units for “Base Elements” originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

CSS-Questions

Css Tricks - Tue, 08/12/2025 - 5:02am

Sunkanmi Fafowora is a frequent flier around here. You’ve probably seen his name pop up in the CSS-Tricks Almanac and we actually just published something today that he wrote up for the color-mix() function. The guy spends a lot of time in the Almanac because he loves technical documentation, something he showed off when writing the CSS Color Functions Guide.

And it’s that love for technical documentation that lead him to ship CSS-Questions (gotta love that hyphenated URL, right?!), a place where you can test your CSS knowledge with over 100 questions. You can take the comprehensive exam or a basic one with 20 questions if all you want is a pop quiz.

And of course, the first question I get is related to CSS color functions!

You’re just trolling me now, aren’t you Sunkanmi?!

My first try was a valiant effort, I’d say…

Almost perfect. Could be the title of my autobiography.

There, now I feel better. &#x1f605;

CSS-Questions originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Getting Creative With Quotes

Css Tricks - Mon, 08/11/2025 - 3:43am

Block quotes and pull quotes are useful for punctuating solid blocks of running text. They’re also two of the best typographic elements for acting as visual landmarks to catch someone’s eye. There are no rules about how long a quote should be, how big it should look, or even how it’s styled.

So, how do you design block quotes and pull quotes to reflect a brand’s visual identity and help tell its story? Here’s how I do it by styling the HTML blockquote element using borders, decorative quote marks, custom shapes, and a few unexpected properties.

Patty Meltt is an up-and-coming country music sensation.

My brief: Patty Meltt is an up-and-coming country music sensation, and she needed a website to launch her new album. She wanted it to be distinctive-looking and memorable, so she called Stuff & Nonsense. Patty’s not real, but the challenges of designing and developing sites like hers are.

First, a quote-unquote “recap.”

There are no limitations on how quotations can be styled. Block and pull quotes can both be eye-catching design elements, but they convey different messages. While a block quote is typically inside the content flow, a pull quote (sometimes called a callout) is extracted from the text to form a separate element.

Pull quotes extracted from the text

The proper HTML for marking up a block quote depends on its contents. My design for Patty Meltt includes concert reviews, which contain the reviewer’s name:

<blockquote> <p>"Patty Meltt’s powerful ballads and toe-tapping anthems had the audience singing along all night."</p> <footer>— Waylon Bootscuffer</footer> </blockquote>

Here, the footer contains information about the source or author of the parent element. This makes it a good fit for attributions inside a blockquote, where it indicates who wrote it. But what about cite?

For years, I used the cite element to mark up attributions. It’s one of those sneaky bits of HTML that felt intuitive until I read the spec and went, “Dagnabbit!” because cite isn’t meant to label people. Instead, it should be used for:

“The title of a creative work (e.g. book, website, song, painting, etc.)”

<blockquote> <p>"Patty Meltt’s powerful ballads and toe-tapping anthems had the audience singing along all night."</p> <footer>— Waylon Bootscuffer, <cite>Country Music Magazine</cite></footer> </blockquote>

So, in that example, footer marks up the attribution, and cite points to the title of the publication, not the person writing it. This gives the markup a semantic boost and helps people who use screen readers.

Styling with personality

Out-of-the-box, browsers do very little to style blockquotes, except for adding inline margins. You could add some simple blockquote styling, but with just a little more style, you can transform them into expressive design elements that reflect a brand’s personality and voice.

Quotation designs to reflect a brand’s personality and voice

For Patty Meltt’s design, I wanted her quotes to feel confident, loud, and a little over the top.

Tip: Interactive examples from this article are available in my lab.

Borders

A simple border, used well, can make block and pull quotes stand out and anchor them into a layout. A border on the left or top separates a block quote from surrounding content, helping a reader recognise it as a different voice from the main narrative.

In magazines and newspapers, block quotes punctuate content blocks and are frequently styled to contrast with the surrounding text. A full-width, bordered block quote encourages a reader to pause for a moment.

Block quote with left border (left) and a block quote with top border (right)

It’s a simple, yet effective, way to focus someone’s attention on a message. A thin border feels quiet and understated:

blockquote { padding-inline: 1.5rem; border-inline-start: 1px solid #98838e; border-inline-end: 1px solid #98838e; } Pull quotes with thin borders

This may suit some brands, but that’s not a style which reflects Patty’s personality. Whereas a bolder, thicker border feels more confident, like it has something important to say:

blockquote { padding-inline: 1.5rem; border-inline-start: 5px solid #98838e; border-inline-end: 5px solid #98838e; } Pull quotes with thick borders

Those borders needn’t always fill the full height or width of a blockquote, so instead of using the border property, use ::before and ::after pseudo-elements to add faux borders at any size:

blockquote { display: flex; flex-direction: column; align-items: center; } blockquote::before, blockquote::after { content: ""; display: block; width: 80px; height: 5px; background-color: #98838e; } Pull quote with faux borders

You could even animate those faux borders using keyframe animations or simple transitions to increase their width when someone interacts with the quotation:

blockquote::before, blockquote::after { content: ""; display: block; width: 80px; height: 5px; background-color: #98838e; transition: 300ms width ease-in-out; } blockquote:hover::before, blockquote:hover::after { width: 100%; } Quote marks

Before choosing how to style your quote marks, consider whether you need them at all. Technically, an HTML blockquote implies its content is a quotation. So, from an accessibility and semantic standpoint, quote marks aren’t required because screen readers and search engines will recognise a blockquote. However, quote marks can visually emphasise quoted content and add interest and personality to a design.

Quote marks add interest and personality

Are both opening and closing marks always needed? Possibly, when a design needs a traditional feel, or a quotation appears in a passage of running text:

blockquote { position: relative; padding-inline: 64px; } blockquote img:first-of-type, blockquote img:last-of-type { position: absolute; } blockquote img:first-of-type { top: 0; left: 0; } blockquote img:last-of-type { right: 0; bottom: 0; } Decorative oversized opening mark

Or, to give a design an editorial feel, you might use only a decorative oversized opening mark for a pull quote, which is separate from the normal flow of text:

blockquote { display: flex; flex-direction: column; align-items: center; } blockquote::after { content: ""; display: block; width: 80px; height: 5px; background-color: #98838e; } Quote marks library

Block quotes don’t necessarily need quote marks, but when you use them with purpose, they become more than punctuation. They become part of the design personality. Decorative marks are ideal when a brand wants to infuse its character into a design.

Poppins quote mark (left) and a Punch 3D quote mark (right)

Sadly, even the nicest designed typefaces can include dull and uninspiring quote marks. So, it’s important to remember that you can choose marks from an altogether different font if they better suit a design.

Part of my quote marks library

That’s why, whenever I audition a new typeface, I check its quote marks. If they’re memorable or noteworthy, I add them as SVGs to my quote marks library so I can easily find them later.

Shapes

Quotation design needn’t stop at borders and quote marks. Block and pull quotes can be any shape. You might style an album or concert review as a speech or thought bubble, and include an avatar for the author. Or, you could use a clip-path or mask to transform a quotation into any shape you can imagine.

Speech bubble, thought bubble, and blob Designing for Patty Meltt

Patty Meltt wanted a website packed with design details. Every element added to a design is an opportunity to be expressive, and that includes her quotations. From the selection of designs I showed her, she felt a mixture of quote marks, avatar images, and borders — with type set in a flowing script — best suited her style.

Design for Patty Meltt’s block quote (left) and pull quote (right)

To implement her pull quote, I used a cursive typeface, which contrasts with the rest of her typographic design:

blockquote p { font-family: "Lobster Two", cursive; font-size: 1.5rem; font-weight: 700; font-style: italic; text-transform: unset; }

Then I added an SVG quote mark from the Ohno type foundry’s Blazeface type family.

<div> <img src="img/blazeface-start.svg" alt="" width="48"> </div>

I turned its parent division into a flex container and aligned the contents vertically:

blockquote div { display: flex; align-items: center; gap: 1.5rem; }

…and used generated content to add a flexible-width horizontal line to fill any remaining space:

blockquote div:first-of-type::after { content: ""; display: block; flex: 1; height: 5px; background-color: #98838e; } Conclusion

With a little care and creativity, block quotes can become expressive, brand-building elements, as distinctive as a logo or headline. Whether you’re working with quiet, thoughtful quotes or loud, in-your-face testimonials, styling them is an opportunity to reinforce a client’s personality and voice.

Patty Meltt’s quotations became mini design statements. But the same principles apply no matter the brand: get the semantics right, choose styling that fits the tone, and don’t be afraid to experiment with borders, quote marks, and even shapes.

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

Stuff & Nonsense Practical Layout Workshop

Css Tricks - Mon, 08/11/2025 - 2:16am

We don’t publish a big ol’ bunch of links pushing products and whatnot around here. But I do like sharing a good resource when it’s available and that’s what I’m doing here with Andy Clarke’s upcoming Practical Layout Workshop.

First off, the deets:

  • Date: Thursday, 18th September
  • Time: 3:00 p.m. (UK)
  • Duration: 2 hours (live, with demos and Q&A)
  • Format: Online (join from anywhere), recording included
  • Price: £69

You probably already know Andy pretty well. He’s been a leading voice in web design forever. He’s also written a slew of super CSS-Tricks articles recently (including this one today, with more on the way!) that are all about “getting creative” with common UI patterns while leveraging CSS to embrace a healthy dose of whimsy.

Here’s what he had to say to me about the workshop:

Over the past year, I’ve been exploring how classic layout principles from comics, posters, and magazines can bring energy and storytelling to digital design. This session is where I share what I’ve learned, with examples, demos, and practical takeaways you can actually use.

Just get a whiff of the stuff he’s been digging into:

Article on Jun 3, 2025 Getting Creative With HTML Dialog Andy Clarke Article on Jul 18, 2025 Getting Creative With Versal Letters Andy Clarke Article on Jan 27, 2025 Revisiting CSS Multi-Column Layout Andy Clarke Article on Mar 21, 2025 Revisiting CSS border-image Andy Clarke Article on Apr 30, 2025 Revisiting Image Maps Andy Clarke

And when you pair that with the Toon Title demos he’s been publishing, you get a really good idea of the clever ideas and solid techniques you’ll take away from this workshop.

Oh, and CSS-Tricks readers can get in at a £5 discount with coupon code FRIEND5 (that’s the number 5 at the end) at checkout. &#x1f609;

Stuff & Nonsense Practical Layout Workshop originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

How to Prepare for CSS-Specific Interview Questions

Css Tricks - Fri, 08/08/2025 - 3:42am

If you landed on this article, chances are you might have a front-end interview coming up, perhaps one with a focus on CSS, or you are in the market to start preparing for CSS-related interviews altogether. Depending on the exact role you are interviewing for, it’s unlikely that you will only be asked questions about CSS. Typically, you will encounter a combination questions covering things like HTML, CSS, and JavaScript.

For this article, we will focus primarily on a set of 10 CSS questions you likely will encounter in front-end interviews, even if they get grouped with HTML or JavaScript. And to be clear, these may or may not be the “best” questions for an interviewer to ask, but what you are likely to see, based on my experience as the founder of frontendlead.com, an all-in-one platform to help front-end engineers prepare for big tech interviews. I have been a software engineer for over 13 years and have been on both ends of hundreds of interviews.

The questions cover different levels of difficulty. To keep things simple, we will start with the “easiest” questions and end with the “hardest” ones.

#Interview QuestionDifficulty1How would you go about building a responsive website?Easy2What are CSS preprocessors, and why are they useful?Easy3How would you make fonts responsive in CSS?Easy4Describe z-index and how stacking context is formed.Medium5What’s the difference between block, inline, and inline-block?Medium6What does * { box-sizing: border-box; } do?Medium7How would you go about making an image responsive in CSS?Medium8How would you make CSS more performant?Hard9What are the pros and cons of CSS in JS vs external CSS import, and which would you choose?Hard10Can you build this layout in CSS?Hard

Before we dive in, I’d like to say that there are many ways to correctly answer the same question. Everything I’m providing here is merely guidance for approaching the types of questions you may face in an interview. The actual questions you encounter may need more elaboration in a particular area or require specific examples that demonstrate your understanding of different concepts.

1. How would you go about building a responsive website? (Easy)

Responsive design is one of the fundamentals you’ll be asked about. Building a responsive website means your layout, images, and typography adapt gracefully to any device or screen size.

The basic tools for responsive design include relative units (such as %, em, and rem), media queries, and fluid layouts. Most interviews expect you to mention a “mobile-first” approach, where your base styles are designed for mobile devices and scaled up for larger screens.

A quick code example using media queries:

/* Main container for your page content, centered and with a max width for larger screens */ .container { max-width: 1200px; /* Prevents content from stretching too wide on large displays */ margin: 0 auto; /* Horizontally center the container */ padding: 16px; /* Adds space inside the container */ } /* Make all images scale with their parent container */ img { max-width: 100%; /* Image will never be wider than its container */ height: auto; /* Keeps the aspect ratio intact */ display: block; /* Removes extra space below images (inline images have baseline spacing) */ } /* Responsive styles for small screens (phones, small tablets) */ @media (max-width: 600px) { .container { padding: 8px; /* Reduce padding to save space on smaller screens */ } /* Example: Stack nav links vertically on small screens nav ul { flex-direction: column; } */ }

You should also mention how you handle navigation and images on mobile devices (such as collapsing navigational menus or leveraging responsive image techniques), as well as how to test layouts using browser Developer Tools.

2. What are CSS preprocessors, and why are they useful? (Easy)

CSS preprocessors, such as SassLess, and Stylus, make writing and maintaining large CSS codebases significantly easier. They add features that aren’t in vanilla CSS, such as mixins, and functions — although those lines are becoming more blurred as CSS ships similar features, such as variablesnesting, and yes, mixins and functions.

Mixins and functions enable you to reuse common patterns and even generate code based on parameters. Here’s an example in Sass:

// Mixin: For a common box shadow you want to reuse @mixin shadow($opacity: 0.12) { box-shadow: 0 2px 8px 0 rgba(24, 39, 75, $opacity); } // Function: Calculate a spacing value for consistent margins and padding @function space($multiplier: 1) { @return $multiplier * 8px; } // Placeholder selector: For base button styles to extend %btn-base { display: inline-block; font-size: $font-size-lg; border-radius: 6px; text-align: center; cursor: pointer; } // Partial import: Example (would be in _variables.scss) // @import 'variables'; // Button styles using everything above .button { @extend %btn-base; // Use base button styles background: $primary; color: #fff; padding: space(1.5) space(3); // Use the custom function for spacing @include shadow(0.15); // Use the mixin for shadow // Nested selector for hover state &:hover { background: lighten($primary, 10%); } // Modifier class (e.g., .button.secondary) &.secondary { background: $secondary; color: #23272f; border: 2px solid $secondary; } // Nested media query (for responsive buttons) @media (max-width: 600px) { padding: space(1) space(2); font-size: 1rem; } }

Preprocessors help keep your codebase DRY (Don’t Repeat Yourself) and make refactoring less painful. While CSS now has native variables (--variable), preprocessors are still widely used for their advanced features.

This is a good opportunity to demonstrate your understanding of modern CSS as well since CSS now supports nesting and work on functions is underway (and indeed are already planned for Chrome 139).

3. How would you make fonts responsive in CSS? (Easy)

Font sizing is a common interview topic because it affects both design and accessibility. Responsive fonts adjust to screen size, ensuring your text remains readable. The classic approach is to use relative units, such as em (scoped to the parent element) and rem (scoped to the root element). Newer CSS features makes this even easier and more flexible with the clamp() function and viewport units (vw and vh). You can also use media queries to step up font sizes for larger screens.

Here are some practical examples:

/* Basic responsive text using rem (scales with root html font size) */ body { font-size: 1rem; /* 1rem is typically 16px, but can be increased for accessibility */ } /* Use rem for headings so they scale with user/browser settings */ h1 { font-size: 2.5rem; /* 2.5 × root font size */ line-height: 1.2; } /* Modern fluid sizing with clamp and viewport units */ h2 { /* Font size is at least 1.5rem, scales with viewport up to 3rem */ font-size: clamp(1.5rem, 4vw, 3rem); } /* Using viewport width units directly */ h3 { font-size: 6vw; /* 6% of viewport width (can get very large/small on extremes) */ } /* Responsive font-size using media queries (manual step-up) */ p { font-size: 1rem; } @media (min-width: 600px) { p { font-size: 1.2rem; } } @media (min-width: 1200px) { p { font-size: 1.4rem; } }
  • rem/em units make your text scale with the root or parent font size, making them more responsive to changes.
  • clamp() lets you set a minimum, fluid, and maximum font size at once (e.g., clamp(1.5rem, 4vw, 3rem) ensures the font size never falls below 1.5rem or exceeds 3rem, scaling smoothly in between).
  • Viewport units (vw, vh) make fonts fluid relative to the screen width or height.
  • Media queries enable fine-tuning of font size for various devices and breakpoints.

Absolute px units are usually avoided for body text, as they don’t scale for users who adjust browser settings for accessibility. Speaking of accessibility, it’s worth calling out that extra consideration needs to go into the possibility of the user zooming into the page.

4. Describe the z-index property and how stacking context is formed. (Medium)

Thez-index property determines which elements appear on top of others, but it only works on elements that have a positioning context, such as position: relative, absolute, or fixed.

stacking context is an environment where stacking and rendering order is controlled. New stacking contexts can be created by elements with specific properties, such as position with a z-index, or CSS properties like opacity less than 1, transform, or filter.

Understanding stacking context is essential for UI components like drop-downs, modals, and tooltips.

Here’s an example demonstrating a stacking context created by a parent element element that contains two children that stack one on top of the other, ordered by z-index:

/* The parent creates a new stacking context by having position and z-index */ .parent { position: relative; /* Triggers a positioning context */ z-index: 2; /* This parent will stack above siblings with lower z-index values */ width: 300px; height: 200px; background: #b3e6fc; margin: 32px; } /* The child is absolutely positioned inside .parent */ .child { position: absolute; /* Needed for z-index to work */ top: 40px; left: 40px; width: 200px; height: 100px; background: #4f46e5; color: #fff; z-index: 10; /* Relative to its parent's stacking context, not the whole page */ display: flex; align-items: center; justify-content: center; } /* Another sibling element at the root level for comparison */ .sibling { position: relative; z-index: 1; /* Lower than .parent, so .parent stacks on top */ width: 320px; height: 140px; background: #fca311; margin: -80px 0 0 220px; /* Overlap with .parent for demo */ display: flex; align-items: center; justify-content: center; color: #23272f; }

If you have ever run into an issue where z-index isn’t behaving as you expect, check if there’s an unexpected stacking context due to a parent element.

5. What’s the difference between the display property’s block, inline, and inline-block values? (Medium)

When you’re asked about the difference between the display property’s block, inline, and inline-block values in CSS, remember that they determine how elements are displayed in the document flow.

  • Block elements always start on a new line and take up the full width of their parent container, regardless of their actual content. Examples include <div> and <p>.
  • Inline elements flow within a line of text, only occupying as much width as needed for their content; you cannot set their width or height. Examples include <span> and <a>.
  • Inline-block elements combine the behaviors of both inline and block elements: They flow inline with text (without forcing a new line), but you can set their width and height like a block element, which makes them especially useful for custom buttons or navigation items.
Display ValueStarts New Line?Width/Height Settable?Example ElementsblockYesYes<div>, <p>, <h1>inlineNoNo<span>, <a>, <strong>inline-blockNoYesCustom buttons, images, icons 6. What does box-sizing: border-box do? (Medium)

By default, CSS uses the content-box model, which means that width and height only apply to the content, excluding padding and border. box-sizing: border-box changes this so that width and height include the padding and border, making sizing more predictable.

Here’s an example of how that might be demonstrated in CSS:

/* Apply border-box sizing to all elements and their pseudo-elements */ *, *::before, *::after { box-sizing: border-box; /* Width and height now include padding and border */ } /* Demo: Without border-box (the default, content-box) */ .box-content { box-sizing: content-box; width: 200px; padding: 20px; border: 4px solid #2563eb; background: #f0f4ff; margin-bottom: 16px; /* The real rendered width will be: 200px (content) + 40px (padding) + 8px (border) = 248px */ } /* Demo: With border-box */ .box-border { box-sizing: border-box; width: 200px; padding: 20px; border: 4px solid #16a34a; background: #e7faed; /* The rendered width will be exactly 200px, since padding and border are included in the width */ }

With border-box, you avoid the classic issue where adding padding or a border makes your boxes overflow their parent or break your layout. It’s now a standard best practice. You can even say that Chris Coyier has deemed February 1 “International box-sizing Awareness Day” which totally should be a real thing.

7. How would you go about making an image responsive in CSS? (Medium)

This is a deceptively hard question because responsive images is a topic big enough for an entire guide. The classic approach is to ensure that photos never exceed the width of their container. For most cases, that means setting a max-width on the image element and ensuring it maintains its proportions:

/* 1. Make images responsive to their container width */ img { max-width: 100%; /* Prevents the image from overflowing its parent */ height: auto; /* Maintains aspect ratio */ display: block; /* Removes bottom whitespace that inline images have */ }

For images that need to maintain a specific aspect ratio (like a 16:9 video thumbnail), you can use the padding-bottom trick:

/* 2. Maintain a specific aspect ratio (e.g., 16:9) using the padding-bottom trick */ .responsive-img-container { position: relative; /* Needed for absolutely positioning the img */ width: 100%; /* Full width of the parent container */ padding-bottom: 56.25%; /* 16:9 aspect ratio (9/16 = 0.5625) */ overflow: hidden; /* Ensures image doesn’t overflow container */ } .responsive-img-container img { position: absolute; /* Take the image out of the normal flow */ top: 0; left: 0; width: 100%; /* Stretch to fill container */ height: 100%; /* Stretch to fill container */ object-fit: cover; /* Ensure the image covers the area, cropping if needed */ }

Modern CSS also has the aspect-ratio property for this:

/* 3. Use the aspect-ratio property for a cleaner approach (modern browsers) */ .aspect-ratio-img { aspect-ratio: 16 / 9; /* Maintain 16:9 ratio automatically */ width: 100%; height: auto; display: block; }

Responsive images often use the HTML srcset attribute and the <picture> element as well for high-DPI and various screen sizes. There’s an entire CSS-Tricks guide on those features. And, of course, there are performance considerations to take into account because the goal is to serve the best image format and size to the right device.

8. How would you make CSS more performant? (Hard)

CSS performance is crucial for delivering fast and smooth experiences, especially on large-scale websites or applications. Poor CSS practices can slow down page loads, increase rendering times, and make maintenance harder. There are several strategies you can use to keep your CSS efficient and your site responsive.

At the same time, CSS is often not the source of performance bottlenecks. It certainly contributes to it, but performance is a more nuanced field where many factors most certainly influence performance than CSS.

1. Minimize your bundle size

Large CSS files slow down initial page loads. Removing unused CSS (also called “dead code elimination”) can significantly reduce file size. Tools like PurgeCSSUnCSS, or the built-in features of frameworks like Next.js and Tailwind can scan your HTML/JSX and only keep styles that are used.

2. Split and lazy-load CSS

Instead of shipping all CSS at once, split your styles by page or feature (“code splitting”). Modern bundlers (such as webpack and Vite) and frameworks (like React, Vue, and Next.js) support the dynamic import() feature, allowing only the CSS required for the current route or component to be loaded.

// Dynamically load styles when the page loads import("./styles/page.css");

This technique improves “first paint” times and reduces bandwidth, especially for users who never visit certain pages.

3. Use simple, shallow selectors

Browsers read CSS from right to left and evaluate deeply nested or complex selectors more slowly. For best performance, use flat selectors like .btn instead of something like .header nav ul li a.active.

4. Minify and compress CSS

Before deploying, always minify your CSS using tools like cssnano or clean-css. Gzip or Brotli compression (handled by your server or CDN) will further shrink the payload sent to users.

5. Use critical CSS (or not!)

Critical CSS refers to inlining the minimal CSS required for above-the-fold content in the initial HTML. This allows the browser to render the visible part of the page immediately, while loading the rest of the CSS asynchronously.

I’d say this is a nice-to-have sort of thing, as it is a fragile and difficult strategy to implement and maintain.

6. Reduce the use of expensive properties

Specific CSS properties, such as heavy box shadows, filters, or animations on significant elements, can cause “repaints” and slow down rendering. Use these effects thoughtfully, and prefer transform and opacity for animating elements — the browser’s compositor can often optimize these.

7. Avoid !important and overly specific selectors

Frequent use of !important and particular selectors can make your CSS hard to override and debug, leading to more duplicated or conflicting rules.

8. Optimize unused CSS

Let’s face it. As a site is iterated, CSS often becomes larger, not smaller. Styles that were relevant at one point are superseded by new ones without fully replacing the older styles, often for fear of introducing unexpected changes in unknown places.

We have lots and lots of tools for detecting and removing unused CSS. There are limitations and possible trade-offs, of course, so your mileage may vary.

There there’s the case of UI kits or component libraries that import numerous unused styles. It’s easy (and maybe even tempting) to use all of the styles provided by a framework, but try importing only what you need, or use tree-shaking to strip unused parts. Many frameworks allow you to configure exactly what you need, like Bootstrap does.

10. Audit CSS regularly

Modern browser DevTools (like Chrome’s Coverage tab, Performance panel, and Rendering panel) let you see which styles are used on a page, helping you identify and remove dead code.

There are online tools as well, like the Specificity VisualizerCSS Specificity Graph Generator, and CSS Stats. You can find more information on these and more in “Tools for Auditing CSS”.

9. What are the pros and cons of CSS-in-JS vs. external CSS imports, and which would you choose? (Hard)

CSS-in-JS may not be the hot topic it was a few years go, but you’re still very likely to see it pop up in an interview. It’s not so much your duty to rail for or against it, but demonstrate your understanding of the concept and how it compares to external CSS imports.

Here’s how I would break it out.

CSS-in-JS (like styled-components, Emotion, or Stitches) ProsConsStyles are scoped to components, preventing unwanted side effects.Adds runtime overhead and may increase JS bundle size.Dynamic styling based on component state or props.Styles may not appear immediately on server-rendered pages without extra setup.Easy to maintain styles close to your component logic.It can be harder to debug in the browser inspector. External CSS imports (classic .css files, global or CSS Modules): ProsConsCSS is loaded by the browser in parallel, allowing for faster rendering.Risk of style collision in global CSS.Easier to cache and split CSS for large projects.Less dynamic—harder to do conditional styles based on state.Great for global themes, resets, or utility classes.

In practice, most modern teams use a combination of global styles and resets in CSS files, along with component-level styles using CSS-in-JS or CSS Modules.

10. Can you build this layout in CSS? (Hard)

You’ll almost always be asked to build layouts on the fly.

Remember, a question like this is a great opportunity because there’s more than one way to solve it. In this case, we’re looking at a pretty classic “Holy Grail” layout, something Geoff has written about before and demonstrated various ways to go about it using CSS Grid.

You could go with a Flexbox approach as well:

CodePen Embed Fallback

It would be easy to fall into the trap of finding the “best” solution, but this perhaps is one case where demonstrating how to think like a front-end web developer is equally, if not more, important than coming up with a single definitive solution.

Conclusion

These are merely example of the sort of core CSS questions you’re likely to encounter in front-end interviews, along with practical examples and the reasoning behind each approach. If you’re comfortable answering these in depth and can code out the examples under time pressure, you’ll be well-prepared.

For more front-end interview questions, consider exploring frontendlead.com, which helps you prepare for front-end interviews across top tech companies. If you have additional topics you’d like to see covered or encounter tricky interview questions, please feel free to post them in the comments — I’d love to see them.

And, of course, best of luck in your interviews!

How to Prepare for CSS-Specific Interview Questions originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Dynamic Context for AI Agents

LukeW - Wed, 08/06/2025 - 2:00pm

For AI applications, context is king. So context management, and thereby context engineering, is critical to getting accurate answers to questions, keeping AI agents on task, and more. But context is also hard earned and fragile, which is why we launched templates in AgentDB.

When an AI agent decides it needs to make use of a database, it needs to go through a multi-step process of understanding. It usually takes 3-7 calls before an agent understands enough about a database's structure to accomplish something meaningful with it. That's a lot of time and tokens spent on understanding. Worse still, this discovery tax gets paid repeatedly. Every new agent session starts from zero, relearning the same database semantics that previous agents already figured out.

Templates in AgentDB tackle this by giving AI agents the context they need upfront, rather than forcing them to discover it through trial and error. Templates provide two key pieces of information about a database upfront: a semantic description and structural definition.

The semantic description explains why the database exists and how it should be used. It includes mappings for enumerated values and other domain-specific knowledge. Think of it as the database's user manual written for AI agents. The structural component uses migration schemas to define the database layout. This gives agents immediate understanding of tables, relationships, and data types without needing to query the system architecture.

With AgentDB templates, agents requests like "give me a list of my to-dos" (to-do database) or "create a new opportunity for this customer" (CRM database) work immediately.

Once you've defined a template, it works for any database that follows that pattern. So one template can provide the context an AI agent needs for any number of databases with the same intent. Like a tot-do list database for every user to keep with an earlier example.

But static instructions for AI agents only go so far. These are thinking machines after all. So AgentDB templates can evolve with on use. For example, a template can be dynamically updated with specific queries that worked well. This creates a feedback loop where templates become more effective over time, learning from real-world usage to provide better guidance to future AI interactions.

AgentDB templates are provided to AI agents as an MCP server which also supports raw SQL access. So AI agents can make use of a database effectively right away and still experiment through querying. AgentDB templates are another example of designing software for AI systems rather than humans because they're different "users".

Bringing Back Parallax With Scroll-Driven CSS Animations

Css Tricks - Wed, 08/06/2025 - 3:39am

For a period in the 2010s, parallax was a guaranteed way to make your website “cool”. Indeed, Chris Coyier was writing about it as far back as 2008.

For those unfamiliar with the concept, parallax is a pattern in which different elements of a webpage move at varying speeds as the user scrolls, creating a three-dimensional, layered appearance. A true parallax effect was once only achievable using JavaScript. However, scroll-driven animations have now given us a CSS-only solution, which is free from the main-thread blocking that can plague JavaScript animations.

Parallax may have become a little cliché, but I think it’s worth revisiting with this new CSS feature.

Note: Scroll-driven animations are available on Chrome, Edge, Opera, and Firefox (behind a feature flag) at the time of writing. Use a supported browser when following this tutorial.

Starting code

In this example, we will apply parallax animations to the background and icons within the three “hero” sections of a universe-themed webpage. We’ll start with some lightly styled markup featuring alternating hero and text sections while including some space-related nonsense as placeholder content.

CodePen Embed Fallback Adding initial animations

Let’s add an animation to the background pattern within each hero section to modify the background position.

@keyframes parallax { from { background-position: bottom 0px center; } to { background-position: bottom -400px center; } } section.hero { /* previous code */ + animation: parallax 3s linear; }

Here we use the keyframes CSS rule to create a start and end position for the background. Then we attach this animation to each of our hero sections using the animation property.

By default, CSS animations are duration-based and run when the specified selector is loaded in the DOM. If you refresh your browser, you will see the animation running for three seconds as soon as the page loads.

We do not want our animation to be triggered immediately. Instead, we intend to use the page’s scroll position as a reference to calculate the animation’s progress.

Scroll-driven animations provide two new animation timeline CSS functions. These additions, view() and scroll(), tell the browser what to reference when calculating the progress of a CSS animation. We will use the view() function later, but for now, let’s focus on scroll(). The scroll progress timeline couples the progression of an animation to the user’s scroll position within a scroll container. Parameters can be included to change the scroll axis and container element, but these are not necessary for our implementation.

Let’s use a scroll progress timeline for our animation:

section.hero { /* previous code */ - animation: parallax 3s linear; + animation: parallax linear; + animation-timeline: scroll(); }

If you refresh the page, you will notice that as you scroll down, the position of the background of each hero section also changes. If you scroll back up, the animation reverses. As a bonus, this CSS animation is handled off the main thread and thus is not subject to blocking by any JavaScript that may be running.

Using the view progress timeline

Now let’s add a new parallax layer by animating the header text and icons within each hero section. This way, the background patterns, headers, and main page content will all appear to scroll at different speeds. We will initially use the scroll() CSS function for the animation timeline here as well.

@keyframes float { from { top: 25%; } to { top: 50%; } } .hero-content { /* previous code */ + position: absolute; + top: 25%; + animation: float linear; + animation-timeline: scroll(); }

That’s not quite right. The animation for the sections further down the page is nearly done by the time they come into view. Luckily, the view animation timeline solves this problem. By setting the animation-timeline property to view(), our animation progresses based on the position of the subject within the scrollport, which is the part of the container that is visible when scrolling. Like the scroll animation timeline, scrolling in reverse will also reverse the animation.

Let’s try changing our animation timeline property for the hero text:

.hero-content { /* previous code */ - animation-timeline: scroll(); + animation-timeline: view(); }

That looks pretty good, but there is a problem with the header content flashing into the view when scrolling back up the document. This is because the view timeline is calculated based on the original, pre-animation positioning of the subject element.

We can solve this by adding an inset parameter to the view() function. This adjusts the size of the container in which the animation will take place. According to MDN’s documentation, the “inset is used to determine whether the element is in view which determines the length of the animation timeline. In other words, the animation lasts as long as the element is in the inset-adjusted view.”

So, by using a negative value, we make the container larger than the window and trigger the animation to start a little before and end a little after the subject is visible. This accounts for the fact that the subject moves during the animation.

- animation-timeline: view(); + animation-timeline: view(-100px);

Now both the text and background animate smoothly at different speeds.

CodePen Embed Fallback Adjusting animations using animation ranges

So far, we have employed both scroll and view progress timelines. Let’s look at another way to adjust the start and end timing of the animations using the animation-range property. It can be used to modify where along the timeline the animation will start and end.

We’ll start by adding a view() timeline animation to the #spaceship emoji:

@keyframes launch { from { transform: translate(-100px, 200px); } to { transform: translate(100px, -100px); } } #spaceship { animation: launch; animation-timeline: view(); }

Again, we see the emoji returning to its 0% position once its original unanimated position is outside of the scrollport.

As discussed before, animations are based on the original pre-animation position of the subject. Previously, we solved this by adding an inset parameter to the view() function. We can also adjust the animation range and tell our animation to continue beyond 100% of the animation timeline without having to manipulate the inset of the view timeline itself.

#spaceship { animation: launch; animation-timeline: view(); + animation-range: 0% 120%; }

Now the animation continues until we have scrolled an extra 20% beyond the calculated scroll timeline’s normal endpoint.

Let’s say that we want to add an animation to the #comet emoji, but we don’t want it to start animating until it has passed 4rem from the bottom of the scrollport:

@keyframes rotate { from { transform: rotate(0deg) translateX(100px); } to { transform: rotate(-70deg) translateX(0px); } } #comet { animation: rotate linear; transform-origin: center 125px; animation-timeline: view(); animation-range: 4rem 120%; }

Here we see the “delayed” animation in action:

We can also combine animation ranges to run completely different animations at different points within the same timeline! Let’s illustrate this by combining animation ranges for the #satellite icon at the top of the page. The result is that the first animation runs until the icon passes 80% of the scrollport, then the second animation takes over for the final 20%.

@keyframes orbit-in { 0% { transform: rotate(200deg); } 100% { transform: rotate(0deg); } } @keyframes orbit-out { 0% { transform: translate(0px, 0px); } 100% { transform: translate(-50px, -15px); } } #satellite { animation: orbit-in linear, orbit-out ease; animation-timeline: view(); animation-range: 0% 80%, 80% 110%; } Fallbacks and accessibility

Our webpage features numerous moving elements that may cause discomfort for some users. Let’s consider accessibility for motion sensitivities and incorporate the prefers-reduced-motion CSS media feature.

There are two possible values: no-preference, and reduce. If we want to fine-tune the webpage with animations disabled by default and then enhance each selector with animations and associated styles, then we can use no-preference to enable them.

@media (prefers-reduced-motion: no-preference) { .my-selector { position: relative; top: 25%; animation: cool-animation linear; animation-timeline: scroll(); } }

For us, however, the webpage content and images will still all be visible if we disable all animations simultaneously. This can be done concisely using the reduce option. It’s important to note that this sort of blanket approach works for our situation, but you should always consider the impact on your specific users when implementing accessibility features.

@media (prefers-reduced-motion: reduce) { .my-selector { animation: none !important; } }

In addition to considering accessibility, we should also take into account that scroll-driven animations are not supported by all browsers at the time of writing. If we care a lot about users seeing our animations, we can add a polyfill (direct link) to extend this functionality to currently unsupported browsers. This, however, will force the animation to run on the main thread.

Alternatively, we could decide that performance is important enough to skip the animations on unsupported browsers, thereby keeping the main thread clear. In this case, we can use the @supports selector and include the styles only on supported browsers.

Here is the final code with everything, including the polyfill and reduced motion fallback:

CodePen Embed Fallback Conclusion

There we go, we just re-created a classic web effect with scroll-driven animations using scroll and view progress timelines. We also discussed some of the parameters that can be used to adjust animation behavior. Whether or not parallax is your thing, I like the idea that we can use a modern approach that is capable of doing what we could before… only better with a dash of progressive enhancement.

More information

Bringing Back Parallax With Scroll-Driven CSS Animations originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Keeping Article Demos Alive When Third-Party APIs Die

Css Tricks - Wed, 07/30/2025 - 3:21am

After four years, the demos in my “Headless Form Submission with the WordPress REST API” article finally stopped working.

The article includes CodePen embeds that demonstrate how to use the REST API endpoints of popular WordPress form plugins to capture and display validation errors and submission feedback when building a completely custom front-end. The pens relied on a WordPress site I had running in the background. But during a forced infrastructure migration, the site failed to transfer properly, and, even worse, I lost access to my account.

Sure, I could have contacted support or restored a backup elsewhere. But the situation made me wonder: what if this had not been WordPress? What if it were a third-party service I couldn’t self-host or fix? Is there a way to build demos that do not break when the services they rely on fail? How can we ensure educational demos stay available for as long as possible?

Or is this just inevitable? Are demos, like everything else on the web, doomed to break eventually?

Parallels with software testing

Those who write tests for their code have long wrestled with similar questions, though framed differently. At the core, the issue is the same. Dependencies, especially third-party ones, become hurdles because they are outside the bounds of control.

Not surprisingly, the most reliable way to eliminate issues stemming from external dependencies is to remove the external service entirely from the equation, effectively decoupling from it. Of course, how this is done, and whether it’s always possible, depends on the context.

As it happens, techniques for handling dependencies can be just as useful when it comes to making demos more resilient.

To keep things concrete, I’ll be using the mentioned CodePen demos as an example. But the same approach works just as well in many other contexts.

Decoupling REST API dependencies

While there are many strategies and tricks, the two most common approaches to breaking reliance on a REST API are:

  1. Mocking the HTTP calls in code and, instead of performing real network requests, returning stubbed responses
  2. Using a mock API server as a stand-in for the real service and serving predefined responses in a similar manner

Both have trade-offs, but let’s look at those later.

Mocking a response with an interceptor

Modern testing frameworks, whether for unit or end-to-end testing, such as Jest or Playwright, offer built-in mocking capabilities.

However, we don’t necessarily need these, and we can’t use them in the pens anyway. Instead, we can monkey patch the Fetch API to intercept requests and return mock responses. With monkey patching, when changing the original source code isn’t feasible, we can introduce new behavior by overwriting existing functions.

Implementing it looks like this:

const fetchWPFormsRestApiInterceptor = (fetch) => async ( resource, options = {} ) => { // To make sure we are dealing with the data we expect if (typeof resource !== "string" || !(options.body instanceof FormData)) { return fetch(resource, options); } if (resource.match(/wp-json\/contact-form-7/)) { return contactForm7Response(options.body); } if (resource.match(/wp-json\/gf/)) { return gravityFormsResponse(options.body); } return fetch(resource, options); }; window.fetch = fetchWPFormsRestApiInterceptor(window.fetch);

We override the default fetch with our own version that adds custom logic for specific conditions, and otherwise lets requests pass through unchanged.

The replacement function, fetchWPFormsRestApiInterceptor, acts like an interceptor. An interceptor is simply a pattern that modifies requests or responses based on certain conditions.

Many HTTP libraries, like the once-popular axios, offer a convenient API to add interceptors without resorting to monkey patching, which should be used sparingly. It’s all too easy to introduce subtle bugs unintentionally or create conflicts when managing multiple overrides.

With the interceptor in place, returning a fake response is as simple as calling the static JSON method of the Response object:

const contactForm7Response = (formData) => { const body = {} return Response.json(body); };

Depending on the need, the response can be anything from plain text to a Blob or ArrayBuffer. It’s also possible to specify custom status codes and include additional headers.

For the CodePen demo, the response might be built like this:

const contactForm7Response = (formData) => { const submissionSuccess = { into: "#", status: "mail_sent", message: "Thank you for your message. It has been sent.!", posted_data_hash: "d52f9f9de995287195409fe6dcde0c50" }; const submissionValidationFailed = { into: "#", status: "validation_failed", message: "One or more fields have an error. Please check and try again.", posted_data_hash: "", invalid_fields: [] }; if (!formData.get("somebodys-name")) { submissionValidationFailed.invalid_fields.push({ into: "span.wpcf7-form-control-wrap.somebodys-name", message: "This field is required.", idref: null, error_id: "-ve-somebodys-name" }); } // Or a more thorough way to check the validity of an email address if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.get("any-email"))) { submissionValidationFailed.invalid_fields.push({ into: "span.wpcf7-form-control-wrap.any-email", message: "The email address entered is invalid.", idref: null, error_id: "-ve-any-email" }); } // The rest of the validations... const body = !submissionValidationFailed.invalid_fields.length ? submissionSuccess : submissionValidationFailed; return Response.json(body); };

At this point, any fetch call to a URL matching wp-json/contact-form-7 returns the faked success or validation errors, depending on the form input.

Now let’s contrast that with the mocked API server approach.

Mocked API server with serverless

Running a traditionally hosted mock API server reintroduces concerns around availability, maintenance, and cost. Even though the hype around serverless functions has quieted, we can sidestep these issues by using them.

And with DigitalOcean Functions offering a generous free tier, creating mocked APIs is practically free and requires no more effort than manually mocking them.

For simple use cases, everything can be done through the Functions control panel, including writing the code in the built-in editor. Check out this concise presentation video to see it in action:

For more complex needs, functions can be developed locally and deployed using doctl (DigitalOcean’s CLI).

To return the mocked response, it’s easier if we create a separate Function for each endpoint, since we can avoid adding unnecessary conditions. Fortunately, we can stick with JavaScript (Node.js), and starting with nearly the same base we used for contactForm7Response:

function main(event) { const body = {}; return { body }; }

We must name the handler function main, which is invoked when the endpoint is called. The function receives the event object as its first argument, containing the details of the request. Once again, we could return anything, but to return the JSON response we need, it’s enough to simply return an object.

We can reuse the same code for creating the response as-is. The only difference is that we have to extract the form input data from the event as FormData ourselves:

function main(event) { // How do we get the FormData from the event? const formData = new FormData(); const submissionSuccess = { // ... }; const submissionValidationFailed = { // ... }; if (!formData.get("somebodys-name")) { submissionValidationFailed.invalid_fields.push({ // ... }); } // Or a more thorough way to check the validity of an email address if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.get("any-email"))) { submissionValidationFailed.invalid_fields.push({ // ... }); } // The rest of the validations... const body = !submissionValidationFailed.invalid_fields.length ? submissionSuccess : submissionValidationFailed; return { body }; }

As far as converting the data, serverless functions typically expect JSON inputs, so for other data types an extra parsing step is required. As it happens, the forms in the CodePen demos are submitted as multipart/form-data.

Without any libraries, we can convert a multipart/form-data string into a FormData by taking advantage of the Response API’s capabilities:

async function convertMultipartFormDataToFormData(data) { const matches = data.match(/^\s*--(\S+)/); if (!matches) { return new FormData(); } const boundary = matches[1]; return new Response(data, { headers: { "Content-Type": `multipart/form-data; boundary=${boundary}` } }).formData(); }

The code is mostly focused on extracting the boundary variable. This is typically autogenerated, for example, when submitting a form in a browser.

The submitted raw data is available via event.http.body, but since it’s base64-encoded, we need to decode it first:

async function main(event) { const formData = await convertMultipartFormDataToFormData( Buffer.from(event?.http?.body ?? "", "base64").toString("utf8") ); // ... const body = !submissionValidationFailed.invalid_fields.length ? submissionSuccess : submissionValidationFailed; return { body }; }

And that’s it. With this approach, all that’s left is to replace calls to the original APIs with calls to the mocked ones.

Closing thoughts

Ultimately, both approaches help decouple the demos from the third-party API dependency. In terms of effort, at least for this specific example, they seem comparable.

It’s hard to beat the fact that there’s no external dependency with the manual mocking approach, not even on something we somewhat control, and everything is bundled together. In general, without knowing specific details, there are good reasons to favor this approach for small, self-contained demos.

But using a mocked server API also has its advantages. A mocked server API can power not only demos, but also various types of tests. For more complex needs, a dedicated team working on the mocked server might prefer a different programming language than JavaScript, or they might opt to use a tool like WireMock instead of starting from scratch.

As with everything, it depends. There are many criteria to consider beyond what I’ve just mentioned.

I also don’t think this approach necessarily needs to be applied by default. After all, I had the CodePen demos working for four years without any issues.

The important part is having a way to know when demos break (monitoring), and when they do, having the right tools at our disposal to handle the situation.

Keeping Article Demos Alive When Third-Party APIs Die originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Making a Masonry Layout That Works Today

Css Tricks - Mon, 07/28/2025 - 2:42am

Many CSS experts have weighed heavily on possible syntaxes for a new masonry layout feature last year. There were two main camps and a third camp that strikes a balance between the two:

  1. Use display: masonry
  2. Use grid-template-rows: masonry
  3. Use item-pack: collapse

I don’t think they’ve came up with a resolution yet. But you might want to know that Firefox already supports the masonry layout with the second syntax. And Chrome is testing it with the first syntax. While it’s cool to see native support for CSS Masonry evolving, we can’t really use it in production if other browsers don’t support the same implementation…

So, instead of adding my voice to one of those camps, I went on to figure out how make masonry work today with other browsers. I’m happy to report I’ve found a way — and, bonus! — that support can be provided with only 66 lines of JavaScript.

In this article, I’m gonna show you how it works. But first, here’s a demo for you to play with, just to prove that I’m not spewing nonsense. Note that there’s gonna be a slight delay since we’re waiting for an image to load first. If you’re placing a masonry at the top fold, consider skipping including images because of this!

Anyway, here’s the demo:

CodePen Embed Fallback What in the magic is this?!

Now, there are a ton of things I’ve included in this demo, even though there are only 66 lines of JavaScript:

  • You can define the masonry with any number of columns.
  • Each item can span multiple columns.
  • We wait for media to load before calculating the size of each item.
  • We made it responsive by listening to changes with the ResizeObserver.

These make my implementation incredibly robust and ready for production use, while also way more flexible than many Flexbox masonry knockoffs out there on the interwebs.

Now, a hot tip. If you combine this with Tailwind’s responsive variants and arbitrary values, you can include even more flexibility into this masonry grid without writing more CSS.

Okay, before you get hyped up any further, let’s come back to the main question: How the heck does this work?

Let’s start with a polyfill

Firefox already supports masonry layouts via the second camp’s syntax. Here’s the CSS you need to create a CSS masonry grid layout in Firefox.

.masonry { display: grid; grid-template-columns: repeat( auto-fit, minmax(min(var(--item-width, 200px), 100%), 1fr) ); grid-template-rows: masonry; grid-auto-flow: dense; /* Optional, but recommended */ }

Since Firefox already has native masonry support, naturally we shouldn’t mess around with it. The best way to check if masonry is supported by default is to check if grid-template-rows can hold the masonry value.

function isMasonrySupported(container) { return getComputedStyle(container).gridTemplateRows === 'masonry' }

If masonry is supported, we’ll skip our implementation. Otherwise, we’ll do something about it.

const containers = document.querySelectorAll('.masonry') containers.forEach(async container => { if (isMasonrySupported(container)) return }) Masonry layout made simple

Now, I want to preface this segment that I’m not the one who invented this technique.

I figured out this technique when I was digging through the web, searching for possible ways to implement a masonry grid today. So kudos goes to the unknown developer who developed the idea first — and perhaps me for understanding, converting, and using it.

The technique goes like this:

  1. We set grid-auto-rows to 0px.
  2. Then we set row-gap to 1px.
  3. Then we get the item’s height through getBoundingClientRect.
  4. We then size the item’s “row allocation” by adding the height the column-gap value together.

This is really unintuitive if you’ve been using CSS Grid the standard way. But once you get this, you can also grasp how this works!

Now, because this is so unintuitive, we’re gonna take things step-by-step so you see how this whole thing evolves into the final output.

Step by step

First, we set grid-auto-rows to 0px. This is whacky because every grid item will effectively have “zero height”. Yet, at the same time, CSS Grid maintains the order of the columns and rows!

containers.forEach(async container => { // ... container.style.gridAutoRows = '0px' })

Second, we set row-gap to 1px. Once we do this, you begin to notice an initial stacking of the rows, each one one pixel below the previous one.

containers.forEach(async container => { // ... container.style.gridAutoRows = '0px' container.style.setProperty('row-gap', '1px', 'important') })

Third, assuming there are no images or other media elements in the grid items, we can easily get the height of each grid item with getBoundingClientRect.

We can then restore the “height” of the grid item in CSS Grid by substituting grow-row-end with the height value. This works because each row-gap is now 1px tall.

When we do this, you can see the grid beginning to take shape. Each item is now (kinda) back at their respective positions:

containers.forEach(async container => { // ... let items = container.children layout({ items }) }) function layout({ items }) { items.forEach(item => { const ib = item.getBoundingClientRect() item.style.gridRowEnd = `span ${Math.round(ib.height)}` }) }

We now need to restore the row gap between items. Thankfully, since masonry grids usually have the same column-gap and row-gap values, we can grab the desired row gap by reading column-gap values.

Once we do that, we add it to grid-row-end to expand the number of rows (the “height”) taken up by the item in the grid:

containers.forEach(async container => { // ... const items = container.children const colGap = parseFloat(getComputedStyle(container).columnGap) layout({ items, colGap }) }) function layout({ items, colGap }) { items.forEach(item => { const ib = item.getBoundingClientRect() item.style.gridRowEnd = `span ${Math.round(ib.height + colGap)}` }) }

And, just like that, we’ve made the masonry grid! Everything from here on is simply to make this ready for production.

Waiting for media to load

Try adding an image to any grid item and you’ll notice that the grid breaks. That’s because the item’s height will be “wrong”.

It’s wrong because we took the height value before the image was properly loaded. The DOM doesn’t know the dimensions of the image yet. To fix this, we need to wait for the media to load before running the layout function.

We can do this with the following code (which I shall not explain since this is not much of a CSS trick &#x1f605;):

containers.forEach(async container => { // ... try { await Promise.all([areImagesLoaded(container), areVideosLoaded(container)]) } catch(e) {} // Run the layout function after images are loaded layout({ items, colGap }) }) // Checks if images are loaded async function areImagesLoaded(container) { const images = Array.from(container.querySelectorAll('img')) const promises = images.map(img => { return new Promise((resolve, reject) => { if (img.complete) return resolve() img.onload = resolve img.onerror = reject }) }) return Promise.all(promises) } // Checks if videos are loaded function areVideosLoaded(container) { const videos = Array.from(container.querySelectorAll('video')) const promises = videos.map(video => { return new Promise((resolve, reject) => { if (video.readyState === 4) return resolve() video.onloadedmetadata = resolve video.onerror = reject }) }) return Promise.all(promises) }

Voilà, we have a CSS masnory grid that works with images and videos!

Making it responsive

This is a simple step. We only need to use the ResizeObserver API to listen for any change in dimensions of the masonry grid container.

When there’s a change, we run the layout function again:

containers.forEach(async container => { // ... const observer = new ResizeObserver(observerFn) observer.observe(container) function observerFn(entries) { for (const entry of entries) { layout({colGap, items}) } } })

This demo uses the standard Resize Observer API. But you can make it simpler by using the refined resizeObserver function we built the other day.

containers.forEach(async container => { // ... const observer = resizeObserver(container, { callback () { layout({colGap, items}) } }) })

That’s pretty much it! You now have a robust masonry grid that you can use in every working browser that supports CSS Grid!

Exciting, isn’t it? This implementation is so simple to use!

Masonry grid with Splendid Labz

If you’re not adverse to using code built by others, maybe you might want to consider grabbing the one I’ve built for you in Splendid Labz.

To do that, install the helper library and add the necessary code:

# Installing the library npm install @splendidlabz/styles /* Import all layouts code */ @import '@splendidlabz/layouts'; // Use the masonry script import { masonry } from '@splendidlabz/styles/scripts' masonry()

One last thing: I’ve been building a ton of tools to help make web development much easier for you and me. I’ve parked them all under the Splendid Labz brand — and one of these examples is this masonry grid I showed you today.

If you love this, you might be interested in other layout utilities that makes layout super simple to build.

Now, I hope you have enjoyed this article today. Go unleash your new CSS masonry grid if you wish to, and all the best!

Making a Masonry Layout That Works Today originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Prompt Building User Interfaces

LukeW - Sun, 07/27/2025 - 2:00pm

Perhaps the biggest problem facing AI products today is: people don't know all the things these products can do nor how to get the best results out of them. Not surprising when you consider most AI product interfaces are just empty text fields asking "what do you want to do?". Prompt building user interfaces can help answer that question and more.

We've been exploring ways to help people understand what's possible and how to accomplish it in Bench. Bench is AI for everyday work tasks. As such, it can do a lot: search the Web, browse the Web as you (with a browser extension), generate reports, make PowerPoint, use your email, and many more of the things that make up people's daily work tasks. The problem is... that's a lot.

To give people a better sense of what Bench can do, we started with suggested prompts (aka instructions) that accomplished specific work tasks. To make these as relevant as possible, we added an initial screen to the Bench start experience asking people to specify their primary roles at work: Engineering, Design, Sales, etc. If they did, the suggested prompts would be reflective of the kinds of things they might do at work. For example Sales folks would see suggestions like: research a prospect, prep for a sales meeting, summarize customer feedback, and so on.

The problem with these kinds of high level suggestions is they are exactly that: too high level. Though relevant to a role, they're not relevant to someone's current work tasks. Sales teams are researching prospects but doing it in a way that's specific to the product they're selling and the prospect they're researching. Generic prompt suggestions aren't that useful.

To account for this, we attempted to personalize the role-based suggestions by researching people's companies in the background while they signed up. This additional information allowed us to make suggestions more specific to the industry and company people worked for. This definitely made suggested prompts more specific, but it also made them less useful. Researching someone's company gives you some context but not nearly the amount its employees have. Because of this, personalized suggested prompts felt "off". So we went back to more generic suggestions but made them more atomic.

Instead of encompassing a complete work task, atomic suggestions just focused on part of it: where the information for a work task was coming from (look at my Gmail, search my Notion) and what the output of a work task should be (create a Word Doc, make a chart). These suggestions gave people a better sense of Bench's capabilities. It can read my calendar, it can make Google sheets. Almost immediately, though, it felt like these atomic suggestions should be combine-able.

To enable this, we made a prompt rewriter that would change based on what atomic suggestions people chose. If they picked Use Salesforce and Create Google Doc, the rewriter would merge these into a single instruction that made sense "Use [variable] from Salesforce to create a Google Doc". This turned the process of writing complex prompts into just clicking suggestions. The way these suggestions were laid out, however, didn't make clear they could be combined like this. They looked and felt like discrete prompts.

Enter the task builder. In the latest version of Bench, atomic suggestions have been expanded and laid out more like the building blocks of a prompt. People can either select what they want to do, use, make, or any combination of the three. The prompt rewriter then stitches together a machine-written prompt along with some optional inputs field people can fill in to provide more details about the work task they want to get done.

This prompt builder UI does a few things for people using Bench. It:

  • makes what the product can do clearer
  • provides a way to surface new functionality as it's added to the product
  • rewrites people's prompts in a way that gets them to better outcomes
  • clarifies what people can add to a prompt to make their tasks more effective

While that's a decent amount of good outcomes, design is never done and AI capabilities keep improving. As a result, I'm sure we're not done with not only Bench's task builder UI but solutions to discoverability and prompting in AI products overall. In other words... more to come.

Prompt Building User Interfaces

LukeW - Sun, 07/27/2025 - 2:00pm

Perhaps the biggest problem facing AI products today is: people don't know all the things these products can do nor how to get the best results out of them. Not surprising when you consider most AI product interfaces are just empty text fields asking "what do you want to do?". Prompt building user interfaces can help answer that question and more.

We've been exploring ways to help people understand what's possible and how to accomplish it in Bench. Bench is AI for everyday work tasks. As such, it can do a lot: search the Web, browse the Web as you (with a browser extension), generate reports, make PowerPoint, use your email, and many more of the things that make up people's daily work tasks. The problem is... that's a lot.

To give people a better sense of what Bench can do, we started with suggested prompts (aka instructions) that accomplished specific work tasks. To make these as relevant as possible, we added an initial screen to the Bench start experience asking people to specify their primary roles at work: Engineering, Design, Sales, etc. If they did, the suggested prompts would be reflective of the kinds of things they might do at work. For example Sales folks would see suggestions like: research a prospect, prep for a sales meeting, summarize customer feedback, and so on.

The problem with these kinds of high level suggestions is they are exactly that: too high level. Though relevant to a role, they're not relevant to someone's current work tasks. Sales teams are researching prospects but doing it in a way that's specific to the product they're selling and the prospect they're researching. Generic prompt suggestions aren't that useful.

To account for this, we attempted to personalize the role-based suggestions by researching people's companies in the background while they signed up. This additional information allowed us to make suggestions more specific to the industry and company people worked for. This definitely made suggested prompts more specific, but it also made them less useful. Researching someone's company gives you some context but not nearly the amount its employees have. Because of this, personalized suggested prompts felt "off". So we went back to more generic suggestions but made them more atomic.

Instead of encompassing a complete work task, atomic suggestions just focused on part of it: where the information for a work task was coming from (look at my Gmail, search my Notion) and what the output of a work task should be (create a Word Doc, make a chart). These suggestions gave people a better sense of Bench's capabilities. It can read my calendar, it can make Google sheets. Almost immediately, though, it felt like these atomic suggestions should be combine-able.

To enable this, we made a prompt rewriter that would change based on what atomic suggestions people chose. If they picked Use Salesforce and Create Google Doc, the rewriter would merge these into a single instruction that made sense "Use [variable] from Salesforce to create a Google Doc". This turned the process of writing complex prompts into just clicking suggestions. The way these suggestions were laid out, however, didn't make clear they could be combined like this. They looked and felt like discrete prompts.

Enter the task builder. In the latest version of Bench, atomic suggestions have been expanded and laid out more like the building blocks of a prompt. People can either select what they want to do, use, make, or any combination of the three. The prompt rewriter then stitches together a machine-written prompt along with some optional inputs field people can fill in to provide more details about the work task they want to get done.

This prompt builder UI does a few things for people using Bench. It:

  • makes what the product can do clearer
  • provides a way to surface new functionality as it's added to the product
  • rewrites people's prompts in a way that gets them to better outcomes
  • clarifies what people can add to a prompt to make their tasks more effective

While that's a decent amount of good outcomes, design is never done and AI capabilities keep improving. As a result, I'm sure we're not done with not only Bench's task builder UI but solutions to discoverability and prompting in AI products overall. In other words... more to come.

AI Has Flipped Software Development

LukeW - Sat, 07/26/2025 - 2:00pm

For years, it's been faster to create mockups and prototypes of software than to ship it to production. As a result, software design teams could stay "ahead" of engineering. Now AI coding agents make development 10x faster, flipping the traditional software development process on its head.

In my thirty years of working on software, the design teams I was part of were typically operating "out ahead" of our software development counterparts. Unburdened by existing codebases, technical debt, performance, and infrastructure limitations, designers could work quickly in mockups, wireframes, and even prototypes to help envision what we could or should build before time and effort was invested into actually building it.

While some software engineering teams could ship in days, in most (especially larger) organizations, building new features or redesigning apps could take months if not quarters or years. So there was plenty of time for designers to explore and iterate. This was also reflected in the ratio of designers to developers in most companies: an average of one designer for every twenty engineers.

When designs did move to the production engineering phase, there'd (hopefully) be a bunch of back and forth to resolve unanswered questions, new issues that came up, or changing requirements. A lot of this burden fell on engineering as they encountered edge cases, things missing in specs, cross-device capability differences, and more. What it added up to though, was that the process to build and launch something often took longer than the process to design it.

AI coding tools change this dynamic. Across several of our companies, software development teams are now "out ahead" of design. To be more specific, collaborating with AI agents (like Augment Code) allows software developers to move from concept to working code 10x faster. This means new features become code at a fast and furious pace.

When software is coded this way, however, it (currently at least) lacks UX refinement and thoughtful integration into the structure and purpose of a product. This is the work that designers used to do upfront but now need to "clean up" afterward. It's like the development process got flipped around. Designers used to draw up features with mockups and prototypes, then engineers would have to clean them up to ship them. Now engineers can code features so fast that designers are ones going back and cleaning them up.

So scary time to be a designer? No. Awesome time to be a designer. Instead of waiting for months, you can start playing with working features and ideas within hours. This allows everyone, whether designer or engineer, an opportunity to learn what works and what doesn’t. At its core rapid iteration improves software and the build, use/test, learn, repeat loop just flipped, it didn't go away.

In his Designing Perplexity talk at Sutter Hill Ventures, Henry Modisett described this new state as "prototype to productize" rather than "design to build". Sounds right to me.

AI Has Flipped Software Development

LukeW - Sat, 07/26/2025 - 2:00pm

For years, it's been faster to create mockups and prototypes of software than to ship it to production. As a result, software design teams could stay "ahead" of engineering. Now AI coding agents make development 10x faster, flipping the traditional software development process on its head.

In my thirty years of working on software, the design teams I was part of were typically operating "out ahead" of our software development counterparts. Unburdened by existing codebases, technical debt, performance, and infrastructure limitations, designers could work quickly in mockups, wireframes, and even prototypes to help envision what we could or should build before time and effort was invested into actually building it.

While some software engineering teams could ship in days, in most (especially larger) organizations, building new features or redesigning apps could take months if not quarters or years. So there was plenty of time for designers to explore and iterate. This was also reflected in the ratio of designers to developers in most companies: an average of one designer for every twenty engineers.

When designs did move to the production engineering phase, there'd (hopefully) be a bunch of back and forth to resolve unanswered questions, new issues that came up, or changing requirements. A lot of this burden fell on engineering as they encountered edge cases, things missing in specs, cross-device capability differences, and more. What it added up to though, was that the process to build and launch something often took longer than the process to design it.

AI coding tools change this dynamic. Across several of our companies, software development teams are now "out ahead" of design. To be more specific, collaborating with AI agents (like Augment Code) allows software developers to move from concept to working code 10x faster. This means new features become code at a fast and furious pace.

When software is coded this way, however, it (currently at least) lacks UX refinement and thoughtful integration into the structure and purpose of a product. This is the work that designers used to do upfront but now need to "clean up" afterward. It's like the development process got flipped around. Designers used to draw up features with mockups and prototypes, then engineers would have to clean them up to ship them. Now engineers can code features so fast that designers are ones going back and cleaning them up.

So scary time to be a designer? No. Awesome time to be a designer. Instead of waiting for months, you can start playing with working features and ideas within hours. This allows everyone, whether designer or engineer, an opportunity to learn what works and what doesn’t. At its core rapid iteration improves software and the build, use/test, learn, repeat loop just flipped, it didn't go away.

In his Designing Perplexity talk at Sutter Hill Ventures, Henry Modisett described this new state as "prototype to productize" rather than "design to build". Sounds right to me.

How to Discover a CSS Trick

Css Tricks - Fri, 07/25/2025 - 3:48am

Do we invent or discover CSS tricks? Michelangelo described his sculpting process as chiseling away superfluous material to reveal the sculpture hidden inside the marble, and Stephen King says his ideas are pre-existing things he locates and uncovers “like fossils in the ground.” Paragraph one is early for me to get pretentious enough to liken myself to those iconic creative forces, but my work on CSS-Tricks feels like “discovering,” not “inventing,” secret synergies between CSS features, which have been eyeing each other from disparate sections of the MDN web docs and patiently waiting for someone to let them dance together in front of the world.

Matchmaking for CSS features

A strategy for finding unexpected alliances between CSS features to achieve the impossible is recursive thinking, which I bring to the CSS world from my engineering background. When you build recursive logic, you need to find an escape hatch to avoid infinite recursion, and this inception-style mindset helps me identify pairings of CSS features that seem at odds with each other yet work together surprisingly well. Take these examples from my CSS experiments:

Accepting there is nothing new under the sun

Indeed, Mark Twain thought new ideas don’t exist — he described them as illusions we create by combining ideas that have always existed, turning and overlaying them in a “mental kaleidoscope” to “make new and curious combinations.” It doesn’t mean creating is easy. No more than a safe can be cracked just by knowing the possible digits.

This brings back memories of playing Space Quest III as a kid because after you quit the game, it would output smart-aleck command-line messages, one of which was: “Remember, we did it all with ones and zeros.” Perhaps the point of the mock inspirational tone is that we likely will not be able to sculpt like Michelangelo or make a bestselling game, even if we were given the same materials and tools (is this an inspirational piece or what?). However, understanding the limits of what creators do is the foundation for cracking the combination of creativity to open the door to somewhere we haven’t been. And one truth that helps with achieving magic with CSS is that its constraints help breed creativity.

Embracing limitations

Being asked “Why would you do that in CSS when you could just use JavaScript?” is like if you asked me: “Why would you write a poem when it’s easier to write prose?” Samuel Coleridge defined prose as “words in their best order,” but poetry as “the best words in the best order.” If you think about it, the difference between prose and poetry is that the latter is based on increased constraints, which force us to find unexpected connections between ideas.

Similarly, the artist Phil Hansen learned that embracing limitation could drive creativity after he suffered permanent nerve damage in his hand, causing it to jitter, which prevented him from drawing the way he had in the past. His early experiments using this new mindset included limiting himself to creating a work using only 80 cents’ worth of supplies. This dovetails with the quote from Antoine de Saint-Exupéry often cited in web design, which says that perfection is achieved when there is nothing left to take away.

Embracing nothingness

The interesting thing about web design is how much it blends art and science. In both art and science, we challenge assumptions about whether commonsense relationships of cause and effect truly exist. Contrary to the saying in vernacular that “you can’t prove a negative,” we can. It’s not necessarily harder than proving a positive. So, in keeping with the discussion above of embracing limitations and removing the superfluous until a creation reveals itself, many of my article ideas prove a negative by challenging the assumption that one thing is necessary to produce another.

Going to extremes

Sometimes we can make a well-worn idea new again by taking it to the extreme. Seth Godin coined the term “edgecraftto describe a technique for generating ideas by pushing a competitive advantage as far to the edge as the market dares us to go. Similarly, sometimes you can take an old CSS feature that people have seen before, but push it further than anyone else to create something unique. For example:

  • CSS-Tricks covered checkbox hacks and radio button hacks back in 2011. But in 2021, I decided to see if I could use hundreds of radio button hacks using HTML generated with Pug to create a working Sudoku app. At one point, I found out that Chrome dev tools can display an infinite spinner of death when you throw too much generated CSS at it, which meant I had to limit myself to a 4×4 Sudoku, but that taught me more about what CSS can do and what it can’t.
  • The :target selector has existed since the 2000s. But in 2024, I took it to the extreme by using HAML to render the thousands of possible states of Tic Tac Toe to create a game with a computer opponent in pure CSS. At one point, CodePen refused to output as much HTML as I had asked it to, but it’s a fun way for newcomers to learn an important CSS feature; more engaging in my opinion than a table of contents demo.
Creating CSS outsider art

Chris Coyier has written about his distaste for the gatekeeping agenda hidden behind the question of whether CSS is a programming language. If CSS isn’t deemed as “real” programming, that can be used as an excuse to hold CSS experts in less esteem than people who code in imperative languages, which leads to unfair pay and toxic workplace dynamics.

But maybe the other side always seems greener due to the envy radiating from the people on that side, because as a full-stack engineer who completed a computer science degree, I always felt left out of the front-end conversations. It didn’t feel right to put “full stack developer” on my résumé when the creation of everything users can see in a web app seemed mysterious to me.

And maybe it wasn’t just psychosomatic that CSS made my head hurt compared to other types of coding because research indicates if you do fMRIs on people who are engaged in design tasks, you see that design cognition appears to involve a unique cognitive profile compared to conventional problem-solving, reflected in the areas of the brain that light up on the fMRIs. Studies show that the brain’s structure changes as people get better at different types of jobs. The brain’s structural plasticity is reminiscent of the way different muscles grow more pronounced with different types of exercise, but achieving what some of my colleagues could with CSS when my brain had been trained for decades on imperative logic felt about as approachable as lifting a car over my head.

The intimidation I felt from CSS started to change when I learned about the checkbox hack because I could relate to hiding and showing divs based on checkboxes, which was routine in my work in the back of the front-end. My designer workmate challenged me to make a game in one night using just CSS. I came up with a pure text adventure game made out of radio button hacks. Since creative and curious people are more sensitive to novel stimuli, the design experts on my team were enthralled by my primitive demo, not because it was cutting-edge gameplay but because it was something they had never seen before. My engineering background was now an asset rather than a hindrance in the unique outsider perspective I could bring to the world of CSS. I was hooked.

The hack I found to rewire my brain to become more CSS-friendly was to find analogies in CSS to the type of problem-solving I was more familiar with from imperative programming:

So if you are still learning web development and CSS (ultimately, we are all still learning), instead of feeling imposter syndrome, consider that the very thing that makes you feel like an outsider could be what enables you to bring something unique to your usage of CSS.

Finding the purpose

Excited as I was when my CSS hacking ended up providing the opportunity to publish my experiments on CSS-Tricks, the first comment on the first hack I published on CSS-Tricks was a generic, defeatist “Why would you do that?” criticism. The other comments popped up and turned out to be more supportive, and I said in a previous article that I’ve made my peace with the fact that not everybody will like my articles. However, this is the second article in which I’ve brought up the critical comment from back in 2021. Hmm…

Surely it wasn’t the reason I didn’t write another CSS-Tricks article for years. And it’s probably a coincidence that when I returned to CSS-Tricks last year, my first new article was a CSS hack that lends itself to accessibility after the person who left the negative comment about my first article seemed to have a bee in their bonnet about checkbox hacks breaking accessibility, even in fun CSS games not intended for production. Then again, limiting myself to CSS hacking that enables accessibility became a source of inspiration. We can all do with a reminder to at all times empathize with users who require screen readers, even when we are doing wacky experimental stuff, because we need to embrace the limitations not just of CSS but of our audience.

I suppose the reason the negative comment continues to rankle with me is that I agree that clarifying the relevance and purpose of a CSS trick is important. And yet, if I’m right in saying a CSS trick is more like something we discover than something we make, then it’s like finding a beautiful feather when we go for a walk. At first, we pick it up just because we can, but if I bring you with me on the journey that led to the discovery, then you can help me decide whether the significance is that the feather we discovered makes a great quill or reveals that a rare species of bird lives in this region.

It’s a journey versus destination thing to share the failures that led to compromises and the limitations I came up against when pushing the boundaries of CSS. When I bring you along on the route to the curious item I found, rather than just showing you that item, then after we part ways, you might retrace the steps and try a different fork in the path we followed, which could lead you to discover your own CSS trick.

How to Discover a CSS Trick originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Atomic Design Certification Course

Css Tricks - Thu, 07/24/2025 - 6:16am

Brad Frost introduced the “Atomic Design” concept wayyyy back in 2013. He even wrote a book on it. And we all took notice, because that term has been part of our lexicon ever since.

It’s a nice way to divide web designs into separate layers of concern, leaning into biology terms to help scope their context by size:

  1. Atoms
  2. Molecules
  3. Organisms
  4. Templates
  5. Pages

Atoms are part of molecules, which are part of organisms, which make up templates, which become full-blown pages. It’s composable design that’s centered on consistency, reusability, and maintainability. Beautiful. We’ve covered this a bunch over the years.

Want to get fully versed in how it works? If so, you’re in luck because Brad and his brother, Ian, are in the process of publishing an entire online course about Atomic Design. It’s in presale for $50 (with discounts for teams).

Normally, I like to jump into a course and check it out before sharing it. But this is Brad and all he does is wonderful things. For example:

Oh, and his newsletter is pretty awesome, too. And I’m sure I’m leaving out more things he has floating around the web, but you get the point: he’s incredibly knowledgeable on the topic, is a highly-effective educator and speaker, and most importantly, has an infectious positive posture about him.

I know the Atomic Design course will be just as awesome. Preordered!

Atomic Design Certification Course originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

A Primer on Focus Trapping

Css Tricks - Mon, 07/21/2025 - 3:14am

Focus trapping is a term that refers to managing focus within an element, such that focus always stays within it:

  • If a user tries to tab out from the last element, we return focus to the first one.
  • If the user tries to Shift + Tab out of the first element, we return focus back to the last one.

This whole focus trap thing is used to create accessible modal dialogs since it’s a whole ‘nother trouble to inert everything else — but you don’t need it anymore if you’re building modals with the dialog API (assuming you do it right).

Anyway, back to focus trapping.

The whole process sounds simple in theory, but it can quite difficult to build in practice, mostly because of the numerous parts to you got to manage.

Simple and easy focus trapping with Splendid Labz

If you are not averse to using code built by others, you might want to consider this snippet with the code I’ve created in Splendid Labz.

The basic idea is:

  1. We detect all focusable elements within an element.
  2. We manage focus with a keydown event listener.
import { getFocusableElements, trapFocus } from '@splendidlabz/utils/dom' const dialog = document.querySelector('dialog') // Get all focusable content const focusables = getFocusableElements(node) // Traps focus within the dialog dialog.addEventListener('keydown', event => { trapFocus({ event, focusables }) })

The above code snippet makes focus trapping extremely easy.

But, since you’re reading this, I’m sure you wanna know the details that go within each of these functions. Perhaps you wanna build your own, or learn what’s going on. Either way, both are cool — so let’s dive into it.

Selecting all focusable elements

I did research when I wrote about this some time ago. It seems like you could only focus an a handful of elements:

  • a
  • button
  • input
  • textarea
  • select
  • details
  • iframe
  • embed
  • object
  • summary
  • dialog
  • audio[controls]
  • video[controls]
  • [contenteditable]
  • [tabindex]

So, the first step in getFocusableElements is to search for all focusable elements within a container:

export function getFocusableElements(container = document.body ) { return { get all () { const elements = Array.from( container.querySelectorAll( `a, button, input, textarea, select, details, iframe, embed, object, summary, dialog, audio[controls], video[controls], [contenteditable], [tabindex] `, ), ) } } }

Next, we want to filter away elements that are disabled, hidden or set with display: none, since they cannot be focused on. We can do this with a simple filter function.

export function getFocusableElements(container = document.body ) { return { get all () { // ... return elements.filter(el => { if (el.hasAttribute('disabled')) return false if (el.hasAttribute('hidden')) return false if (window.getComputedStyle(el).display === 'none') return false return true }) } } }

Next, since we want to trap keyboard focus, it’s only natural to retrieve a list of keyboard-only focusable elements. We can do that easily too. We only need to remove all tabindex values that are less than 0.

export function getFocusableElements(container = document.body ) { return { get all () { /* ... */ }, get keyboardOnly() { return this.all.filter(el => el.tabIndex > -1) } } }

Now, remember that there are two things we need to do for focus trapping:

  • If a user tries to tab out from the last element, we return focus to the first one.
  • If the user tries to Shift + Tab out of the first element, we return focus back to the last one.

This means we need to be able to find the first focusable item and the last focusable item. Luckily, we can add first and last getters to retrieve these elements easily inside getFocusableElements.

In this case, since we’re dealing with keyboard elements, we can grab the first and last items from keyboardOnly:

export function getFocusableElements(container = document.body ) { return { // ... get first() { return this.keyboardOnly[0] }, get last() { return this.keyboardOnly[0] }, } }

We have everything we need — next is to implement the focus trapping functionality.

How to trap focus

First, we need to detect a keyboard event. We can do this easily with addEventListener:

const container = document.querySelector('.some-element') container.addEventListener('keydown', event => {/* ... */})

We need to check if the user is:

  • Pressing tab (without Shift)
  • Pressing tab (with Shift)

Splendid Labz has convenient functions to detect these as well:

import { isTab, isShiftTab } from '@splendidlabz/utils/dom' // ... container.addEventListener('keydown', event => { if (isTab(event)) // Handle Tab if (isShiftTab(event)) // Handle Shift Tab /* ... */ })

Of course, in the spirit of learning, let’s figure out how to write the code from scratch:

  • You can use event.key to detect whether the Tab key is being pressed.
  • You can use event.shiftKey to detect if the Shift key is being pressed

Combine these two, you will be able to write your own isTab and isShiftTab functions:

export function isTab(event) { return !event.shiftKey && event.key === 'Tab' } export function isShiftTab(event) { return event.shiftKey && event.key === 'Tab' }

Since we’re only handling the Tab key, we can use an early return statement to skip the handling of other keys.

container.addEventListener('keydown', event => { if (event.key !== 'Tab') return if (isTab(event)) // Handle Tab if (isShiftTab(event)) // Handle Shift Tab /* ... */ })

We have almost everything we need now. The only thing is to know where the current focused element is at — so we can decide whether to trap focus or allow the default focus action to proceed.

We can do this with document.activeElement.

Going back to the steps:

  • Shift focus if user Tab on the last item
  • Shift focus if the user Shift + Tab on the first item

Naturally, you can tell that we need to check whether document.activeElement is the first or last focusable item.

container.addEventListener('keydown', event => { // ... const focusables = getFocusableElements(container) const first = focusables.first const last = focusables.last if (document.activeElement === last && isTab(event)) { // Shift focus to the first item } if (document.activeElement === first && isShiftTab(event)) { // Shift focus to the last item } })

The final step is to use focus to bring focus to the item.

container.addEventListener('keydown', event => { // ... if (document.activeElement === last && isTab(event)) { first.focus() } if (document.activeElement === first && isShiftTab(event)) { last.focus() } })

That’s it! Pretty simple if you go through the sequence step-by-step, isn’t it?

Final callout to Splendid Labz

As I resolve myself to stop teaching (so much) and begin building applications, I find myself needing many common components, utilities, even styles.

Since I have the capability to build things for myself, (plus the fact that I’m super particular when it comes to good DX), I’ve decided to gather these things I find or build into a couple of easy-to-use libraries.

Just sharing these with you in hopes that they will help speed up your development workflow.

Thanks for reading my shameless plug. All the best for whatever you decide to code!

A Primer on Focus Trapping originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Designing Software for AI Agents

LukeW - Sun, 07/20/2025 - 2:00pm

From making apps, browsing the Web, to creating files, today's AI agents today can take on an increasing number of computing tasks on their own. But the software underlying these capabilities, wasn't made for agents. It was designed and built for people to use. As such there's an opportunity, and perhaps an increasing need, to rethink these systems for agent use.

When building agent-based AI applications, you'll likely butt up against a number of situations where existing software isn't optimized for what thinking machines can do. For instance, Web search. Nearly every agent-based AI application makes use of information on the Web to get things done. But Web Search APIs weren't written with agents in mind.

They provide a limited number of search results and a condensed snippet format that lines up more with how people use Web search interfaces. We get a page of ten blue links and scan them to decide which one to click. But AI agents aren't people. Not only can they make sense of many more search results at once, but their performance usually improves with larger document summaries and contents. People on the other hand, are unlikely to read through all search results before making a decision. So search APIs could certainly be rethought for agents.

Similarly, when agents are developing applications or collecting data, they can make use of databases. But once again databases were designed and built for people to use not AI agents. And once again they can be rethought for agents, which is what we did with our most recent launch: AgentDB.

Agents can (and do) produce 1000x more databases than people every day, so the process of spinning up and managing any database for an agent needs to be as easy and maintenance-free as possible. Most of the databases AI agents create will be short-lived after serving their initial purpose. But some databases will be used again and others still will be used regularly.

With this kind of volume costs can become an issue, so keeping that many databases available needs to be as cost effective as possible. Last but not least, the content of databases needs to work well as context for AI models so agents can use this data as part of their tasks.

AgentDB is a database system designed around these considerations. With AgentDB, creating a database only requires a Universally Unique Identifier (UUID). There's no setup or configuration step. So whenever an AI agent decides it needs a database, it has one simply by creating a UUID. No forms or set-up wizards involved.

Databases in AgentDB are stored as files not hosted services requiring compute and maintenance. If an AI agent needs to query a database or append to it, it can. But if it never needs to access it again, the database is just a file. That means you're only paying for the cost of storage to keep it around and because AgentDB databases are just files, they scale. Meaning they can easily keep up with the scale of AI agents.

To make data within each AgentDB database easily accessible as context for AI models, every AgentDB account is also an MCP server. This makes the data portable across AI applications as long as they support MCP server connections (which most do).

Altogether this example illustrates how even the most fundamental software infrastructure systems, like databases, can be rethought for the age of AI. The AgentDB database system doesn't look like a hosted database as a service solution because it's not designed and built for database admins and back-end developers. It's built for today's thinking machines.

And as agents take on more computing tasks for people, it won't be the only software made with agents as first class users.

Designing Software for AI Agents

LukeW - Sun, 07/20/2025 - 2:00pm

From making apps, browsing the Web, to creating files, today's AI agents today can take on an increasing number of computing tasks on their own. But the software underlying these capabilities, wasn't made for agents. It was designed and built for people to use. As such there's an opportunity, and perhaps an increasing need, to rethink these systems for agent use.

When building agent-based AI applications, you'll likely butt up against a number of situations where existing software isn't optimized for what thinking machines can do. For instance, Web search. Nearly every agent-based AI application makes use of information on the Web to get things done. But Web Search APIs weren't written with agents in mind.

They provide a limited number of search results and a condensed snippet format that lines up more with how people use Web search interfaces. We get a page of ten blue links and scan them to decide which one to click. But AI agents aren't people. Not only can they make sense of many more search results at once, but their performance usually improves with larger document summaries and contents. People on the other hand, are unlikely to read through all search results before making a decision. So search APIs could certainly be rethought for agents.

Similarly, when agents are developing applications or collecting data, they can make use of databases. But once again databases were designed and built for people to use not AI agents. And once again they can be rethought for agents, which is what we did with our most recent launch: AgentDB.

Agents can (and do) produce 1000x more databases than people every day, so the process of spinning up and managing any database for an agent needs to be as easy and maintenance-free as possible. Most of the databases AI agents create will be short-lived after serving their initial purpose. But some databases will be used again and others still will be used regularly.

With this kind of volume costs can become an issue, so keeping that many databases available needs to be as cost effective as possible. Last but not least, the content of databases needs to work well as context for AI models so agents can use this data as part of their tasks.

AgentDB is a database system designed around these considerations. With AgentDB, creating a database only requires a Universally Unique Identifier (UUID). There's no setup or configuration step. So whenever an AI agent decides it needs a database, it has one simply by creating a UUID. No forms or set-up wizards involved.

Databases in AgentDB are stored as files not hosted services requiring compute and maintenance. If an AI agent needs to query a database or append to it, it can. But if it never needs to access it again, the database is just a file. That means you're only paying for the cost of storage to keep it around and because AgentDB databases are just files, they scale. Meaning they can easily keep up with the scale of AI agents.

To make data within each AgentDB database easily accessible as context for AI models, every AgentDB account is also an MCP server. This makes the data portable across AI applications as long as they support MCP server connections (which most do).

Altogether this example illustrates how even the most fundamental software infrastructure systems, like databases, can be rethought for the age of AI. The AgentDB database system doesn't look like a hosted database as a service solution because it's not designed and built for database admins and back-end developers. It's built for today's thinking machines.

And as agents take on more computing tasks for people, it won't be the only software made with agents as first class users.

Getting Creative With Versal Letters

Css Tricks - Fri, 07/18/2025 - 6:12am

A while back, our man Geoff Graham treated us to a refresher on the CSS initial-letter property, but how can you style drop and initial caps to reflect a brand’s visual identity and help to tell its stories?

Here’s how I do it in CSS by combining ::first-letter and initial-letter with other unexpected properties, including border-image, and clip-path.

Patty Meltt is an up-and-coming country music sensation.

My brief: Patty Meltt is an up-and-coming country music sensation, and she needed a website to launch her new album. She wanted it to be distinctive-looking and memorable, so she called Stuff & Nonsense. Patty’s not real, but the challenges of designing and developing sites like hers are.

First, a drop cap recap. Chris Coyier wrote about drop caps several years ago. They are a decorative letter at the beginning of a paragraph, often spanning several lines of text. It’s a typographic flourish found in illuminated manuscripts and traditional book design, where it adds visual interest and helps guide a reader’s eye to where they should begin.

Study manuscripts from the Middle Ages onwards, and you’ll find hand-decorated illuminated capitals. The artists who made these initial letters were fabulously called “illuminators.” These medieval versals went beyond showing someone where to start reading; historiated letters also illustrated the stories, which was especially useful since most people in the Middle Ages couldn’t read.

A basic drop cap

On the web, drop caps can improve readability and reflect a brand’s visual identity.

A brief refresher on properties and values

In CSS, drop caps are created using the ::first-letter pseudo-element in combination with initial-letter. As you might expect, ::first-letter targets the very first letter of a block of text, enabling you to style it independently from the rest of a paragraph. The first number sets how many lines tall the letter appears, and the second controls its baseline alignment — that is, which line of text the bottom of the cap sits on.

p::first-letter { -webkit-initial-letter: 3 3; initial-letter: 3 3; }

Because browser support still varies, it’s common to include both the unprefixed and -webkit- prefixed properties for maximum compatibility. And speaking of browser support, it’s also sensible to wrap the initial-letter property inside an @supports CSS at-rule so we can check for browser support and provide a fallback, if needed:

@supports (initial-letter:2) or (-webkit-initial-letter:2) { p::first-letter { -webkit-initial-letter: 3 3; initial-letter: 3 3; } }

The initial-letter property automatically calculates the font size to match the number of lines a drop cap spans. On its own, this can make for quite a first impression. However, drop caps really start to come to life when you combine initial-letter with other CSS properties.

Tip: Interactive examples from this article are available in my lab.

Shadows Text shadows applied to first letters (live demo)

When I want to lift a drop cap off the page, I can add a single text-shadow. Shadows can be colourful and don’t have to be black. I created a full live demo you can check out.

p::first-letter { /* ... *// text-shadow: 6px 6px 0 #e6d5b3; }

But why use just one shadow when two hard-edged shadows will turn a cap into a classic graphic typographic element?

p::first-letter { /* ... */ text-shadow: -6px -6px 0 #7d6975, 6px 6px 0 #e6d5b3; } Examples showing unstyled, single text shadow, and two text shadows (live demo) Strokes A text shadow applied to a first letter (live demo)

The text-stroke property — shorthand for text-stroke-width and text-stroke-color — adds an outline to the centre of the text shape. It’s a Baseline feature and is now widely available. I can make the cap text transparent or colour it to match the page background.

p::first-letter { /* ... */ text-stroke: 5px #e6d5b3; } Backgrounds Solid and gradient backgrounds applied to first letters (live demo)

Adding a background is a simple way to start making a cap more decorative. I could start by adding a solid background-color.

p::first-letter { /* ... */ background-color: #97838f; }

To add a lighting effect, I could apply a conical, linear, or radial gradient background image (here’s a demo):

p::first-letter { /* ... */ background-color: #e6d5b3; background-image: linear-gradient(135deg,#c8b9c2 0%, #7d6975 50%); }

And even an image URL to use a bitmap or vector image as a background (and here’s that demo):

p::first-letter { /* ... */ background-color: #e6d5b3; background-image: url(...); background-size: cover; } Background images and a background clipped to text

Things become even more interesting by clipping a bitmap, gradient, or vector background image to the text while setting its colour to transparent. Now, the image will only appear inside the text space (demo).

p::first-letter { /* ... */ background-clip: text; color: transparent; } Borders Two examples of borders applied to first letters, one square and one rounded

You might think borders are boring, but there’s plenty you can do to make them look interesting. I could start by applying a solid border to surround the cap box (demo).

p::first-letter { /* ... */ border: 5px solid #e6d5b3; }

Then, I could apply border-radius to slightly round all its corners (demo).

p::first-letter { /* ... */ border-radius: 1rem; }

Or, I might round individual corners for a more interesting look (demo):

p::first-letter { /* ... */ border-top-left-radius: 3rem; border-bottom-right-radius: 3rem; } A border radius applied to the first letter, where the top-left and bottom-right edges are rounded (live demo)

And then there’s the border-image property, a powerful, yet often overlooked CSS tool. By slicing, repeating, and outsetting images, you can create intricate borders and decorative drop caps with minimal code.

A CSS border image applied to a first letter (live demo)

You can insert a bitmap or vector format image, or drop a CSS gradient into the border space:

p::first-letter { /* ... */ border-style: solid; border-width: 10px; border-image: conic-gradient(...) 1; } Clipping Clipping first letters

The clip-path property lets you define a custom shape that controls which parts of an element are visible and which are hidden. Instead of always showing a rectangular box, you can use clip-path to crop elements into circles, polygons, or even complex shapes defined with SVG paths. It’s an effective way to create visual effects like this right-facing arrow. Clipping the drop cap into an arrow shape isn’t just decorative — it reinforces direction and hierarchy, literally pointing readers to where the story begins. Here’s a demo of the following example.

p::first-letter { /* ... */ padding-inline: 1rem 2rem; background-color: #e6d5b3; clip-path: polygon(...); }

Or a glossy sticker shape cap, made by combining clip-path with a gradient background image and a text shadow (demo).

Transforms Two examples of transforming first letters, one rotated (demo) and one scaled (demo)

You can transform a drop cap independently from the rest of a paragraph by rotating, scaling, skewing, or translating it to make it feel more dynamic:

p::first-letter { /* ... */ margin-inline-end: 2.5em; transform: skew(20deg, 0deg); }

And with a little trial and error to arrive at the correct values, you could even flow the remaining paragraph text around the cap using the shape-outside property (demo):

p::first-letter { /* ... */ display: block; float: left; shape-outside: polygon(0 0, 0 200px, 250px 600px); shape-margin: 50px; transform: skew(20deg, 0deg) translateX(-60px); }

Drop caps don’t just help guide a reader’s eye to where they should begin; they also set the tone for what follows. A well-designed drop cap adds visual interest at the start of a block of text, drawing attention in a way that feels intentional and designed. Because it’s often the first element the reader sees, caps can carry a lot of visual weight, making them powerful tools for expressing a brand’s identity.

Designing for Patty Meltt

Patty Meltt wanted a website packed with design details. Every element added to a design is an opportunity to be expressive, and that includes her drop caps.

Her biography page is presentable, but we felt a focus on where someone should start reading was lacking.

Patty Meltt’s biography without a drop cap

From the selection of designs I showed her, she felt the sticker-style cap best suited her brand.

To implement it, first, I added a cursive typeface which matches her branding and contrasts with the rest of her typographic design:

p::first-letter { font-family: "Lobster Two", sans-serif; font-weight: 700; }

I changed the cap colour to match the page background and added a semi-transparent text shadow:

p::first-letter { /* ... */ color: #140F0A; text-shadow: 6px 6px 0 rgba(163,148, 117, .8); }

Next, I clipped the cap box to a visible area shaped like a sticker:

p::first-letter { /* ... */ clip-path: polygon(...); }

…before applying two background images — a noise-filled SVG and a radial gradient — that I blended using a background-blend-mode:

p::first-letter { /* ... */ background-image: url(img/cap-noise.svg), radial-gradient(circle, #e6d5b3 0%, #cdaa65 100%); background-blend-mode: soft-light, normal; } Patty Meltt’s biography with a stylsh new drop cap (demo)

The result is a drop cap that’s as stylish as cut-off jeans and a pair of gator-skinned boots.

Conclusion

Styling drop caps isn’t just about decoration — it’s about setting a tone, drawing readers in, and using every detail to express a brand’s voice. CSS has the tools to go beyond the default: gradients, textures, borders, and even complex shapes all help transform first letters into statements. So don’t waste the opportunities that drop caps give you. Make ’em sing.

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

Getting Clarity on Apple’s Liquid Glass

Css Tricks - Thu, 07/17/2025 - 3:23am

Folks have a lot to say about “liquid glass,” the design aesthetic that Apple introduced at WWDC 2025. Some love it, some hate it, and others jumped straight into seeing how to they could create it in CSS.

There’s a lot to love, hate, and experience with liquid glass. You can love the way content reflects against backgrounds. You can hate the poor contrast between foreground and background. And you can be eager to work with it. All of those can be true at the same time.

Image credit: Apple

I, for one, am generally neutral with things like this for that exact reason. I’m intrigued by liquid glass, but hold some concern about legibility, particularly as someone who already struggles with the legibility of Apple’s existing design system (notably in Control Center). And I love looking at the many and clever ways that devs have tried to replicate liquid glass in their own experiments.

So, I’m in the process of gathering notes on the topic as I wrap my head around this “new” (or not-so-new, depending on who’s talking) thing and figure out where it fits in my own work. These links are a choice selection of posts that I’ve found helpful and definitely not meant to be an exhaustive list of what’s out there.

WWDC Introduction

Always a good idea to start with information straight from the horse’s mouth.

In short:

  • It’s the first design system that is universally applied to all of Apple’s platforms, as opposed to a single platform like Apple’s last major overhaul, iOS 7.
  • It’s designed to refract light and dynamically react to user interactions.
  • By “dynamic” we’re referring to UI elements updating into others as the context changes, such as displaying additional controls. This sounds a lot like the Dynamic Island, supporting shape-shifting animations.
  • There’s a focus on freeing up space by removing hard rectangular edges, allowing UI elements to become part of the content and respond to context.

Apple also released a more in-depth video aimed at introducing liquid glass to designers and developers.

In short:

  • Liquid glass is an evolution of the “aqua” blue interface from macOS 10, the real-time introduced in iOS 7, the “fluidity” of iOS 10, the flexibility of the Dynamic Island, and the immersive interface of visionOS.
  • It’s a “digital meta-material” that dynamically bends and shapes light while moving fluidly like water.
  • It’s at least partially a response to hardware devices adopting deeper rounded corners.
  • Lensing: Background elements are bended and warped rather than scattering light as it’s been in previous designs. There’s gel-like feel to elements.
  • Translucence helps reveal what is underneath a control, such as a progress indicator you can scrub more precisely by seeing what is behind the surface.
  • Controls are persistent between views for establishing a relationship between controls and states. This reminds me of the View Transition API.
  • Elements automatically adapt to light and dark modes.
  • Liquid glass is composed of layers: highlight (light casting and movement), shadow (added depth for separation between foreground and background), and illumination (the flexible properties of the material).
  • It is not meant to be used everywhere but is most effective for the navigation layer. And avoid using glass on glass.
  • There are two variants: regular (most versatile) and clear (does not have adaptive behaviors for allowing content to be more visible below the surface).
  • Glass can be tinted different colors.
Documentation

Right on cue, Apple has already made a number of developer resources available for using and implementing liquid glass that are handy references.

‘Beautiful’ and ‘Hard to Read’: Designers React to Apple’s Liquid Glass Update

This Wired piece is a nice general overview of what liquid glass is and context about how it was introduced at WWDC 2025. I like getting a take on this from a general tech perspective as opposed to, say, someone’s quick hot take. It’s a helpful pulse on what’s happening from a high level without a bunch of hyperbole, setting the stage for digging deeper into things.

In short:

  • Apple is calling this “Liquid Glass.”
  • It’s Apple’s first significant UI overhaul in 10 years.
  • It will be implemented across all of Apple’s platforms, including iOS, macOS, iPadOS, and even the Vision Pro headset from which it was inspired.
  • “From a technical perspective, it’s a very impressive effect. I applaud the time and effort it must have taken to mimic refraction and dispersion of light to such a high degree.”
  • “Similar to the first beta for iOS 7, what we’ve seen so far is rough on the edges and potentially veers into distracting or challenging to read, especially for users with visual impairments.”
Accessibility

Let’s get right to the heart of where the pushback against liquid glass is coming from. While the aesthetic, purpose, and principles of liquid glass are broadly applauded, many are concerned about the legibility of content against a glass surface.

Traditionally, we fill backgrounds with solid or opaque solid color to establish contrast between the foreground and background, but with refracted light, color plays less a role and it’s possible that highlighting or dimming a light source will not produce enough contrast, particularly for those with low-vision. WCAG 2.2 emphasizes color and font size for improving contrast and does provide guidance for something that’s amorphous like liquid glass where bending the content below it is what establishes contrast.

“Apple’s “Liquid Glass” and What It Means for Accessibility”:

  • “When you have translucent elements letting background colors bleed through, you’re creating variable contrast ratios that might work well over one background, but fail over a bright photo of the sunset.”
  • “Apple turned the iPhone’s notch into the Dynamic Island, Android phones that don’t have notches started making fake notches, just so they could have a Dynamic Island too. That’s influence. But here they are making what looks like a purely aesthetic decision without addressing the accessibility implications.”
  • “People with dyslexia, who already struggle with busy backgrounds and low-contrast text, now deal with an interface where visual noise is baked into the design language. People with attention disorders may have their focus messed up when they see multiple translucent layers creating a whole lot of visual noise.”
  • “It’s like having a grand entrance and a side door marked ‘accessible.’ Technically compliant. But missing the point.”
  • “The legal landscape adds another layer. There’s thousands of digital accessibility lawsuits filed in the U.S. yearly for violating the ADA, or the American Disabilities Act. Companies are paying millions in settlements. But this is Apple. They have millions. Plus all the resources in the world to save them from legal risks. But their influence means they’re setting precedents.”

“Liquid Glass: Apple vs accessibility”:

  • “Yet even in Apple’s press release, linked earlier, there are multiple screenshots where key interface components are, at best, very difficult to read. That is the new foundational point for Apple design. And those screenshots will have been designed to show the best of things.”
  • “Apple is still very often reactive rather than proactive regarding vision accessibility. Even today, there are major problems with the previous versions of its operating systems (one example being the vestibular trigger if you tap-hold the Focus button in Control Centre). One year on, they aren’t fixed.”
  • “State, correctly, that Apple is a leader in accessibility. But stop assuming that just because this new design might be OK for you and because Apple has controls in place that might help people avoid the worst effects of design changes, everything is just peachy. Because it isn’t.”

“Liquid Glass” by Hardik Pandya

  • “The effect is technically impressive, but it introduces a layer of visual processing between you and your memories. What was once immediate now feels mediated. What was once direct now feels filtered.”
  • “While Apple’s rationale for Liquid Glass centers on ‘seeing’ content through a refractive surface, user interface controls are not meant to be seen—they are meant to be operated. When you tap a button, slide a slider, or toggle a switch, you are not observing these elements. You are manipulating them directly.”
  • “Buttons become amorphous shapes. Sliders lose their mechanical clarity. Toggle switches abandon their physical affordances. They appear as abstract forms floating behind glass—beautiful perhaps, but disconnected from the fundamental purpose of interface controls: to invite and respond to direct manipulation.”
  • “The most forward-thinking interface design today focuses on invisibility – making the interaction so seamless that the interface itself disappears. Liquid Glass makes the interface more visible, more present, and more demanding of attention.”

“Liquid glass, now with frosted tips”:

  • It’s easy to dump on liquid glass in its introductory form, but it’s worth remembering that it’s in beta and that Apple is actively developing it ahead of its formal release.
  • A lot has changed between the Beta 2 and Beta 3 releases. The opacity between glass and content has been bumped up in several key areas.
Tutorials, Generators, and Frameworks

It’s fun to see the difference approaches many folks have used to re-create the liquid glass effect in these early days. It amazes me that there is already a deluge of tutorials, generators, and even UI frameworks when we’re only a month past the WWDC 2025 introduction.

Experiments

Let’s drop in a few interesting demos that folks have created. To be clear, glass-based interfaces are not new and have been plenty explored, which you can find over at CodePen in abundance. These are recent experiments. The most common approaches appear to reach for SVG filters and background blurs, though there are many programmatic demos as well.

Using a CSS-only approach with an SVG filter with backdrop-filter with a series of nested containers that sorta mimics how Apple describes glass as being composed of three layers (highlight, shadow and illumination):

CodePen Embed Fallback

Same sort of deal here, but in the context of a theme toggle switch that demonstrates how glass can be tinted:

CodePen Embed Fallback

Comparing a straight-up CSS blur with an SVG backdrop:

CodePen Embed Fallback

Contextual example of a slider component:

CodePen Embed Fallback

Using WebGL:

CodePen Embed Fallback Assorted links and coverage

A few more links from this browser tab group I have open:

Getting Clarity on Apple’s Liquid Glass originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Syndicate content
©2003 - Present Akamai Design & Development.