I mentioned last time that I’ve been working on a new website for Emmy-award-winning game composer Mike Worth. He hired me to create a highly graphical design that showcases his work.
Mike loves ’90s animation, particularly Disney’s Duck Tales and other animated series. He challenged me to find a way to incorporate their retro ’90s style into his design without making it a pastiche. But that wasn’t my only challenge. I also needed to achieve that ’90s feel by using up-to-the-minute code to maintain accessibility, performance, responsiveness, and semantics.
Designing for Mike was like a trip back to when mainstream website design seemed more spontaneous and less governed by conventions and best practices. Some people describe these designs as “whimsical”:
adjective
spontaneously fanciful or playful
given to whims; capricious
quaint, unusual, or fantastic
— Collins English Dictionary
But I’m not so sure that’s entirely accurate. “Playful?” Definitely. “Fanciful?” Possibly. But “fantastic?” That depends. “Whimsy” sounds superfluous, so I call it “expressive” instead.
Studying design from way back, I remembered how websites often included graphics that combined branding, content, and navigation. Pretty much every reference to web design in the ’90s — when I designed my first website — talks about Warner Brothers’ Space Jam from 1996.
Warner Brothers’ Space Jam (1996)
So, I’m not going to do that.
Brands like Nintendo used their home pages to direct people to their content while making branded visual statements. Cheestrings combined graphics with navigation, making me wonder why we don’t see designs like this today. Goosebumps typified this approach, combining cartoon illustrations with brightly colored shapes into a functional and visually rich banner, proving that being useful doesn’t mean being boring.
Left to right: Nintendo, Cheestrings, Goosebumps.
In the ’90s, when I developed graphics for websites like these, I either sliced them up and put their parts in tables or used mostly forgotten image maps.
A brief overview of properties and values
Let’s run through a quick refresher. Image maps date all the way back to HTML 3.2, where, first, server-side maps and then client-side maps defined clickable regions over an image using map and area elements. They were popular for graphics, maps, and navigation, but their use declined with the rise of CSS, SVG, and JavaScript.
<map> adds clickable areas to a bitmap or vector image.
<map name="projects">
...
</map>
That <map> is linked to an image using the usemap attribute:
<img usemap="#projects" ...>
Those elements can have separate href and alt attributes and can be enhanced with ARIA to improve accessibility:
Despite their age, image maps still offer plenty of benefits. They’re lightweight and need (almost) no JavaScript. More on that in just a minute. They’re accessible and semantic when used with alt, ARIA, and title attributes. Despite being from a different era, even modern mobile browsers support image maps.
Design by Andy Clarke, Stuff & Nonsense. Mike Worth’s website will launch in April 2025, but you can see examples from this article on CodePen.
My design for Mike Worth includes several graphic navigation elements, which made me wonder if image maps might still be an appropriate solution.
Image maps in action
Mike wants his website to showcase his past work and the projects he’d like to do. To make this aspect of his design discoverable and fun, I created a map for people to explore by pressing on areas of the map to open modals. This map contains numbered circles, and pressing one pops up its modal.
My first thought was to embed anchors into the external map SVG:
This approach is problematic. Those anchors are only active when SVG is inline and don’t work with an <img> element. But image maps work perfectly, even though specifying their coordinates can be laborious. Fortunately, plenty of tools are available, which make defining coordinates less tedious. Upload an image, choose shape types, draw the shapes, and copy the markup:
Image maps work well when images are fixed sizes, but flexible images present a problem because map coordinates are absolute, not relative to an image’s dimensions. Making image maps responsive needs a little JavaScript to recalculate those coordinates when the image changes size:
On the other hand, a map’s <area> coordinates are absolute to the top-left of an image, so <path> values need to be converted. Fortunately, Raphael Monnerat has written PathToPoints, a tool which does precisely that. Upload an SVG, choose the point frequency, copy the coordinates for each path, and add them to a map area’s coords:
Image maps are hard-coded and time-consuming to create without tools. Even with tools for generating image maps, converting paths to points, and then recalculating them using JavaScript, they could be challenging to maintain at scale.
<area> elements aren’t visible, and except for a change in the cursor, they provide no visual feedback when someone hovers over or presses a link. Plus, there’s no easy way to add animations or interaction effects.
But the deal-breaker for me was that an image map’s pixel-based values are unresponsive by default. So, what might be an alternative solution for implementing my map using CSS, HTML, and SVG?
Anchors positioned absolutely over my map wouldn’t solve the pixel-based positioning problem or give me the irregular-shaped clickable areas I wanted. Anchors within an external SVG wouldn’t work either.
But the solution was staring me in the face. I realized I needed to:
Create a new SVG path for each clickable area.
Make those paths invisible.
Wrap each path inside an anchor.
Place the anchors below other elements at the end of my SVG source.
Replace that external file with inline SVG.
I created a set of six much larger paths which define the clickable areas, each with its own fill to match its numbered circle. I placed each anchor at the end of my SVG source:
Then, I reduced those anchors’ opacity to 0 and added a short transition to their full-opacity hover state:
#links a {
opacity: 0;
transition: all .25s ease-in-out;
}
#links a:hover {
opacity: 1;
}
While using an image map’s <area> sadly provides no visual feedback, embedded anchors and their content can respond to someone’s action, hint at what’s to come, and add detail and depth to a design.
I might add gloss to those numbered circles to be consistent with the branding I’ve designed for Mike. Or, I could include images, titles, or other content to preview the pop-up modals:
Designing Mike Worth’s website gave me a chance to blend expressive design with modern development techniques, and revisiting image maps reminded me just how important a tool image maps were during the period Mike loves so much.
Ultimately, image maps weren’t the right tool for Mike’s website. But exploring them helped me understand what I really needed: a way to recapture the expressiveness and personality of ’90s website design using modern techniques that are accessible, lightweight, responsive, and semantic. That’s what design’s about: choosing the right tool for a job, even if that sometimes means looking back to move forward.
Biography: Andy Clarke
Often referred to as one of the pioneers of web design, Andy Clarke has been instrumental in pushing the boundaries of web design and is known for his creative and visually stunning designs. His work has inspired countless designers to explore the full potential of product and website design.
Andy’s written several industry-leading books, including Transcending CSS, Hardboiled Web Design, and Art Direction for the Web. He’s also worked with businesses of all sizes and industries to achieve their goals through design.
Visit Andy’s studio, Stuff & Nonsense, and check out his Contract Killer, the popular web design contract template trusted by thousands of web designers and developers.
Brad Frost is running this new little podcast called Open Up. Folks write in with questions about the “other” side of web design and front-end development — not so much about tools and best practices as it is about the things that surround the work we do, like what happens if you get laid off, or AI takes your job, or something along those lines. You know, the human side of what we do in web design and development.
Well, it just so happens that I’m co-hosting the show. In other words, I get to sprinkle in a little advice on top of the wonderful insights that Brad expertly doles out to audience questions.
Our second episode just published, and I thought I’d share it. We’re finding our sea legs with this whole thing and figuring things out as we go. We’ve opened things up (get it?!) to a live audience and even pulled in one of Brad’s friends at the end to talk about the changing nature of working on a team and what it looks like to collaborate in a remote-first world.
There’s not enough chairs for them to all sit at the bar, so you need the tenth div to sit on the lap of one of the other divs, say the second one. We can visually cover the second div with the tenth div but have to make sure they are sitting next to each other in the HTML as well. The order matters.
<div>1</div>
<div>2</div>
<div>10</div><!-- Sitting next to Div #2-->
<div>3</div>
<div>4</div>
<div>5</div>
<div>6</div>
<div>7</div>
<div>8</div>
<div>9</div>
The tenth div needs to sit on the second div’s lap rather than next to it. So, perhaps we redefine the relationship between them and make this a parent-child sorta thing.
<div>1</div>
<div class="parent">
2
<div class="child">10</div><!-- Sitting in Div #2's lap-->
</div>
<div>3</div>
<div>4</div>
<div>5</div>
<div>6</div>
<div>7</div>
<div>8</div>
<div>9</div>
Now we can do a little tricky positioning dance to contain the tenth div inside the second div in the CSS:
Anchor positioning simplifies this process a heckuva lot because it just doesn’t care where the tenth div is in the HTML. Instead, we can work with our initial markup containing 10 individuals exactly as they entered the bar. You’re going to want to follow along in the latest version of Chrome since anchor positioning is only supported there by default at the time I’m writing this.
Instead, we define the second div as an anchor element using the anchor-name property. I’m going to continue using the .parent and .child classes to keep things clear.
.parent {
anchor-name: --anchor; /* this can be any name formatted as a dashed ident */
}
Then we connect the child to the parent by way of the position-anchor property:
.child {
position-anchor: --anchor; /* has to match the `anchor-name` */
}
The last thing we have to do is position the child so that it covers the parent’s lap. We have the position-area property that allows us to center the element over the parent:
No punchline — just one of the things that makes anchor positioning something I’m so excited about. The fact that it eschews HTML source order is so CSS-y because it’s another separation of concerns between content and presentation.
You would be forgiven if you’ve never heard of Cohost.org. The bespoke, Tumblr-like social media website came and went in a flash. Going public in June 2022 with invite-only registrations, Cohost’s peach and maroon landing page promised that it would be “posting, but better.” Just over two years later, in September 2024, the site announced its shutdown, its creators citing burnout and funding problems. Today, its servers are gone for good. Any link to cohost.org redirects to the Wayback Machine’s slow but comprehensive archive.
The landing page for Cohost.org, featuring our beloved eggbug.
Despite its short lifetime, I am confident in saying that Cohost delivered on its promise. This is in no small part due to its user base, consisting mostly of niche internet creatives and their friends — many of whom already considered “posting” to be an art form. These users were attracted to Cohost’s opinionated, anti-capitalist design that set it apart from the mainstream alternatives. The site was free of advertisements and follower counts, all feeds were purely chronological, and the posting interface even supported a subset of HTML.
It was this latter feature that conjured a community of its own. For security reasons, any post using HTML was passed through a sanitizer to remove any malicious or malformed elements. But unlike most websites, Cohost’s sanitizer was remarkably permissive. The vast majority of tags and attributes were allowed — most notably inline CSS styles on arbitrary elements.
Users didn’t take long to grasp the creative opportunities lurking within Cohost’s unassuming “new post” modal. Within 48 hours of going public, the fledgling community had figured out how to post poetry using the <details> tag, port the Apple homepage from 1999, and reimplement a quick-time WarioWare game. We called posts like these “CSS Crimes,” and the people who made them “CSS Criminals.” Without even intending to, the developers of Cohost had created an environment for a CSS community to thrive.
In this post, I’ll show you a few of the hacks we found while trying to push the limits of Cohost’s HTML support. Use these if you dare, lest you too get labelled a CSS criminal.
Width-hacking
Many of the CSS crimes of Cohost were powered by a technique that user @corncycle dubbed “width-hacking.” Using a combination of the <details> element and the CSS calc() function, we can get some pretty wild functionality: combination locks, tile matching games, Zelda-style top-down movement, the list goes on.
If you’ve been around the CSS world for a while, there’s a good chance you’ve been exposed to the old checkbox hack. By combining a checkbox, a label, and creative use of CSS selectors, you can use the toggle functionality of the checkbox to implement all sorts of things. Tabbed areas, push toggles, dropdown menus, etc.
However, because this hack requires CSS selectors, that meant we couldn’t use it on Cohost — remember, we only had inline styles. Instead, we used the relatively new elements <details> and <summary>. These elements provide the same visibility-toggling logic, but now directly in HTML. No weird CSS needed.
These elements work like so: All children of the <details> element are hidden by default, except for the <summary> element. When the summary is clicked, it “opens” the parent details element, causing its children to become visible.
We can add all sorts of styles to these elements to make this example more interesting. Below, I have styled the constituent elements to create the effect of a button that lights up when you click on it.
This is achieved by giving the <summary> element a fixed position and size, a grey background color, and an outset border to make it look like a button. When it’s clicked, a sibling <div> is revealed that covers the <summary> with its own red background and border. Normally, this <div> would block further click events, but I’ve given it the declaration pointer-events: none. Now all clicks pass right on through to the <summary> element underneath, allowing you to turn the button back off.
This is all pretty nifty, but it’s ultimately the same logic as before: something is toggled either on or off. These are only two states. If we want to make games and other gizmos, we might want to represent hundreds to thousands of states.
Width-hacking gives us exactly that. Consider the following example:
In this example, three <details> elements live together in an inline-flex container. Because all the <summary> elements are absolutely-positioned, the width of their respective <details> elements are all zero when they’re closed.
Now, each of these three <details> has a small <div> inside. The first has a child with a width of 1px, the second a child with a width of 2px, and the third a width of 4px. When a <details> element is opened, it reveals its hidden <div>, causing its own width to increase. This increases the width of the inline-flex container. Because the width of the container is the sum of its children, this means its width directly corresponds to the specific <details> elements that are open.
For example, if just the first and third <details> are open, the inline-flex container will have the width 1px + 4px = 5px. Conversely, if the inline-flex container is 2px wide, we can infer that the only open <details> element is the second one. With this trick, we’ve managed to encode all eight states of the three <details> into the width of the container element.
This is pretty cool. Maybe we could use this as an element of some kind of puzzle game? We could show a secret message if the right combination of buttons is checked. But how do we do that? How do we only show the secret message for a specific width of that container div?
In the preceding CodePen, I’ve added a secret message as two nested divs. Currently, this message is always visible — complete with a TODO reminding us to implement the logic to hide it unless the correct combination is set.
You may wonder why we’re using two nested divs for such a simple message. This is because we’ll be hiding the message using a peculiar method: We will make the width of the parent div.secret be zero. Because the overflow: hidden property is used, the child div.message will be clipped, and thus invisible.
Now we’re ready to implement our secret message logic. Thanks to the fact that percentage sizes are relative to the parent, we can use 100% as a stand-in for the parent’s width. We can then construct a complicated CSS calc() formula that is 350px if the container div is our target size, and 0px otherwise. With that, our secret message will be visible only when the center button is active and the others are inactive. Give it a try!
This complicated calc() function that’s controlling the secret div’s width has the following graph:
You can see that it’s a piecewise linear curve, constructed from multiple pieces using min/max. These pieces are placed in just the right spots so that the function maxes out when the container div is 2px— which we’ve established is precisely when only the second button is active.
A surprising variety of games can be implemented using variations on this technique. Here is a tower of Hanoi game I had made that uses both width and height to track the game’s state.
SVG animation
So far, we’ve seen some basic functionality for implementing a game. But what if we want our games to look good? What if we want to add ✨animations?✨ Believe it or not, this is actually possible entirely within inline CSS using the power of SVG.
SVG (Scalable Vector Graphics) is an XML-based image format for storing vector images. It enjoys broad support on the web — you can use it in <img> elements or as the URL of a background-image property, among other things.
Like HTML, an SVG file is a collection of elements. For SVG, these elements are things like <rect>, <circle>, and <text>, to name a few. These elements can have all sorts of properties defined, such as fill color, stroke width, and font family.
A lesser-known feature of SVG is that it can contain <style> blocks for configuring the properties of these elements. In the example below, an SVG is used as the background for a div. Inside that SVG is a <style> block that sets the fillcolor of its <circle> to red.
An even lesser-known feature of SVG is that its styles can use media queries. The size used by those queries is the size of the div it is a background of.
In the following example, we have a resizable <div> with an SVG background. Inside this SVG is a media query which will change the fill color of its <circle> to blue when the width exceeds 100px. Grab the resize handle in its bottom right corner and drag until the circle turns blue.
Because resize handles don’t quite work on mobile, unfortunately, this and the next couple of CodePens are best experienced on desktop.
This is an extremely powerful technique. By mixing it with width-hacking, we could encode the state of a game or gizmo in the width of an SVG background image. This SVG can then show or hide specific elements depending on the corresponding game state via media queries.
But I promised you animations. So, how is that done? Turns out you can use CSS animations within SVGs. By using the CSS transition property, we can make the color of our circle smoothly transition from red to blue.
Amazing! But before you try this yourself, be sure to look at the source code carefully. You’ll notice that I’ve had to add a 1×1px, off-screen element with the ID #hack. This element has a very simple (and nearly unnoticeable) continuous animation applied. A “dummy animation” like this is necessary to get around some web browsers’ buggy detection of SVG animation. Without that hack, our transition property wouldn’t work consistently.
For the fun of it, let’s combine this tech with our previous secret message example. Instead of toggling the secret message’s width between the values of 0px and 350px, I’ve adjusted the calc formula so that the secret message div is normally 350px, and becomes 351px if the right combination is set.
Instead of HTML/CSS, the secret message is now just an SVG background with a <text> element that says “secret message.” Using media queries, we change the transform scale of this <text> to be zero unless the div is 351px. With the transition property applied, we get a smooth transition between these two states.
Click the center button to activate the secret message:
The first cohost user to discover the use of media queries within SVG backgrounds was @ticky for this post. I don’t recall who figured out they could animate, but I used the tech quite extensively for this quiz that tells you what kind of soil you’d like if you were a worm.
Wrapping up
And that’s will be all for now. There are a number of techniques I haven’t touched on — namely the fun antics one can get up to with the resize property. If you’d like to explore the world of CSS crimes further, I’d recommend this great linkdump by YellowAfterlife, or this video retrospective by rebane2001.
It will always hurt to describe Cohost in the past tense. It truly was a magical place, and I don’t think I’ll be able to properly convey what it was like to be there at its peak. The best I can do is share the hacks we came up with: the lost CSS tricks we invented while “posting, but better.”
Hey, did you see the post Jen Simmons published about WebKit’s text-wrap: pretty implementation? It was added to Safari Technology Preview and can be tested now, as in, like, today. Slap this in a stylesheet and your paragraphs get a nice little makeover that improves the ragging, reduces hyphenation, eliminates typographic orphans at the end of the last line, and generally avoids large typographic rivers as a result. The first visual in the post tells the full story, showing how each of these is handled.
That’s a lot of heavy lifting for a single value! And according to Jen, this is vastly different from Chromium’s implementation of the exact same feature.
According to an article by the Chrome team, Chromium only makes adjustments to the last four lines of a paragraph. It’s focused on preventing short last lines. It also adjusts hyphenation if consecutive hyphenated lines appear at the end of a paragraph.
Jen suggests that performance concerns are the reason for the difference. It does sound like the pretty value does a lot of work, and you might imagine that would have a cumulative effect when we’re talking about long-form content where we’re handling hundreds, if not thousands, of lines of text. If it sounds like Safari cares less about performance, that’s not the case, as their approach is capable of handling the load.
One thing to know as a developer, the performance of text-wrap is not affected by how many elements on the page it’s applied to. Perf concerns emerge as the pretty algorithm takes more and more lines into consideration as it calculates what to do. In WebKit-based browsers or apps, your text element would need to be many hundreds or thousands of lines long to see a performance hit — and that kind of content is unusual on the web. If your content is broken up into typical-length paragraphs, then you have no reason to worry. Use text-wrap: pretty as much as you want, and rely on our browser engineers to ensure your users will not experience any downsides.
Great, carry on! But now you know that two major browsers have competing implementations of the same feature. I’ve been unclear on the terminology of pretty since it was specced, and now it truly seems that what is considered “pretty” really is in the eye of the beholder. And if you’re hoping to choose a side, don’t, because the specification is intentionally unopinionated in this situation, as it says (emphasis added):
The user agent may among other things attempt to avoid excessively short last lines… but it should also improve the layout in additional ways. The precise set of improvements is user agent dependent, and may include things such as: reducing the variation in length between lines; avoiding typographic rivers; prioritizing different classes of soft wrap opportunities, hyphenation opportunities, or justification opportunities; avoiding hyphenation on too many consecutive lines.
So, there you have it. One new feature. Two different approaches. Enjoy your new typographic powers. 💪
There was once upon a time when native CSS lacked many essential features, leaving developers to come up with all sorts of ways to make CSS easier to write over the years.
These ways can mostly be categorized into two groups:
Pre-processors
Post-processors
Pre-processors include tools like Sass, Less, and Stylus. Like what the category’s name suggests, these tools let you write CSS in their syntax before compiling your code into valid CSS.
Post-processors work the other way — you write non-valid CSS syntax into a CSS file, then post-processors will change those values into valid CSS.
There are two major post-processors today:
PostCSS
LightningCSS
PostCSS is the largest kid on the block while Lightning CSS is a new and noteworthy one. We’ll talk about them both in a bit.
I think post-processors have won the compiling game
Post-processors have always been on the verge of winning since PostCSS has always been a necessary tool in the toolchain.
The most obvious (and most useful) PostCSS plugin for a long time is Autoprefixer — it creates vendor prefixes for you so you don’t have to deal with them.
Arguably, we don’t need Autoprefixer much today because browsers are more interopable, but nobody wants to go without Autoprefixer because it eliminates our worries about vendor prefixing.
What has really tilted the balance towards post-processors includes:
Native CSS gaining essential features
Tailwind removing support for pre-processors
Lightning CSS
Let me expand on each of these.
Native CSS gaining essential features
CSS pre-processors existed in the first place because native CSS lacked features that were critical for most developers, including:
CSS variables
Nesting capabilities
Allowing users to break CSS into multiple files without additional fetch requests
Conditionals like if and for
Mixins and functions
Native CSS has progressed a lot over the years. It has gained great browser support for the first two features:
CSS Variables
Nesting
With just these two features, I suspect a majority of CSS users won’t even need to fire up pre-processors or post-processors. What’s more, The if() function is coming to CSS in the future too.
But, for the rest of us who needs to make maintenance and loading performance a priority, we still need the third feature — the ability to break CSS into multiple files. This can be done with Sass’s use feature or PostCSS’s import feature (provided by the postcss-import plugin).
PostCSS also contains plugins that can help you create conditionals, mixins, and functions should you need them.
Tailwind 4 has officially removed support for pre-processors. From Tailwind’s documentation:
Tailwind CSS v4.0 is a full-featured CSS build tool designed for a specific workflow, and is not designed to be used with CSS pre-processors like Sass, Less, or Stylus. Think of Tailwind CSS itself as your pre-processor — you shouldn’t use Tailwind with Sass for the same reason you wouldn’t use Sass with Stylus. Since Tailwind is designed for modern browsers, you actually don’t need a pre-processor for things like nesting or variables, and Tailwind itself will do things like bundle your imports and add vendor prefixes.
If you included Tailwind 4 via its most direct installation method, you won’t be able to use pre-processors with Tailwind.
@import `tailwindcss`
That’s because this one import statement makes Tailwind incompatible with Sass, Less, and Stylus.
But, (fortunately), Sass lets you import CSS files if the imported file contains the .css extension. So, if you wish to use Tailwind with Sass, you can. But it’s just going to be a little bit wordier.
Either way, many people won’t know you can continue to use pre-processors with Tailwind. Because of this, I suspect pre-processors will get less popular as Tailwind gains more momentum.
Now, beneath Tailwind is a CSS post-processor called Lightning CSS, so this brings us to talking about that.
Lightning CSS
Lightning CSS is a post-processor can do many things that a modern developer needs — so it replaces most of the PostCSS tool chain including:
Besides having a decent set of built-in features, it wins over PostCSS because it’s incredibly fast.
Lightning CSS is over 100 times faster than comparable JavaScript-based tools. It can minify over 2.7 million lines of code per second on a single thread.
Speed helps Lightning CSS win since many developers are speed junkies who don’t mind switching tools to achieve reduced compile times. But, Lightning CSS also wins because it has great distribution.
It can be used directly as a Vite plugin (that many frameworks support). Ryan Trimble has a step-by-step article on setting it up with Vite if you need help.
If you need other PostCSS plugins, you can also include that as part of the PostCSS tool chain.
// postcss.config.js
// Import other plugins...
import lightning from 'postcss-lightningcss'
export default {
plugins: [lightning, /* Other plugins */],
}
Many well-known developers have switched to Lightning CSS and didn’t look back. Chris Coyier says he’ll use a “super basic CSS processing setup” so you can be assured that you are probably not stepping in any toes if you wish to switch to Lightning, too.
If you wanna ditch pre-processors today
You’ll need to check the features you need. Native CSS is enough for you if you need:
CSS Variables
Nesting capabilities
Lightning CSS is enough for you if you need:
CSS Variables
Nesting capabilities
import statements to break CSS into multiple files
Tailwind (with @apply) is enough for you if you need:
all of the above
Mixins
If you still need conditionals like if, for and other functions, it’s still best to stick with Sass for now. (I’ve tried and encountered interoperability issues between postcss-for and Lightning CSS that I shall not go into details here).
That’s all I want to share with you today. I hope it helps you if you have been thinking about your CSS toolchain.
This article covers tips and tricks on effectively utilizing the CSS backdrop-filter property to style contemporary user interfaces. You’ll learn how to layer backdrop filters among multiple elements, and integrate them with other CSS graphical effects to create elaborate designs.
Below is a hodgepodge sample of what you can build based on everything we’ll cover in this article. More examples are coming up.
The blurry, frosted glass effect is popular with developers and designers these days — maybe because Josh Comeau wrote a deep-dive about it somewhat recently — so that is what I will base my examples on. However, you can apply everything you learn here to any relevant filter. I’ll also be touching upon a few of them in my examples.
What’s essential in a backdrop filter?
If you’re familiar with CSS filter functions like blur() and brightness(), then you’re also familiar with backdrop filter functions. They’re the same. You can find a complete list of supported filter functions here at CSS-Tricks as well as over at MDN.
The difference between the CSS filter and backdrop-filter properties is the affected part of an element. Backdrop filter affects the backdrop of an element, and it requires a transparent or translucent background in the element for its effect to be visible. It’s important to remember these fundamentals when using a backdrop filter, for these reasons:
to decide on the aesthetics,
to be able to layer the filters among multiple elements, and
to combine filters with other CSS effects.
The backdrop
Design is subjective, but a little guidance can be helpful. If you’ve applied a blur filter to a plain background and felt the result was unsatisfactory, it could be that it needed a few embellishments, like shadows, or more often than not, it’s because the backdrop is too plain.
Plain backdrops can be enhanced with filters like brightness(), contrast(), and invert(). Such filters play with the luminosity and hue of an element’s backdrop, creating interesting designs. Textured backdrops complement distorting filters like blur() and opacity().
<main>
<div>
<section>
<h1>Weather today</h1>
Cloudy with a chance of meatballs. Ramenstorms at 3PM that will last for ten minutes.
</section>
</div>
</main>
main {
background: center/cover url("image.jpg");
box-shadow: 0 0 10px rgba(154 201 255 / 0.6);
/* etc. */
div {
backdrop-filter: blur(10px);
color: white;
/* etc. */
}
}
Layering elements with backdrop filters
As we just discussed, backdrop filters require an element with a transparent or translucent background so that everything behind it, with the filters applied, is visible.
If you’re applying backdrop filters on multiple elements that are layered above one another, set a translucent (not transparent) background to all elements except the bottommost one, which can be transparent or translucent, provided it has a backdrop. Otherwise, you won’t see the desired filter buildup.
<main>
<div>
<section>
<h1>Weather today</h1>
Cloudy with a chance of meatballs. Ramenstorms at 3PM that will last for ten minutes.
</section>
<p>view details</p>
</div>
</main>
main {
background: center/cover url("image.jpg");
box-shadow: 0 0 10px rgba(154 201 255 / 0.6);
/* etc. */
div {
background: rgb(255 255 255 / .1);
backdrop-filter: blur(10px);
/* etc. */
p {
backdrop-filter: brightness(0) contrast(10);
/* etc. */
}
}
}
Combining backdrop filters with other CSS effects
When an element meets a certain criterion, it gets a backdrop root (not yet a standardized name). One criterion is when an element has a filter effect (from filter and background-filter). I believe backdrop filters can work well with other CSS effects that also use a backdrop root because they all affect the same backdrop.
Of those effects, I find two interesting: mask and mix-blend-mode. Combining backdrop-filter with mask resulted in the most reliable outcome across the major browsers in my testing. When it’s done with mix-blend-mode, the blur backdrop filter gets lost, so I won’t use it in my examples. However, I do recommend exploring mix-blend-mode with backdrop-filter.
Backdrop filter with mask
Unlike backdrop-filter, CSS mask affects the background and foreground (made of descendants) of an element. We can use that to our advantage and work around it when it’s not desired.
<main>
<div>
<div class="bg"></div>
<section>
<h1>Weather today</h1>
Cloudy with a chance of meatballs. Ramenstorms at 3PM that will last for ten minutes.
</section>
</div>
</main>
main {
background: center/cover url("image.jpg");
box-shadow: 0 0 10px rgba(154 201 255 / 0.6);
/* etc. */
> div {
.bg {
backdrop-filter: blur(10px);
mask-image: repeating-linear-gradient(90deg, transparent, transparent 2px, white 2px, white 10px);
/* etc. */
}
/* etc. */
}
}
Backdrop filter for the foreground
We have the filter property to apply graphical effects to an element, including its foreground, so we don’t need backdrop filters for such instances. However, if you want to apply a filter to a foreground element and introduce elements within it that shouldn’t be affected by the filter, use a backdrop filter instead.
<main>
<div class="photo">
<div class="filter"></div>
</div>
<!-- etc. -->
</main>
In the example below, hover over the blurred photo.
There are plenty of ways to play with the effects of the CSS backdrop-filter. If you want to layer the filters across stacked elements then ensure the elements on top are translucent. You can also combine the filters with other CSS standards that affect an element’s backdrop. Once again, here’s the set of UI designs I showed at the beginning of the article, that might give you some ideas on crafting your own.
The cursor is a staple of the desktop interface but is scarcely touched by websites. This is for good reason. People expect their cursors to stay fairly consistent, and meddling with them can unnecessarily confuse users. Custom cursors also aren’t visible for people using touch interfaces — which excludes the majority of people.
Custom cursors with CSS are great, but we can take things to the next level with JavaScript. Using JavaScript, we can use an element as our cursor, which lets us style it however we would anything else. This lets us transition between cursor states, place dynamic text within the cursor, apply complex animations, and apply filters.
In its most basic form, we just need a div that continuously positions itself to the cursor location. We can do this with the mousemove event listener. While we’re at it, we may as well add a cool little effect when clicking via the mousedown event listener.
That’s wonderful. Now we’ve got a bit of a custom cursor going that scales on click. You can see that it is positioned based on the mouse coordinates relative to the page with JavaScript. We do still have our default cursor showing though, and it is important for our new cursor to indicate intent, such as changing when hovering over something clickable.
We can disable the default cursor display completely by adding the CSS rule cursor: none to *. Be aware that some browsers will show the cursor regardless if the document height isn’t 100% filled.
We’ll also need to add pointer-events: none to our cursor element to prevent it from blocking our interactions, and we’ll show a custom effect when hovering certain elements by adding the pressable class.
Very nice. That’s a lovely little circular cursor we’ve got here.
Fallbacks, accessibility, and touchscreens
People don’t need a cursor when interacting with touchscreens, so we can disable ours. And if we’re doing really funky things, we might also wish to disable our cursor for users who have the prefers-reduced-motion preference set.
We can do this without too much hassle:
What we’re doing here is checking if the user is accessing the site with a touchscreen or if they prefer reduced motion and then only enabling the custom cursor if they aren’t. Because this is handled with JavaScript, it also means that the custom cursor will only show if the JavaScript is active, otherwise falling back to the default cursor functionality as defined by the browser.
const isTouchDevice = "ontouchstart"in window || navigator.maxTouchPoints > 0;
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (!isTouchDevice && !prefersReducedMotion && cursor) {
// Cursor implementation is here
}
Currently, the website falls back to the default cursors if JavaScript isn’t enabled, but we could set a fallback cursor more similar to our styled one with a bit of CSS. Progressive enhancement is where it’s at!
Here we’re just using a very basic 32px by 32px base64-encoded circle. The 16 values position the cursor hotspot to the center.
html {
cursor:
url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0Jve
D0iMCAwIDMyIDMyIj4KICA8Y2lyY2xlIGN4PSIxNiIgY3k9IjE2IiByPSIxNiIgZmlsbD0iYmxhY2siIC8+Cjwvc3ZnPg==")
16 16,
auto;
}
Taking this further
Obviously this is just the start. You can go ballistic and completely overhaul the cursor experience. You can make it invert what is behind it with a filter, you can animate it, you could offset it from its actual location, or anything else your heart desires.
As a little bit of inspiration, some really cool uses of custom cursors include:
Studio Mesmer switches out the default cursor for a custom eye graphic when hovering cards, which is tasteful and fits their brand.
Iara Grinspun’s portfolio has a cursor implemented with JavaScript that is circular and slightly delayed from the actual position which makes it feel floaty.
Marlène Bruhat’s portfolio has a sleek cursor that is paired with a gradient that appears behind page elements.
Terra features a giant glowing orb containing text describing what you’re hovering over.
Please do take care when replacing browser or native operating system features in this manner. The web is accessible by default, and we should take care to not undermine this. Use your power as a developer with taste and restraint.