Front End Web Development
Quick Hit #11
Free e-book from Jens Oliver Meiert that’ll bore you to death in the best way: Rote Learning HTML & CSS
Quick Hit #11 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Quick Hit #10
Killed by Google is called a “graveyard” but I also see it as a resume in experimentation.
Quick Hit #10 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
“Smart” Layouts With Container Queries
Modern CSS keeps giving us a lot of new, easier ways to solve old problems, but often the new features we’re getting don’t only solve old problems, they open up new possibilities as well.
Container queries are one of those things that open up new possibilities, but because they look a lot like the old way of doing things with media queries, our first instinct is to use them in the same way, or at least a very similar way.
When we do that, though, we aren’t taking advantage of how “smart” container queries are when compared to media queries!
Because of how important media queries were for ushering in the era of responsive web design I don’t want to say anything mean about them… but media queries are dumb. Not dumb in terms of the concept, but dumb in that they don’t know very much. In fact, most people assume that they know more than they do.
Let’s use this simple example to illustrate what I mean:
html { font-size: 32px; } body { background: lightsalmon; } @media (min-width: 35rem) { body { background: lightseagreen; } }What would the viewport size be for the background color to change? If you said 1120px wide — which is the product of multiplying 35 by 32 for those who didn’t bother doing the math — you aren’t alone in that guess, but you’d also be wrong.
Remember when I said that media queries don’t know very much? There are only two things they do know:
- the size of the viewport, and
- the browser’s font size.
And when I say the browser’s font size, I don’t mean the root font size in your document, which is why 1120px in the above example was wrong.
The font size they look at is the initial font size coming from the browser before any values, including the user agent styles, are applied. By default, that’s 16px, though users can change that in their browser settings.
And yes, this is on purpose. The media query specification says:
Relative length units in media queries are based on the initial value, which means that units are never based on results of declarations.
This might seem like a strange decision, but if it didn’t work that way, what would happen if we did this:
html { font-size: 16px; } @media (min-width: 30rem) { html { font-size: 32px; } }If the media query looked at the root font-size (like most assume it does), you’d run into a loop when the viewport would get to 480px wide, where the font-size would go up in size, then back down over and over again.
Container queries are a lot smarterWhile media queries have this limitation, and for good reason, container queries don’t have to worry about this type of problem and that opens up a lot of interesting possibilities!
For example, let’s say we have a grid that should be stacked at smaller sizes, but three columns at larger sizes. With media queries, we sort of have to magic number our way to the exact point where this should happen. Using a container query, we can determine the minimum size we want a column to be, and it’ll always work because we’re looking at the container size.
That means we don’t need a magic number for the breakpoint. If I want three columns with a minimum size of 300px, I know I can have three columns when the container is 900px wide. If I did that with a media query, it wouldn’t work, because when my viewport is 900px wide, my container is, more often than not, smaller than that.
But even better, we can use any unit we want as well, because container queries, unlike media queries, can look at the font size of the container itself.
To me, ch is perfect for this sort of thing. Using ch I can say “when I have enough room for each column to be a minimum of 30 characters wide, I want three columns.”
We can do the math ourselves here like this:
.grid-parent { container-type: inline-size; } .grid { display: grid; gap: 1rem; @container (width > 90ch) { grid-template-columns: repeat(3, 1fr); } }And this does work pretty well, as you can see in this example.
CodePen Embed FallbackAs another bonus, thanks to Miriam Suzanne, I recently learned that you can include calc() inside media and container queries, so instead of doing the math yourself, you can include it like this: @container (width > calc(30ch * 3)) as you can see in this example:
CodePen Embed Fallback A more practical use caseOne of the annoying things about using container queries is having to have a defined container. A container cannot query itself, so we need an extra wrapper above the element we want to select with a container query. You can see in the examples above that I needed a container on the outside of my grid for this to work.
Even more annoying is when you want grid or flex children to change their layout depending on how much space they have, only to realize that this doesn’t really work if the parent is the container. Instead of having that grid or flex container be the defined container, we end up having to wrap each grid or flex item in a container like this:
<div class="grid"> <div class="card-container"> <div class="card"> </div> <div class="card-container"> <div class="card"> </div> <div class="card-container"> <div class="card"> </div> </div> .card-container { container-type: inline-size; }It’s not that bad in the grand scheme of things, but it is kind of annoying.
Except there are ways around this!For example, if you’re using repeat(auto-fit, ...) you can use the main grid as the container!
.grid-auto-fit { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(min(30ch, 100%)), 1fr); container-type: inline-size; }Knowing that the minimum size of a column is 30ch, we can leverage that info to restyle individual grid items depending on how many columns we have:
/* 2 columns + gap */ @container (width > calc(30ch * 2 + 1rem)) { ... } /* 3 columns + gaps */ @container (width > calc(30ch * 3 + 2rem)) { ... }I’ve used this in this example to change the styles of the first child in my grid based on whether we have one, two, or three columns.
CodePen Embed FallbackAnd while changing the background color of something is great for demos, we can, of course, do much more with this:
CodePen Embed Fallback The downside to this approachThe only downside I’ve found using this approach is that we can’t use custom properties for the breakpoints, which would really improve the DX of this.
That should eventually change considering custom media queries are in the spec editor’s draft of the Media Queries Level 5 specifications, but its been in there for a while with no movement from any browsers, so it might be a long time before we can use them.
And while my opinion is that having custom properties for these would both make them more readable and easier to update, it opens up enough possibilities that it’s still worth it without them.
What about flexbox?With flexbox, the flex items are what define the layout, so it’s a little strange in that the sizes we apply on the items are what are important in the breakpoints.
It can still work, but there is a big issue that can arise if you do this with flexbox. Before we look at the issue, here is a quick example of how we can get this working with flexbox:
.flex-container { display: flex; gap: 1rem; flex-wrap: wrap; container-type: inline-size; } .flex-container > * { /* full-width at small sizes */ flex-basis: 100%; flex-grow: 1; /* when there is room for 3 columns including gap */ @container (width > calc(200px * 3 + 2rem)) { flex-basis: calc(200px); } }In this case, I used px to show it works as well, but you could use any unit there, as I did with the grid examples.
This might look like something you can use a media query for as well — you can use the calc() in them too! — but this would only work in one if the parent has a width that matches the viewport width, which most of the time isn’t the case.
CodePen Embed Fallback This breaks if the flex items have paddingA lot of people don’t realize it, but the flexbox algorithm doesn’t take padding or borders into account, even if you change your box-sizing. If you have padding on your flex items, you’ll basically have to magic number your way to getting it to work.
Here’s an example where I added some padding but I haven’t changed anything else, and you’ll notice one of those awkward two-columns with one stretched on the bottom layouts at one point:
CodePen Embed FallbackBecause of this, I do generally find myself using this type of approach more often with grid than flexbox, but there are definitely situations where it can still work.
Like before, because we’re aware of how many columns we have, we can leverage that to make more dynamic and interesting layouts depending on the space available for a given element, or elements.
CodePen Embed Fallback Opening up some interesting possibilitiesI’ve only started playing around with this type of thing, and I’ve found that it’s opened up some new possibilities that we never had with media queries, and that makes me excited to see what else is possible!
“Smart” Layouts With Container Queries originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Dialogues Blog
“This isn’t a website. Or even a blog. It’s a conversation.”
That’s the idea! Jay Hoffman and I’ve been chatting a long time now, back since he began writing a series on web history. It’s easy-going talking with someone with all that knowledge about the web’s interesting nooks and crannies.
Anyway, I always look forward to those chats. Sometimes, though, one of can’t make it for whatever reason. At the time, we’d been talking about different approaches to blogging and were particularly keen on the whole “digital garden” concept. We weren’t going to plant one or anything, but that inspired us to move our long-running conversation to a blog — you know, one of the best mediums ever in web history.
We didn’t want something linear in the sense that the blog is an archive of posts in chronological order. No, instead we wanted it to start as a seed that grows into a vine that twists and turns at different bends.
That’s where the “Dialogues” idea came in. It’s all Jay’s, really. He starts by asking me a question that I answer in the form of a post with a follow-up question that he, in turn, answers in a post with a follow-up question, and on and on.
The first question is already up there, and it’s a nice little ice breaker: What was the moment the web clicked for you? I had to take a few moments to let that one sink in and reflect on myself as a web user from 30 years ago. What a great little mind exercise and thing to talk about!
Dialogues Blog originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
font-size Limbo
You might recall that Alvaro suggests bumping up font-size to 1.25rem from the default user agent size of 16px. Sebastian Laube pokes at that:
I wouldn’t adopt Alvaro’s suggestion without further ado, as I would waste so much space on a smartphone, for example, and many users would probably be irritated by the large font.
I set a font size of 1.2rem from a certain viewport size. But this also has to be done carefully, because then grey areas arise in which media queries suddenly fall back into another area…
I personally agree with Alvaro that the default 16px size is too small. That’s just how I feel as someone who is uncomfortably close to wearing the bottoms of actual Coke bottles to see anything clearly on a screen.
On the flip side, I professionally agree with Sebastian, not that many users would probably be irritated by the large font, but to openly question an approach rather than adopting someone else’s approach wholesale based on a single blog post. It may very well be that a font-size bump is the right approach. Everything is relative, after all, and we ought to be listening to the people who use the thing we’re making for decisions like this.
The much bigger question is the one Sebastian poses right at the end there:
Should browsers perhaps use a larger font size on large screens from the outset if the user does not specify otherwise? Or do we need an information campaign to make them aware that they should check their system settings or set a different default font size in the browser?
Fantastic, right?! I’m honestly unsure where I’d draw the viewport breakpoint for 16px being either too large or small and where to start making adjustments. Is 16px the right default at any viewport size? Or perhaps user agents ought to consider a fluid type implementation that defines a default font scale and range of sizes instead? It’s great food for thought.
font-size Limbo originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Mental Health in Tech Podcast Interview
Mental health is always tough to talk about, especially in an industry that, to me, often rewards ego over vulnerability. I still find it tough even after having written about my own chronic depression and exploring UX case studies about it.
But that’s exactly the sort of discussions that Schalk Venter and Schalk Neethling host on their Mental Health in Tech podcast. They invited me on the show and we got deep into what it’s like to do your best work when you’re not feeling your best. We got so deep into it that we didn’t realize two hours blew right by, and the interview was split into two parts, the second of which published today.
Vulnerability and Breaking the Facade as a Balancing Act – Geoff – Part 1 by Schalk Neethling
Read on SubstackThe Emotional Rollercoaster of Tech Layoffs, Reviving CSS-Tricks, and Recovery – Geoff – Part 2 by Schalk Neethling
Read on SubstackMental Health in Tech Podcast Interview originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Quick Hit #9
Heydon with a reminder that <address> isn’t for, you know, mailing addresses.
Quick Hit #9 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
CSS Chronicles XLII
Remember these? Chris would write a post now and then to chronicle things happening around the ol’ CSS-Tricks site. It’s only been 969 days since the last one, give or take. Just think: back then we were poking at writing CSS in JavaScript and juuuuuuust starting to get excited about a set of proposed new color features that are mostly implemented today. We’re nesting CSS rules now. Container queries became an actual thing.
CSS was going gosh-darned hog wild. Probably not the “best” time for a site about CSS to take a break, eh?
That’s why I thought I’d dust off the chronicles. It’s been a hot minute and a lot is happening around CSS-Tricks today.
I’m (sorta) backWe may as well begin here! Yeah, I was “let go” last year. There was no #HotDrama. A bunch of really good folks — all in the DigitalOcean community team — were let go at the same time. It was a business decision, love it or not.
Things changed at DigitalOcean after that. A new leadership team is on board and, with it, a re-dedicated focus on re-establishing the community side of things. That, and Chris published a meaty post about the CSS-Tricks situation from his perspective. Coincidentally or not, a new job opened that looked a lot like my old gig. I had feelings about that, of course.
This little flurry of activity led to a phone call. And a few more. And now I’m back to help get the ol’ CSS-Tricks engine purring, hopefully making it the rich resource we’ve loved for so long. I’m on contract at the moment and feeling things out.
So far? Man, it feels great to be back.
What I did during the “lull”I jumped over to Smashing Magazine. Gosh, that team is incredible. It tickles me that we still have Smashing Magazine. And here’s a piece of trivia for your next front-end cocktail party: Smashing Magazine was launched in September 2006, a mere 11 months before Chris published the very first article here on CSS-Tricks.
I also spent my time teaching front-end development at a couple of colleges that are local to me where I live in Colorado. I had already been teaching but bumped up the load. But not too much because I decided this was as good a time as any to work on a master’s degree. So, I enrolled and split my days as a part-time editor, part-time educator, and part-time student.
The degree went quicker than expected, so I used the rest of my time finishing up an online course I had started a couple years earlier and finally got around to publishing it! It’s probably not the sort of course for someone reading this post, but for complete beginners who are likely writing their very first line of HTML or CSS. You ever get asked how to build a website but don’t have the energy (or time) to explain everything? Yeah, me too. That’s who this course is for. And my mom.
I call it The Basics — and I’d love it if you shared it with anyone you think might use it as a starting point into web development.
What I want for CSS-Tricks, going forwardThis site’s always been great, even long before I was brought on board. Historically, it’s been more of a personal blog turned multi-author blog with a steady stream of content. Nothing wrong with that at all.
What’s lacking, though, is structure. Most everything we publish is treated like a blog post: write it, smash the Publish button, and let it sit on top of the stream until the next blog post comes out. We’re talking about a time-based approach in which posts become a timeline of activity in reverse chronological order. Where do you find that one post you came across last month? It’s probably buried by this point and you’ve gotta either hit the post archives or try your hand searching for it by keyword. That might work for a blog with a few hundred posts, but there are more than 7,000 here and searching has become more like finding the metaphorical needle in the equally metaphorical haystack.
So, you may have noticed that I’m shuffling things around. Everything is still a “post” but we’re now using a Category taxonomy more effectively than we had been in the past. Each category is a “type” of post. And the type of post is determined by what exactly we’re trying to get out of it. Let’s actually break this out into its own section because it’s a sizeable change with some explanation around it.
The “types” of things we’re publishingOK, so everything used to be an article or an Almanac entry. We still have “articles” and “entries” but there are better ways to classify and distinguish them, most notably with articles.
This is how it shakes out:
- Articles: The tutorials that have been the CSS-Tricks bread and butter forever
- Guides: Comprehensive deep dives into a specific CSS topic (like the Flexbox guide)
- Almanac: Reference pieces for understanding CSS selectors and properties that can be cited in articles and guides.
- Notes: A post for taking notes on things we’re learning. They’re meant to be loose and a little rough around the edges, just like taking notes you’d take from a class lecture — only we’re taking notes on the things that others in the community (like you!) are writing about.
- Links: Things we’re reading that we find interesting and want to share with you. A link might evolve into a Note down the road, but they’re also useful resources that can be cited in the Almanac, a guide, or an article.
- Quick Hits: I hate this name but the idea is to have a place to post little one-liners, like a thought, an idea, or perhaps some timely news. I’m openly accepting ideas for a better name for these. 😇
This is what we’re looking at right now, but there are obviously other ways we can slice-n-dice content. For example, we have an archive of “snippets” that we’ve buried for many years but could be useful. Same with videos. And more, if you can believe it. So, there’s plenty of housekeeping to do to keep us busy! This is still very much early days. You’ll likely experience some turbulence during your flight. And I’m okay with that because this is a learning place, and the people working it are learning, too.
Yes, I did just say, “people” as in more than one person because I’d to…
Welcome a couple of new faces!The thing that excites me most — even more than the ice cream truck excites my daughters — is bringing new people along for the ride. Running CSS-Tricks is a huge job (no matter how easy I make it look 😝). So, I’ve brought on a couple of folks to help share the load!
Juan Diego Rodriguez
Ryan Trimble
I got to know Juan Diego while editing for Smashing Magazine. He had written a couple of articles for Smashing before I joined and his latest work, the first part of a series of articles discussing the “headaches” of working with Gatsby, landed on my desk. It’s really, really good — you should check it out. What you should know about Juan Diego that I’ve come know is that the dude cares a lot about the web platform. Not only that, but pays close attention to it. I’m pretty sure he reads CSSWG specifications for pleasure over tea. His love and curiosity for all-things-front-end is infectious and I’ve already learned a bunch from him. I know you will, too.
Ryan, on the other hand, is a total nerd for design systems that advocates for accessible interfaces. He actually reached out to me on Mastodon when he caught wind that I needed help. It was perfect timing and I couldn’t be more grateful that he poked me when he did. As I’ve gotten to know him, I’m realizing how versatile his skillset is. Working with “design systems” can mean lots of different things. For Ryan, it means consistent, predictable user interfaces based on modular and reusable web components — specifically web components that are native to the platform. In fact, he’s currently working on a design system called Platform UI. I’ve also become a fan of his personal blog, especially his weekly roundups of articles he finds interesting.
You’ll be seeing a lot of Juan Diego and Ryan around here! They’re both hard at work on bringing the trusty Almanac up-to-date but will be posting articles as well. No one’s full time here, me included, so it’s truly a team effort.
Please give ’em both a hearty welcome!
This is all an ongoing work in progress…and probably always will be! I love that CSS-Tricks is a place where everyone learns together. It might be directly about CSS. Maybe it’s not. Perhaps it’s only tangentially related to web development. It may even be a rough idea that isn’t fully baked, but we put it out there and learn new things together with an open mind to the fact that the web is a massive place where everyone has something to contribute and a unique perspective we all benefit from — whether it’s from a specialization in CSS, semantics, performance, accessibility, design, typography, marketing, or what have you.
Do you wanna write for CSS-Tricks?You can and you should! You get paid, readers learn something, and that gets people coming to the site. Everybody wins!
I know writing isn’t everyone’s top skill. But that’s exactly what the team is here for. You don’t have to be a superior writer, but only be willing to write something. We’ll help polish it off and make it something you’re super proud of.
More than 200 web developers, designers, and specialists just like you have written for this site. You should apply to write an article and join the club!
So, yes: CSS-Tricks is back!In its own weird way! In my perfect world, there would be no doubt whether CSS-Tricks is publishing content on any given day. But that’s not entirely up to me. It not only has to be of at least some value to people like you who depend on sites like CSS-Tricks but also to DigitalOcean. It’s a delicate dance but I think everyone’s on the same page with a shared interest of keeping this site around and healthy.
I’m stoked I get to be a part of it. And that Juan Diego and Ryan do, too. And you, as well.
We’re all in it together. 🧡
CSS Chronicles XLII originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
It’s Time To Talk About “CSS5”
Ever search for CSS info and run into some article — perhaps even one or a dozen on this site — that looks promising until you realize it was published when dinosaurs roamed the planet? The information is good, but maybe isn’t the best reflection of modern best practices?
I’ve wondered that a bunch. And so has Chris. But so has Brecht De Ruyte and he’s actually doing something about it, along with other folks who make up the W3C CSS-Next community group. I worked with him on this article for Smashing Magazine and was stoked to see how much discussion, thought, and intention have gone into “versioning” CSS.
The idea? We’d “skip” CSS4, so to speak, slapping a CSS4 label on a lot of what’s released since CSS3:
CSS3 (~2009-2012):
Level 3 CSS specs as defined by the CSSWG. (immutable)
CSS4 (~2013-2018):
Essential features that were not part of CSS3 but are already a fundamental part of CSS..
From there?
CSS5 (~2019-2024):
Newer features whose adoption is steadily growing.
CSS6 (~2025+):
Early-stage features that are planned for future CSS
The most important part of the article, though, is that you (yes, you) can help the CSS-Next community group.
We also want you to participate. Anyone is welcome to join the CSS-Next group and we could certainly use help brainstorming ideas. There’s even an incubation group that conducts a biweekly hour-long session that takes place on Mondays at 8:00 a.m. Pacific Time (2:00 p.m. GMT).
It’s Time To Talk About “CSS5” originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Steven Heller’s Font of the Month: Doublethink
Read the book, Typographic Firsts
Steven Heller takes a closer look at the Doublethink font family, from Barnbrook Fonts.
The post Steven Heller’s Font of the Month: Doublethink appeared first on I Love Typography.
Let’s make a simpler, more accessible web
Christian Heilmann gave this talk at Typo3 Developer Days. I’m linking it up because it strikes an already stricken nerve in me. The increasing complexity of web development has an inverse relationship with the decreasing number of entry points for those getting into web development.
I love how Christian compares two hypothetical development stacks.
Thenindex.html
Now- Get the right editor with all the right extensions
- Set up your terminal with the right font and all the cool dotfiles
- Install framework flügelhorn.js with bundler wolperdinger.io
- Go to the terminal and run packagestuff –g install
- Look at all the fun warning messages and update dependencies
- Doesn’t work? Go SUDO, all the cool kids are …
- Don’t bother with the size of the modules folder
- Learn the abstraction windfarm.css – it does make you so much more effective
- Use the templating language funsocks – it is much smaller than HTML
- Check out the amazing hello world example an hour later…
He’s definitely a bit glib, but the point is solid. Things are more complex today than they were, say, ten years ago. I remember struggling with Grunt back then and thinking I’d never get it right. I did eventually, and my IDE was never the same after that.
It’s easy to get swept up in the complexity, even for those with experience in the field:
This world is unfortunately becoming lost or, at least, degraded — not because it is no longer possible to view the source of a webpage, but because that source is often inscrutable, even on simple webpages.
— Pixel Envy “A View Source Web”Christian’s post reminds me that the essence of the web is not only still alive but getting better every day:
- Browsers are constantly updated.
- The web standardisation process is much faster than it used to be.
- We don’t all need to build the next killer app. Many a framework promises scaling to infinity and only a few of us will ever need that.
He goes on to suggest many ways to remove complexity and abstractions from a project. My biggest takeaway is captured by a single headline:
The web is built on resilient technologies – we just don’t use them
Which recalls what Molly White said earlier this year that there’s always an opportunity to swing the pendulum back:
The thing is: none of this is gone. Nothing about the web has changed that prevents us from going back. If anything, it’s become a lot easier. We can return. Better, yet: we can restore the things we loved about the old web while incorporating the wonderful things that have emerged since, developing even better things as we go forward, and leaving behind some things from the early web days we all too often forget when we put on our rose-colored glasses.
We can return. We can restore all the things. So, tell me: do you take the red pill or the blue one?
Let’s make a simpler, more accessible web originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
CSS Olympic Rings
It was a few years ago during the 2020 Olympics in Tokyo 2020 that I made a demo of animated 3D Olympic rings. I like it, it looks great, and I love the effect of the rings crossing each other.
CodePen Embed FallbackBut the code itself is kind of old. I wrote it in SCSS, and crookedly at that. I know it could be better, at least by modern standards.
So, I decided to build the demo again from scratch in honor of this year’s Olympics. I’m writing vanilla CSS this time, leveraging modern features like trigonometric functions for fewer magic numbers and the relative color syntax for better color management. The kicker, turns out, is that the new demo winds up being more efficient with fewer lines of code than the old SCSS version I wrote in 2020!
Look at the CSS tab in that first demo again because we’ll wind up with something vastly different — and better — with the approach we’re going to use together. So, let’s begin!
The markupWe’ll use layers to create the 3D effect. These layers are positioned one after the other (on the z-axis) to get the depth of the 3D object which, in our case, is a ring. The combination of the shape, size, and color of each layer — plus the way they vary from layer to layer — is what creates the full 3D object.
In this case, I’m using 16 layers where each layer is a different shade (with the darker layers stacked at the back) to get a simple lighting effect, and using the size and thickness of each layer to establish a round, circular shape.
As far as HTML goes, we need five <div> elements, one for each ring, where each <div> contains 16 elements that act as the layers, which I’m wrapping in <i> tags. Those five rings we’ll put in a parent container to hold things together. We’ll give the parent container a .rings class and each ring, creatively, a .ring class.
This is an abbreviated version of the HTML showing how that comes together:
<div class="rings"> <div class="ring"> <i style="--i: 1;"></i> <i style="--i: 2;"></i> <i style="--i: 3;"></i> <i style="--i: 4;"></i> <i style="--i: 5;"></i> <i style="--i: 6;"></i> <i style="--i: 7;"></i> <i style="--i: 8;"></i> <i style="--i: 9;"></i> <i style="--i: 10;"></i> <i style="--i: 11;"></i> <i style="--i: 12;"></i> <i style="--i: 13;"></i> <i style="--i: 14;"></i> <i style="--i: 15;"></i> <i style="--i: 16;"></i> </div> <!-- 4 more rings... --> </div>Note the --i custom property I’ve dropped on the style attribute of each <i> element:
<i style="--i: 1;"></i> <i style="--i: 2;"></i> <i style="--i: 3;"></i> <!-- etc. -->We’re going to use --i to calculate each layer’s position, size, and color. That’s why I’ve set their values as integers in ascending order — those will be multipliers for arranging and styling each layer individually.
Pro tip: You can avoid writing the HTML for each and every layer by hand if you’re working on an IDE that supports Emmet. But if not, no worries, because CodePen does! Enter the following into your HTML editor then press the Tab key on your keyboard to expand it into 16 layers: i*16[style="--i: $;"]
The (vanilla) CSSLet’s start with the parent .rings container for now will just get a relative position. Without relative positioning, the rings would be removed from the document flow and wind up off the page somewhere when setting absolute positioning on them.
.rings { position: relative; } .ring { position: absolute; }Let’s do the same with the <i> elements, but use CSS nesting to keep the code compact. We’ll throw in border-radius while we’re at it to clip the boxy edges to form perfect circles.
.rings { position: relative; } .ring { position: absolute; i { position: absolute; border-radius: 50%; } }The last piece of basic styling we’ll apply before moving on is a custom property for the --ringColor. This’ll make coloring the rings fairly straightforward because we can write it once, and then override it on a layer-by-layer basis. We’re declaring --ringColor on the border property because we only want coloration on the outer edges of each layer rather than filling them in completely with background-color:
.rings { position: relative; } .ring { position: absolute; --ringColor: #0085c7; i { position: absolute; inset: -100px; border: 16px var(--ringColor) solid; border-radius: 50%; } }Did you notice I snuck something else in there? That’s right, the inset property is also there and set to a negative value of 100px. That might look a little strange, so let’s talk about that first as we continue styling our work.
Negative insettingSetting a negative value on the inset property means that the layer’s position falls outside the .ring element. So, we might think of it more like an “outset” instead. In our case, the .ring has no size as there are no content or CSS properties to give it dimensions. That means the layer’s inset (or rather “outset”) is 100px in each direction, resulting in a .ring that is 200×200 pixels.
Let’s check in with what we have so far:
CodePen Embed Fallback Positioning for depthWe’re using the layers to create the impression of depth. We do that by positioning each of the 16 layers along the z-axis, which stacks elements from front to back. We’ll space each one a mere 2px apart — that’s all the space we need to create a slight visual separation between each layer, giving us the depth we’re after.
Remember the --i custom property we used in the HTML?
<i style="--i: 1;"></i> <i style="--i: 2;"></i> <i style="--i: 3;"></i> <!-- etc. -->Again, those are multipliers to help us translate each layer along the z-axis. Let’s create a new custom property that defines the equation so we can apply it to each layer:
i { --translateZ: calc(var(--i) * 2px); }What do we apply it to? We can use the CSS transform property. This way, we can rotate the layers vertically (i.e., rotateY()) while translating them along the z-axis:
i { --translateZ: calc(var(--i) * 2px); transform: rotateY(-45deg) translateZ(var(--translateZ)); } Color for shadingFor color shading, we’ll darken the layers according to their position so that the layers get darker as we move from the front of the z-axis to the back. There are a few ways to do it. One is dropping in another black layer with decreasing opacity. Another is modifying the “lightness” channel in a hsl() color function where the value is “lighter” up front and incrementally darker towards the back. A third option is playing with the layer’s opacity, but that gets messy.
Even though we have those three approaches, I think the modern CSS relative color syntax is the best way to go. We’ve already defined a default --ringColor custom property. We can put it through the relative color syntax to manipulate it into other colors for each ring <i> layer.
First, we need a new custom property we can use to calculate a “light” value:
.ring { --ringColor: #0085c7; i { --light: calc(var(--i) / 16); border: 16px var(--ringColor) solid; } }We’ll use the calc()-ulated result in another custom property that puts our default --ringColor through the relative color syntax where the --light custom property helps modify the resulting color’s lightness.
.ring { --ringColor: #0085c7; i { --light: calc(var(--i) / 16); --layerColor: rgb(from var(--ringColor) calc(r * var(--light)) calc(g * var(--light)) calc(b * var(--light))); border: 16px var(--ringColor) solid; } }That’s quite an equation! But it only looks complex because the relative color syntax needs arguments for each channel in the color (RGB) and we’re calculating each one.
rgb(from origin-color channelR channelG channelB)As far as the calculations go, we multiply each RGB channel by the --light custom property, which is a number between 0 and 1 divided by the number of layers, 16.
Time for another check to see where we’re at:
CodePen Embed Fallback Creating the shapeTo get the circular ring shape, we’ll set the layer’s size (i.e., thickness) with the border property. This is where we can start using trigonometry in our work!
We want the thickness of each ring to be a value between 0deg to 180deg — since we’re only actually making half of a circle — so we will divide 180deg by the number of layers, 16, which comes out to 11.25deg. Using the sin() trigonometric function (which is equivalent to the opposite and hypotenuse sides of a right angle), we get this expression for the layer’s --size:
--size: calc(sin(var(--i) * 11.25deg) * 16px);So, whatever --i is in the HTML, it acts as a multiplier for calculating the layer’s border thickness. We have been declaring the layer’s border like this:
i { border: 16px var(--ringColor) solid; )Now we can replace the hard-coded 16px value with --size calculation:
i { --size: calc(sin(var(--i) * 11.25deg) * 16px); border: var(--size) var(--layerColor) solid; )But! As you may have noticed, we aren’t changing the layer’s size when we change its border width. As a result, the round profile only appears on the layer’s inner side. The key thing here is understanding that setting the --size with the inset property which means it does not affect the element’s box-sizing. The result is a 3D ring for sure, but most of the shading is buried.
⚠️ Auto-playing mediaWe can bring the shading out by calculating a new inset for each layer. That’s kind of what I did in the 2020 version, but I think I’ve found an easier way: add an outline with the same border values to complete the arc on the outer side of the ring.
i { --size: calc(sin(var(--i) * 11.25deg) * 16px); border: var(--size) var(--layerColor) solid; outline: var(--size) var(--layerColor) solid; }We have a more natural-looking ring now that we’ve established an outline:
CodePen Embed Fallback Animating the ringsI had to animate the ring in that last demo to compare the ring’s shading before and after. We’ll use that same animation in the final demo, so let’s break down how I did that before we add the other four rings to the HTML
I’m not trying to do anything fancy; I’m just setting the rotation on the y-axis from -45deg to 45deg (the translateZ value remains constant).
@keyframes ring { from { transform: rotateY(-45deg) translateZ(var(--translateZ, 0)); } to { transform: rotateY(45deg) translateZ(var(--translateZ, 0)); } }As for the animation property, I’ve given named it ring , and a hard-coded (at least for now) a duration of 3s, that loops infinitely. Setting the animation’s timing function with ease-in-out and alternate, respectively, gives us a smooth back-and-forth motion.
i { animation: ring 3s infinite ease-in-out alternate; }That’s how the animation works!
Adding more ringsNow we can add the remaining four rings to the HTML. Remember, we have five rings total and each ring contains 16 <i> layers. It could look as simple as this:
<div class="rings"> <div class="ring"> <!-- <i> layers --> </div> <div class="ring"> <!-- <i> layers --> </div> <div class="ring"> <!-- <i> layers --> </div> <div class="ring"> <!-- <i> layers --> </div> <div class="ring"> <!-- <i> layers --> </div> </div>There’s something elegant about the simplicity of this markup. And we could use the CSS nth-child() pseudo-selector to select them individually. I like being a bit more declarative than that and am going to give each .ring and additional class we can use to explicitly select a given ring.
<div class="rings"> <div class="ring ring__1"> <!-- layers --> </div> <div class="ring ring__2"> <!-- layers --> </div> <div class="ring ring__3"> <!-- layers --> </div> <div class="ring ring__4"> <!-- layers --> </div> <div class="ring ring__5"> <!-- layers --> </div> </div>Our task now is to adjust each ring individually. Right now, everything looks like the first ring we made together. We’ll use the unique classes we just set in the HTML to give them their own color, position, and animation duration.
The good news? We’ve been using custom properties this entire time! All we have to do is update the values in each ring’s unique class.
.ring { &.ring__1 { --ringColor: #0081c8; --duration: 3.2s; --translate: -240px, -40px; } &.ring__2 { --ringColor: #fcb131; --duration: 2.6s; --translate: -120px, 40px; } &.ring__3 { --ringColor: #444444; --duration: 3.0s; --translate: 0, -40px; } &.ring__4 { --ringColor: #00a651; --duration: 3.4s; --translate: 120px, 40px; } &.ring__5 { --ringColor: #ee334e; --duration: 2.8s; --translate: 240px, -40px; } }If you’re wondering where those --ringColor values came from, I based them on the International Olympic Committee’s documented colors. Each --duration is slightly offset from one another to stagger the movement between rings, and the rings are --translate‘d 120px apart and then staggered vertically by alternating their position 40px and -40px.
Let’s apply the translation stuff to the .ring elements:
.ring { transform: translate(var(--translate)); }Earlier, we set the animation’s duration to a hard-coded three seconds:
i { animation: ring 3s infinite ease-in-out alternate; }This is the time to replace that with a custom property that calculates the duration for each ring separately.
i { animation: ring var(--duration) -10s infinite ease-in-out alternate; }Whoa, whoa! What’s the -10s value doing in there? Even though each ring layer is set to animate for a different duration, the starting angle of the animations is all the same. Adding a constant negative delay on changing durations will make sure that each ring’s animation starts at a different angle.
Now we have something that is almost finished:
CodePen Embed Fallback Some final touchesWe’re at the final stretch! The animation looks pretty great as-is, but I want to add two more things. The first one is a small-10deg “tilt” on the x-axis of the parent .rings container. This will make it look like we’re viewing things from a higher perspective.
.rings { rotate: x -10deg; }The second finishing touch has to do with shadows. We can really punctuate the 3D depth of our work and all it takes is selecting the .ring element’s ::after pseudo-element and styling it like a shadow.
First, we’ll set the width of the pseudos’ border and outline to a constant (24px) while setting the color to a semi-transparent black (#0003). Then we’ll translate them so they appear to be further away. We’ll also inset them so they line up with the actual rings. Basically, we’re shifting the pseudo-elements around relative to the actual element.
.ring { /* etc. */ &::after { content: ''; position: absolute; inset: -100px; border: 24px #0003 solid; outline: 24px #0003 solid; translate: 0 -100px -400px; } }The pseudos don’t look very shadow-y at the moment. But they will if we blur() them a bit:
.ring { /* etc. */ &::after { content: ''; position: absolute; inset: -100px; border: 24px #0003 solid; outline: 24px #0003 solid; translate: 0 -100px -400px; filter: blur(12px); } }The shadows are also pretty box-y. Let’s make sure they’re round like the rings:
.ring { /* etc. */ &::after { content: ''; position: absolute; inset: -100px; border: 24px #0003 solid; outline: 24px #0003 solid; translate: 0 -100px -400px; filter: blur(12px); border-radius: 50%; } }Oh, and we ought to set the same animation on the pseudo so that the shadows move in harmony with the rings:
.ring { /* etc. */ &::after { content: ''; position: absolute; inset: -100px; border: 24px #0003 solid; outline: 24px #0003 solid; translate: 0 -100px -400px; filter: blur(12px); border-radius: 50%; animation: ring var(--duration) -10s infinite ease-in-out alternate; } } Final demoLet’s stop and admire our completed work:
CodePen Embed FallbackAt the end of the day, I’m really happy with the 2024 version of the Olympic rings. The 2020 version got the job done and was probably the right approach for that time. But with all of the features we’re getting in modern CSS today, I had plenty of opportunities to improve the code so that it is not only more efficient but more reusable — for example, this could be used in another project and “themed” simply by updating the --ringColor custom property.
Ultimately, this exercise proved to me the power and flexibility of modern CSS. We took an existing idea with complexities and recreated it with simplicity and elegance.
CSS Olympic Rings originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
(Hyper) Links About (Hyper) Links
Heydon on the virtues of hyperlinking hypertext in an anchor element:
Sometimes, the <a> is referred to as a hyperlink, or simply a link. But it is not one of these and people who say it is one are technically wrong (the worst kind of wrong).
[…]
An <a> is an interactive element (well, it is if it has an href). The text inside an interactive element is sometimes referred to as a label since it should tell you what the element does. Since anchors take you places on the web, the text should tell you where you would be going or what you can do there.
[…]
Web developers and content editors, the world over, make the mistake of not making text that describes a link actually go inside that link. This is collosally [sic] unfortunate, given it’s the main thing to get right when writing hypertext.
As far as where that anchor hyperlinks to, Jim Nielsen back in 2003 discussed a bunch of considerations that go into designing URLs. More recently, he’s mused on the the potential of well-designed URLs to change — or more accurately, the potential of humans to change things:
If a slug is going to be human-friendly, i.e. human-readable, then it’s going to contain information that is subject to change because humans make errors.
Swapping the contents of a URL is a breaking change. If we were to start with a wonderful URL like, say:
<a href=“css-tricks.com/almanac”>…but decide that we now like “Docs” instead of “Almanac” then we might do this:
<a href=“css-tricks.com/docs”>Naturally, we’d drop some sorta redirect on the server so that anyone attempting to hit /almanac is automatically directed to /docs instead. But now we’ve got a form of technical debt to maintain that may not be any more dangerous than walking and chewing gum at the same time, but could become a mouthful much later. We’ve got a gazillion redirects on CSS-Tricks for a gazillion different reasons, most often for totally human reasons like typos. Remember the CSS-Tricks Chronicles we used to write? Botching the Roman numeral numbering system on those was standard fare. Look at the very last edition from 2001, titled “CSS-Tricks Choronicles XLI” and its URL:
https://css-tricks.com/css-tricks-chronicle-xxxxi/🥸
I’ve been thinking about this a lot while attempting to organize the 7,000 some-odd articles on this site. For years, we’ve maintained a “flat” structure in the sense that the title of an article becomes the URL (after, perhaps, with some light editing):
<a href=“css-tricks.com/geoff-is-on-another-dumb-rant”>But I’m starting to think about the content on this site in terms of type rather than title alone. For example, we’ve always had “articles” on this site with a smattering of “links” sprinkled in alongside Almanac “entries” and “guides” among other categories of content. We’ve just never reflected that in our URLs because, well, the design is flat. Adding another layer for the type of content borks the original URL!
<a href=“css-tricks.com/soapbox/geoff-is-on-another-dumb-rant”>Jay Hoffman has been thinking about this, too.
A dead link may not seem like it means very much, even in the aggregate. But they are. One-way links, the way they exist on the web where anyone can link to anything, is what makes the web universal. In fact, the first name for URL’s was URI’s, or Universal Resource Identifier. It’s right there in the name. And as Berners-Lee once pointed out, “its universality is essential.”
[…]
Time and time again, when the web goes into crisis and part of it is lost, the Internet Archive and similar efforts come to the rescue. But even the Internet Archive is having a hard time protecting against a barrage of link rot we can’t seem to get away from.
All of this dovetails into recent reporting that Google has decided to sunset its URL shortener. All of those goo.gl URLs accumulated since the shortener was introduced in 2018?
Any developers using links built with the Google URL Shortener in the form https://goo.gl/* will be impacted, and these URLs will no longer return a response after August 25th, 2025. We recommend transitioning these links to another URL shortener provider.
There’s some minutiae of consolation for Google itself:
Note that goo.gl links generated via Google apps (such as Maps sharing) will continue to function.
To be clear, this move is less a form of link rot than it is a straight-up pruning to cut things off. If link rot is akin to allowing your hair to go gray, then deprecating Google’s URL shortener is a total head shave. Nick Heer believes there’s a good side to it, however:
In principle, I support this deprecation because it is confusing and dangerous for Google’s own shortened URLs to have the same domain as ones created by third-party users. But this is a Google-created problem because it designed its URLs poorly. It should have never been possible for anyone else to create links with the same URL shortener used by Google itself.
I tend to agree. The whole situation is a Rosemary’s Baby predicament presenting two terribly uncomfortable choices. The right uncomfortable decision was made, but we still have to deal with the repercussions of wiping out part of the web’s context.
Heydon’s post led me down this rabbit trail, so I’ll link it up here for you to take a hike with it.
(Hyper) Links About (Hyper) Links originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
HTML Web Components Make Progressive Enhancement and CSS Encapsulation Easier!
I have to thank Jeremy Keith and his wonderfully insightful article from late last year that introduced me to the concept of HTML Web Components. This was the “a-ha!” moment for me:
When you wrap some existing markup in a custom element and then apply some new behaviour with JavaScript, technically you’re not doing anything you couldn’t have done before with some DOM traversal and event handling. But it’s less fragile to do it with a web component. It’s portable. It obeys the single responsibility principle. It only does one thing but it does it well.
Until then, I’d been under the false assumption that all web components rely solely on the presence of JavaScript in conjunction with the rather scary-sounding Shadow DOM. While it is indeed possible to author web components this way, there is yet another way. A better way, perhaps? Especially if you, like me, advocate for progressive enhancement. HTML Web Components are, after all, just HTML.
While it’s outside the exact scope of what we’re discussing here, Andy Bell has a recent write-up that offers his (excellent) take on what progressive enhancement means.
Let’s look at three specific examples that show off what I think are the key features of HTML Web Components — CSS style encapsulation and opportunities for progressive enhancement — without being forced to depend on JavaScript to work out of the box. We will most definitely use JavaScript, but the components ought to work without it.
The examples can all be found in my Web UI Boilerplate component library (built using Storybook), along with the associated source code in GitHub.
Example 1: <webui-disclosure> Live demoI really like how Chris Ferdinandi teaches building a web component from scratch, using a disclosure (show/hide) pattern as an example. This first example extends his demo.
Let’s start with the first-class citizen, HTML. Web components allow us to establish custom elements with our own naming, which is the case in this example with a <webui-disclosure> tag we’re using to hold a <button> designed to show/hide a block of text and a <div> that holds the <p> of text we want to show and hide.
<webui-disclosure data-bind-escape-key data-bind-click-outside > <button type="button" class="button button--text" data-trigger hidden > Show / Hide </button> <div data-content> <p>Content to be shown/hidden.</p> </div> </webui-disclosure>If JavaScript is disabled or doesn’t execute (for any number of possible reasons), the button is hidden by default — thanks to the hidden attribute on it— and the content inside of the div is simply displayed by default.
Nice. That’s a really simple example of progressive enhancement at work. A visitor can view the content with or without the <button>.
I mentioned that this example extends Chris Ferdinandi’s initial demo. The key difference is that you can close the element either by clicking the keyboard’s ESC key or clicking anywhere outside the element. That’s what the two [data-attribute]s on the <webui-disclosure tag are for.
We start by defining the custom element so that the browser knows what to do with our made-up tag name:
customElements.define('webui-disclosure', WebUIDisclosure);Custom elements must be named with a dashed-ident, such as <my-pizza> or whatever, but as Jim Neilsen notes, by way of Scott Jehl, that doesn’t exactly mean that the dash has to go between two words.
I typically prefer using TypeScript for writing JavaScript to help eliminate stupid errors and enforce some degree of “defensive” programming. But for the sake of simplicity, the structure of the web component’s ES Module looks like this in plain JavaScript:
default class WebUIDisclosure extends HTMLElement { constructor() { super(); this.trigger = this.querySelector('[data-trigger]'); this.content = this.querySelector('[data-content]'); this.bindEscapeKey = this.hasAttribute('data-bind-escape-key'); this.bindClickOutside = this.hasAttribute('data-bind-click-outside'); if (!this.trigger || !this.content) return; this.setupA11y(); this.trigger?.addEventListener('click', this); } setupA11y() { // Add ARIA props/state to button. } // Handle constructor() event listeners. handleEvent(e) { // 1. Toggle visibility of content. // 2. Toggle ARIA expanded state on button. } // Handle event listeners which are not part of this Web Component. connectedCallback() { document.addEventListener('keyup', (e) => { // Handle ESC key. }); document.addEventListener('click', (e) => { // Handle clicking outside. }); } disconnectedCallback() { // Remove event listeners. } }Are you wondering about those event listeners? The first one is defined in the constructor() function, while the rest are in the connectedCallback() function. Hawk Ticehurst explains the rationale much more eloquently than I can.
This JavaScript isn’t required for the web component to “work” but it does sprinkle in some nice functionality, not to mention accessibility considerations, to help with the progressive enhancement that allows the <button> to show and hide the content. For example, JavaScript injects the appropriate aria-expanded and aria-controls attributes enabling those who rely on screen readers to understand the button’s purpose.
That’s the progressive enhancement piece to this example.
For simplicity, I have not written any additional CSS for this component. The styling you see is simply inherited from existing global scope or component styles (e.g., typography and button).
However, the next example does have some extra scoped CSS.
Example 2: <webui-tabs>That first example lays out the progressive enhancement benefits of HTML Web Components. Another benefit we get is that CSS styles are encapsulated, which is a fancy way of saying the CSS doesn’t leak out of the component. The styles are scoped purely to the web component and those styles will not conflict with other styles applied to the current page.
Let’s turn to a second example, this time demonstrating the style encapsulating powers of web components and how they support progressive enhancement in user experiences. We’ll be using a tabbed component for organizing content in “panels” that are revealed when a panel’s corresponding tab is clicked — the same sort of thing you’ll find in many component libraries.
Live demoStarting with the HTML structure:
<webui-tabs> <div data-tablist> <a href="#tab1" data-tab>Tab 1</a> <a href="#tab2" data-tab>Tab 2</a> <a href="#tab3" data-tab>Tab 3</a> </div> <div id="tab1" data-tabpanel> <p>1 - Lorem ipsum dolor sit amet consectetur.</p> </div> <div id="tab2" data-tabpanel> <p>2 - Lorem ipsum dolor sit amet consectetur.</p> </div> <div id="tab3" data-tabpanel> <p>3 - Lorem ipsum dolor sit amet consectetur.</p> </div> </webui-tabs>You get the idea: three links styled as tabs that, when clicked, open a tab panel holding content. Note that each [data-tab] in the tab list targets an anchor link matching a tab panel ID, e.g., #tab1, #tab2, etc.
We’ll look at the style encapsulation stuff first since we didn’t go there in the last example. Let’s say the CSS is organized like this:
webui-tabs { [data-tablist] { /* Default styles without JavaScript */ } [data-tab] { /* Default styles without JavaScript */ } [role='tablist'] { /* Style role added by JavaScript */ } [role='tab'] { /* Style role added by JavaScript */ } [role='tabpanel'] { /* Style role added by JavaScript */ } }See what’s happening here? We have two style rules — [data-tablist] and [data-tab] — that contain the web component’s default styles. In other words, these styles are there regardless of whether JavaScript loads or not. Meanwhile, the other three style rules are selectors that are injected into the component as long as JavaScript is enabled and supported. This way, the last three style rules are only applied if JavaScript plops the **role** attribute on those elements in the HTML. Right there, we’re already supplying a touch of progressive enhancement by setting styles only when JavasScript is needed.
All these styles are fully encapsulated, or scoped, to the <webui-tabs> web component. There is no “leakage” so to speak that would bleed into the styles of other web components, or even to anything else on the page within the global scope. We can even choose to forego classnames, complex selectors, and methodologies like BEM in favour of simple descendent selectors for the component’s children, allowing us to write styles more declaratively on semantic elements.
Quickly: “Light” DOM versus Shadow DOMFor most web projects, I generally prefer to bundle CSS (including the web component Sass partials) into a single CSS file so that the component’s default styles are available in the global scope, even if the JavaScript doesn’t execute.
However, it is possible to import a stylesheet via JavaScript that is only consumed by this web component if JavaScript is available:
import styles from './styles.css'; class WebUITabs extends HTMLElement { constructor() { super(); this.adoptedStyleSheets = [styles]; } } customElements.define('webui-tabs', WebUITabs);Alternatively, we could inject a <style> tag containing the component’s styles instead:
class WebUITabs extends HTMLElement { connectedCallback() { this.attachShadow({ mode: 'open' }); // Required for JavaScript access this.shadowRoot.innerHTML = ` <style> <!-- styles go here --> </style> // etc. `; } } customElements.define('webui-tabs', WebUITabs);Whichever method you choose, these styles are scoped directly to the web component, preventing component styles from leaking out, but allowing global styles to be inherited.
Now consider this simple example. Everything we write in between the component’s opening and closing tags is considered part of the “Light” DOM.
<my-web-component> <!-- This is Light DOM --> <div> <p>Some content... styles are inherited from the global scope</p> </div> ----------- Shadow DOM Boundary ------------- | <!-- Anything injected by JavaScript --> | --------------------------------------------- </my-web-component>Dave Rupert has an excellent write-up that makes it really easy to see how external styles are able to “pierce” the Shadow DOM and select an element in the Light DOM. Notice how the <button> element that is written in between the custom element’s tags receives the button selector’s styles in the global CSS, while the <button> injected via JavaScript is left untouched.
If we want to style the Shadow DOM <button> we’d have to do that with internal styles like the examples above for importing a stylesheet or injecting an inline <style> block.
That doesn’t mean that all CSS style properties are blocked by the Shadow DOM. In fact, Dave outlines 37 properties that web components inherit, mostly along the lines of text, list, and table formatting.
Progressively enhance the tabbed component with JavaScriptEven though this second example is more about style encapsulation, it’s still a good opportunity to see the progressive enhancement we get practically for free from web components. Let’s step into the JavaScript now so we can see how we can support progressive enhancement. The full code is quite lengthy, so I’ve abbreviated things a bit to help make the points a little clearer.
default class WebUITabs extends HTMLElement { constructor() { super(); this.tablist = this.querySelector('[data-tablist]'); this.tabpanels = this.querySelectorAll('[data-tabpanel]'); this.tabTriggers = this.querySelectorAll('[data-tab]'); if ( !this.tablist || this.tabpanels.length === 0 || this.tabTriggers.length === 0 ) return; this.createTabs(); this.tabTriggers.forEach((tabTrigger, index) => { tabTrigger.addEventListener('click', (e) => { this.bindClickEvent(e); }); tabTrigger.addEventListener('keydown', (e) => { this.bindKeyboardEvent(e, index); }); }); } createTabs() { // 1. Hide all tabpanels initially. // 2. Add ARIA props/state to tabs & tabpanels. } bindClickEvent(e) { e.preventDefault(); // Show clicked tab and update ARIA props/state. } bindKeyboardEvent(e, index) { e.preventDefault(); // Handle keyboard ARROW/HOME/END keys. } } customElements.define('webui-tabs', WebUITabs);The JavaScript injects ARIA roles, states, and props to the tabs and content blocks for screen reader users, as well as extra keyboard bindings so we can navigate between tabs with the keyboard; for example, the TAB key is reserved for accessing the component’s active tab and any focusable content inside the active tabpanel, and the tabs can be traversed with the ARROW keys. So, if JavaScript fails to load, the default experience is still an accessible one where the tabs still anchor link to their respective panels, and those panels naturally stack vertically, one on top of the other.
And if JavaScript is enabled and supported? We get an enhanced experience, complete with updated accessibility considerations.
Example 3: <webui-ajax-loader> Live demoThis final example differs from the previous two in that it is entirely generated by JavaScript, and uses the Shadow DOM. This is because it is only used to indicate a “loading” state for Ajax requests, and is therefore only needed when JavaScript is enabled.
The HTML markup is just the opening and closing component tags:
<webui-ajax-loader></webui-ajax-loader>The simplified JavaScript structure:
default class WebUIAjaxLoader extends HTMLElement { constructor() { super(); const shadow = this.attachShadow({ mode: 'open' }); shadow.innerHTML = ` <svg role="img" part="svg"> <title>loading</title> <circle cx="50" cy="50" r="47" /> </svg> `; } } customElements.define('webui-ajax-loader',WebUIAjaxLoader);Notice right out of the gate that everything in between the <webui-ajax-loader> tags is injected with JavaScript, meaning it’s all in the Shadow DOM, encapsulated from other scripts and styles not directly bundled with the component.
But also notice the part attribute that’s set on the <svg> element. Here’s where we’ll zoom in:
<svg role="img" part="svg"> <!-- etc. --> </svg>That’s yet another way we have to style the custom element: named parts. Now we can style that SVG from outside of the template literal we used to establish the element. There’s a ::part pseudo-selector to make that happen:
webui-ajax-loader::part(svg) { // Shadow DOM styles for the SVG... }And here’s something cool: that selector can access CSS custom properties, whether they are scoped globally or locally to the element.
webui-ajax-loader { --fill: orangered; } webui-ajax-loader::part(svg) { fill: var(--fill); }As far as progressive enhancement goes, JavaScript supplies all of the HTML. That means the loader is only rendered if JavaScript is enabled and supported. And when it is, the SVG is added, complete with an accessible title and all.
Wrapping upThat’s it for the examples! What I hope is that you now have the same sort of epiphany that I had when reading Jeremy Keith’s post: HTML Web Components are an HTML-first feature.
Of course, JavaScript does play a big role, but only as big as needed. Need more encapsulation? Want to sprinkle in some UX goodness when a visitor’s browser supports it? That’s what JavaScript is for and that’s what makes HTML Web Components such a great addition to the web platform — they rely on vanilla web languages to do what they were designed to do all along, and without leaning too heavily on one or the other.
HTML Web Components Make Progressive Enhancement and CSS Encapsulation Easier! originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
CSS Functions and Mixins Module Notes
Most days, I’m writing vanilla CSS. Thanks to CSS variables and nesting, I have fewer reasons to reach for Sass or any other preprocessor. The times I reach for Sass tend to be when I need a @mixin to loop through a list of items or help keep common styles DRY.
That could change for me in the not-so-distant future since a new CSS Functions and Mixins Module draft was published in late June after the CSSWG resolved to adopt the proposal back in February.
Notice the module’s name: Functions and Mixins. There’s a distinction between the two.
This is all new and incredibly unbaked at the moment with plenty of TODO notes in the draft and points to consider in future drafts. The draft spec doesn’t even have a definition for mixins yet. It’ll likely be some time before we get something real to work and experiment with, but I like trying to wrap my mind around these sorts of things while they’re still in early days, knowing things are bound to change.
In addition to the early draft spec, Miriam Suzanne published a thorough explainer that helps plug some of the information gaps. Miriam’s an editor on the spec, so I find anything she writes about this to be useful context.
There’s a lot to read! Here are my key takeaways…
Custom functions are advanced custom propertiesWe’re not talking about the single-purpose, built-in functions we’ve come to love in recent years — e.g., calc(), min(), max(), etc. Instead, we’re talking about custom functions defined with an @function at-rule that contains logic for returning an expected value.
That makes custom functions a lot like a custom property. A custom property is merely a placeholder for some expected value that we usually define up front:
:root { --primary-color: hsl(25 100% 50%); }Custom functions look pretty similar, only they’re defined with @function and take parameters. This is the syntax currently in the draft spec:
@function <function-name> [( <parameter-list> )]? { <function-rules> result: <result>; }The result is what the ultimate value of the custom function evaluates to. It’s a little confusing to me at the moment, but how I’m processing this is that a custom function returns a custom property. Here’s an example straight from the spec draft (slightly modified) that calculates the area of a circle:
@function --circle-area(--r) { --r2: var(--r) * var(--r); result: calc(pi * var(--r2)); }Calling the function is sort of like declaring a custom property, only without var() and with arguments for the defined parameters:
.element { inline-size: --circle-area(--r, 1.5rem); /* = ~7.065rem */ }Seems like we could achieve the same thing as a custom property with current CSS features:
:root { --r: 1rem; --r2: var(--r) * var(--r); --circle-area: calc(pi * var(--r2)); } .element { inline-size: var(--circle-area, 1.5rem); }That said, the reasons we’d reach for a custom function over a custom property are that (1) they can return one of multiple values in a single stroke, and (2) they support conditional rules, such as @supports and @media to determine which value to return. Check out Miriam’s example of a custom function that returns one of multiple values based on the inline size of the viewport.
/* Function name */ @function --sizes( /* Array of possible values */ --s type(length), --m type(length), --l type(length), /* The returned value with a default */ ) returns type(length) { --min: 16px; /* Conditional rules */ @media (inline-size < 20em) { result: max(var(--min), var(--s, 1em)); } @media (20em < inline-size < 50em) { result: max(var(--min), var(--m, 1em + 0.5vw)); } @media (50em < inline-size) { result: max(var(--min), var(--l, 1.2em + 1vw)); } }Miriam goes on to explain how a comma-separated list of parameters like this requires additional CSSWG work because it could be mistaken as a compound selector.
Mixins help maintain DRY, reusable style blocksMixins feel more familiar to me than custom functions. Years of writing Sass mixins will do that to you, and indeed, is perhaps the primary reason I still reach for Sass every now and then.
Mixins sorta look like the new custom functions. Instead of @function we’re working with @mixin which is exactly how it works in Sass.
/* Custom function */ @function <function-name> [( <parameter-list> )]? { <function-rules> result: <result>; } /* CSS/Sass mixin */ @mixin <mixin-name> [( <parameter-list> )]? { <mixin-rules> }So, custom functions and mixins are fairly similar but they’re certainly different:
- Functions are defined with @function; mixins are defined with @mixin but are both named with a dashed ident (e.g. --name).
- Functions result in a value; mixins result in style rules.
This makes mixins ideal for abstracting styles that you might use as utility classes, say a class for hidden text that is read by screenreaders:
.sr-text { position: absolute; left: -10000px; top: auto; width: 1px; height: 1px; overflow: hidden; }In true utility fashion, we can sprinkle this class on elements in the HTML to hide the text.
<a class="sr-text">Skip to main content</a>Super handy! But as any Tailwind-hater will tell you, this can lead to ugly markup that’s difficult to interpret if we rely on many utility classes. Screereader text isn’t in too much danger of that, but a quick example from the Tailwind docs should illustrate that point:
<div class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg">It’s a matter of preference, really. But back to mixins! The deal is that we can use utility classes almost as little CSS snippets to build out other style rules and maintain a clearer separation between markup and styles. If we take the same .sr-text styles from before and mixin-erize them (yep, I’m coining this):
@mixin --sr-text { position: absolute; left: -10000px; top: auto; width: 1px; height: 1px; overflow: hidden; }Instead of jumping into HTML to apply the styles, we can embed them in other CSS style rules with a new @apply at-rule:
header a:first-child { @apply --sr-text; /* Results in: */ position: absolute; left: -10000px; top: auto; width: 1px; height: 1px; overflow: hidden; }Perhaps a better example is something every project seems to need: centering something!
@mixin --center-me { display: grid; place-items: center; }This can now be part of a bigger ruleset:
header { @apply --center-me; /* display: grid; place-items: center; */ background-color: --c-blue-50; color: --c-white; /* etc. */ }That’s different from Sass which uses @include to call the mixin instead of @apply. We can even return larger blocks of styles, such as styles for an element’s ::before and ::after pseudos:
@mixin --center-me { display: grid; place-items: center; position: relative; &::after { background-color: hsl(25 100% 50% / .25); content: ""; height: 100%; position: absolute; width: 100%; } }And, of course, we saw that mixins accept argument parameters just like custom functions. You might use arguments if you want to loosen up the styles for variations, such as defining consistent gradients with different colors:
@mixin --gradient-linear(--color-1, --color-2, --angle) { /* etc. */ }We’re able to specify the syntax for each parameter as a form of type checking:
@mixin --gradient-linear( --color-1 type(color), --color-2 type(color), --angle type(angle), ) { /* etc. */ }We can abstract those variables further and set default values on them:
@mixin --gradient-linear( --color-1 type(color), --color-2 type(color), --angle type(angle), ) { --from: var(--color-1, orangered); --to: var(--from-color, goldenrod); --angle: var(--at-angle, to bottom right); /* etc. */ }…then we write the mixin’s style rules with the parameters as variable placeholders.
@mixin --gradient-linear( --color-1 type(color), --color-2 type(color), --angle type(angle), ) { --from: var(--color-1, orangered); --to: var(--from-color, goldenrod); --angle: var(--at-angle, to bottom right); background: linear-gradient(var(--angle), var(--from), var(--to)); }Sprinkle conditional logic in there if you’d like:
@mixin --gradient-linear( --color-1 type(color), --color-2 type(color), --angle type(angle), ) { --from: var(--color-1, orangered); --to: var(--from-color, goldenrod); --angle: var(--at-angle, to bottom right); background: linear-gradient(var(--angle), var(--from), var(--to)); @media (prefers-contrast: more) { background: color-mix(var(--from), black); color: white; } }This is all set to @apply the mixin in any rulesets we want:
header { @apply --gradient-linear; /* etc. */ } .some-class { @apply --gradient-linear; /* etc. */ }…and combine them with other mixins:
header { @apply --gradient-linear; @apply --center-me; /* etc. */ }This is all very high level. Miriam gets into the nuances of things like:
- Applying mixins at the root level (i.e., not in a selector)
- Working with Container Queries with the limitation of having to set global custom properties on another element than the one that is queried.
- The possibility of conditionally setting mixin parameters with something like @when/@else in the mixin. (Which makes me wonder about the newly-proposed if() function and whether it would be used in place of @when.)
- Why we might draw a line at supporting loops the same way Sass does. (CSS is a declarative language and loops are imperative flows.)
- Scoping mixins (@layer? scope? Something else?)
Miriam has an excellent outline of the open questions and discussions happening around mixins.
That’s, um, it… at least for now.Gah, this is a lot for my blonde brain! Anytime I’m neck-deep in CSS specification drafts, I have to remind myself that the dust is still settling. The spec authors and editors are wrestling with a lot of the same questions we have — and more! — so it’s not like a cursory read of the drafts is going to make experts out of anyone. And that’s before we get to the fact that things can, and likely will, change by the time it all becomes a recommended feature for browsers to implement.
This will be an interesting space to watch, which is something you can do with the following resources:
- Proposal: Custom CSS Functions & Mixins (GitHub Issue #9350)
- CSS Mixins & Functions Explainer (Miriam Suzanne)
- Layered Toggles: Optional CSS Mixins (Roman Komarov)
- All GitHub issues tagged css-mixins
CSS Functions and Mixins Module Notes originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Where You Can Still Get A Book Apart Titles
It’s been a few months out since A Book Apart closed shop. I’m sad about it, of course. You probably are, too, if you have one of their many brightly-colored paperbacks sitting on a bookshelf strategically placed as a backdrop for your video calls.
It looked for a bit like the books would still be available for purchase through third-party distributors who could print them on demand or whatever. And then a redaction on A Book Apart’s original announcement:
UPDATE: Ownership and publishing rights for all books have been given back to their respective authors. Many authors are continuing to offer their work for free or in new editions. Our hope is that these books will continue to live on forever. A Book Apart no longer sells or distributes books, please reach out to authors for information about availability.
Oh, snap. The books are on the loose and several authors are making sure they’re still available. Eric Meyer, for example, says he and co-author Sara Wachter-Boettcher still figuring out what’s next for their Design for Real Life title:
One of the things Sara and I have decided to do is to eventually put the entire text online for free, as a booksite. That isn’t ready yet, but it should be coming somewhere down the road.
In the meantime, we’ve decided to cut the price of print and e-book copies available through Ingram. [Design for Real Life] was the eighteenth book [A Book Apart] put out, so we’ve decided to make the price of both the print and e-book $18, regardless of whether those dollars are American, Canadian, or Australian.
Ethan Marcotte has followed suit by listing his three titles on his personal website and linking up where they can be purchased at a generous discount off the original price tag, including his latest, You Deserve a Tech Union.
Others have quickly responded with free online versions of their books. Mat Marquis has offered JavaScript for Web Designers free online for a long time. He helped Chris Coyier do the same with Practical SVG this past week. Jeremy Keith put out one of my personal ABA faves (and the first ever ABA-published book) for free, HTML5 for Web Designers.
What about all the other titles? I dunno. A Book Apart simply doesn’t sell or distribute them anymore. Rachel McConnell sells Leading Content Design directly. Every other book I checked seems to be a link back to A Book Apart. We’ll have to see where the proverbial dust settles. The authors now hold all the rights to their works and may or may not decide to re-offer them. Meanwhile, many of the titles are listed in places like Goodreads, Amazon, Barnes & Noble, etc.
A couple of folks have even started tracking the books on their personal sites, like Ryan Trimble and Alan Dalton. (Thanks for the tip, Chris!)
Thanks for all the great reads and years, A Book Apart! You’ve helped man, many people become better web citizens, present company included.
Where You Can Still Get A Book Apart Titles originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Smashing Hour With Dave Rupert
Smashing Magazine invited me to sit down for a one-on-one with “Uncle” Dave Rupert to discuss web components, yes, but also check in on Dave’s new Microsoft gig and what the ShopTalk co-host is working on these days.
I first met Dave in 2015 when CSS Dev Conf took place in my backyard, Long Beach. It’s not like we’ve been in super close touch between then and now — we may have only chatted one-on-one like that a couple other times — but talking with Dave each time feels like hanging with a close friend ands this time was no different. Good, good vibes and web nerdery.
Smashing Hour With Dave Rupert originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Letter Spacing is Broken and There’s Nothing We Can Do About It… Maybe
This post came up following a conversation I had with Emilio Cobos — a senior developer at Mozilla and member of the CSSWG — about the last CSSWG group meeting. I wanted to know what he thought were the most exciting and interesting topics discussed at their last meeting, and with 2024 packed with so many new or coming flashy things like masonry layout, if() conditionals, anchor positioning, view transitions, and whatnot, I thought his answers had to be among them.
He admitted that my list of highlights was accurate on what is mainstream in the community, especially from an author’s point of view. However, and to my surprise, his favorite discussion was on something completely different: an inaccuracy on how the letter-spacing property is rendered across browsers. It’s a flaw so ingrained on the web that browsers have been ignoring the CSS specification for years and that can’t be easily solved by a lack of better options and compatibility issues.
Emilios’s answer makes sense — he works on Gecko and rendering fonts is an art in itself. Still, I didn’t get what the problem is exactly, why he finds it so interesting, and even why it exists in the first place since letter-spacing is a property as old as CSS. It wasn’t until I went into the letter-spacing rabbit hole that I understood how amazingly complex the issue gets and I hope to get you as interested as I did in this (not so) simple property.
What’s letter spacing?The question seems simple: letter spacing is the space between letters. Hooray! That was easy, for humans. For a computer, the question of how to render the space between letters has a lot more nuance. A human just writes the next letter without putting in much thought. Computers, on the other hand, need a strategy on how to render that space: should they add the full space at the beginning of the letter, at the end, or halve it and add it on both sides of the letter? Should it work differently from left-to-right (LTR) languages, like English, to right-to-left (RTL) like Hebrew? These questions are crucial since choosing one as a standard shapes how text measurement and line breaks work across the web.
Which of the three strategies is used on the web? Depends on who you ask. The implementation in the CSS specifications completely differs from what the browsers do, and there is even incompatibility between browsers rendering engines, like Gecko (Firefox), Blink (Chrome, Brave, Opera, etc.), and WebKit (Safari).
What the CSS spec saysLet’s backpedal a bit and first know how the spec says letter spacing should work. At the time of writing, letter-spacing:
Specifies additional spacing between typographic character units. Values may be negative, but there may be implementation-dependent limits.
The formal specification has more juice to it, but this one gives us enough to understand how the CSS spec wants letter-spacing to behave. The keyword is between, meaning that the letter spacing should only affect the space between characters. I know, sounds pretty obvious.
So, as the example given on the spec, the following HTML:
<p>a<span>bb</span>c</p>…with this CSS:
p { letter-spacing: 1em; } span { letter-spacing: 2em; }…should give an equal space between the two “b” letters:
However, if we run the same code on any browser (e.g., Chrome, Firefox, or Safari), we’ll see the spacing isn’t contained between the “b” letters, but also at the end of the complete word.
What browsers doI thought it was normal for letter-spacing to attach spacing at the end of a character and didn’t know the spec said otherwise. However, if you think about it, the current behavior does seem off… it’s just that we’re simply used to it.
Why would browsers not follow the spec on this one?
As we saw before, letter spacing isn’t straightforward for computers since they must stick to a strategy for where spacing is applied. In the case of browsers, the standard has been to apply an individual space at the end of each character, ignoring if that space goes beyond the full word. It may have not been the best choice, but it’s what the web has leaned into, and changing it now would result in all kinds of text and layout shifts across the web.
This leaves a space at the end of elements with bigger letter spacing, which is somewhat acceptable for LTR text, but it leaves a hole at the beginning of the text in an RTL writing mode.
CodePen Embed FallbackThe issue is more obvious with centered text, where the ending space pushes the text away from the element’s dead center. You’ve probably had to add padding on the opposite side of an element to make up for any letter-spacing you’ve applied to the text at least one time, like on a button.
CodePen Embed FallbackAs you can see, the blue highlight creates a symmetrical pyramid which our text sadly doesn’t follow.
What’s worse, the “end of each character” means something different to browsers, particularly when working in an RTL writing mode. Chrome and Safari (Blink/WebKit) say the end of a character is always on the right-hand side. Firefox (Gecko), on the other hand, adds space to the “reading” end — which in Hebrew and Arabic is the left-hand side. See the difference yourself:
Can this be fixed?The first thought that comes to mind is to simply follow what the spec says and trim the unnecessary space at the ending character, but this (anti) solution brings compatibility risks that are simply too big to even consider; text measurement and line breaks would change, possibly causing breakage on lots of websites. Pages that have removed that extra space with workarounds probably did it by offsetting the element’s padding/margin, which means changing the behavior as it currently stands makes those offsets obsolete or breaking.
There are two real options for how letter-spacing can be fixed: reworking how the space is distributed around the character or allowing developers an option to choose where we want the ending space.
Option 1: Reworking the space distributionThe first option would be to change the current letter-spacing definition so it says something like this:
Specifies additional spacing applied to each typographic character unit except those with zero advance. The additional spacing is divided equally between the inline-start and -end sides of the typographic character unit. Values may be negative, but there may be implementation-dependent limits.
Simply put, instead of browsers applying the additional space at the end of the character, they would divide it equally at the start and end, and the result is symmetrical text. This would also change text measurements and line breaks, albeit to a lesser degree.
Now text that is center-aligned text is correctly aligned to the center:
Option 2: Allowing developers an option to chooseEven if the offset is halved, it could still bring breaking layout shifts to pages which to some is still (rightfully) unacceptable. It’s a dilemma: most pages need, or at least would benefit, from leaving letter-spacing as-is, while new pages would enjoy symmetrical letter spacing. Luckily, we could do both by giving developers the option to choose how the space is applied to characters. The syntax is anybody’s guess, but we could have a new property to choose where to place the spacing:
letter-spacing-justify: [ before | after | left | right | between | around];Each value represents where the space should be added, taking into account the text direction:
- before: the spacing is added at the beginning of the letter, following the direction of the language.
- after: the spacing is added at the end of the letter, following the direction of the language.
- left: the spacing is added at the left of the letter, ignoring the direction of the language.
- right: the spacing is added at the right of the letter, ignoring the direction of the language.
- between: the spacing is added between characters, following the spec.
- around: the spacing is divided around the letter.
Logically, the current behavior would be the default to not break anything and letter-spacing would become a shorthand for both properties (length and placing).
letter-spacing: 1px before; letter-spacing: 1px right; letter-spacing: 1px around; letter-spacing: 1px; /* same as: */ letter-spacing: 1px before; What about a third option?And, of course, the third option is to leave things as they are. I’d say this is unlikely since the CSSWG resolved to take action on the issue, and they’ll probably choose the second option if I had to bet the nickel in my pocket on it.
Now you know letter-spacing is broken… and we have to live with it, at least for the time being. But there are options that may help correct the problem down the road.
Letter Spacing is Broken and There’s Nothing We Can Do About It… Maybe originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Pop(over) the Balloons
I’ve always been fascinated with how much we can do with just HTML and CSS. The new interactive features of the Popover API are yet another example of just how far we can get with those two languages alone.
You may have seen other tutorials out there showing off what the Popover API can do, but this is more of a beating-it-mercilessly-into-submission kind of article. We’ll add a little more pop music to the mix, like with balloons… some literal “pop” if you will.
What I’ve done is make a game — using only HTML and CSS, of course — leaning on the Popover API. You’re tasked with popping as many balloons as possible in under a minute. But be careful! Some balloons are (as Gollum would say) “tricksy” and trigger more balloons.
I have cleverly called it Pop(over) the Balloons and we’re going to make it together, step by step. When we’re done it’ll look something like (OK, exactly like) this:
CodePen Embed Fallback Handling the popover attributeAny element can be a popover as long as we fashion it with the popover attribute:
<div popover>...</div>We don’t even have to supply popover with a value. By default, popover‘s initial value is auto and uses what the spec calls “light dismiss.” That means the popover can be closed by clicking anywhere outside of it. And when the popover opens, unless they are nested, any other popovers on the page close. Auto popovers are interdependent like that.
The other option is to set popover to a manual value:
<div popover=“manual”>...</div>…which means that the element is manually opened and closed — we literally have to click a specific button to open and close it. In other words, manual creates an ornery popup that only closes when you hit the correct button and is completely independent of other popovers on the page.
CodePen Embed Fallback Using the <details> element as a starterOne of the challenges of building a game with the Popover API is that you can’t load a page with a popover already open… and there’s no getting around that with JavaScript if our goal is to build the game with only HTML and CSS.
Enter the <details> element. Unlike a popover, the <details> element can be open by default:
<details open> <!-- rest of the game --> </details>If we pursue this route, we’re able to show a bunch of buttons (balloons) and “pop” all of them down to the very last balloon by closing the <details>. In other words, we can plop our starting balloons in an open <details> element so they are displayed on the page on load.
This is the basic structure I’m talking about:
<details open> <summary>🎈</summary> <button>🎈</button> <button>🎈</button> <button>🎈</button> </details>In this way, we can click on the balloon in <summary> to close the <details> and “pop” all of the button balloons, leaving us with one balloon (the <summary> at the end (which we’ll solve how to remove a little later).
You might think that <dialog> would be a more semantic direction for our game, and you’d be right. But there are two downsides with <dialog> that won’t let us use it here:
- The only way to close a <dialog> that’s open on page load is with JavaScript. As far as I know, there isn’t a close <button> we can drop in the game that will close a <dialog> that’s open on load.
- <dialog>s are modal and prevent clicking on other things while they’re open. We need to allow gamers to pop balloons outside of the <dialog> in order to beat the timer.
Thus we will be using a <details open> element as the game’s top-level container and using a plain ol’ <div> for the popups themselves, i.e. <div popover>.
All we need to do for the time being is make sure all of these popovers and buttons are wired together so that clicking a button opens a popover. You’ve probably learned this already from other tutorials, but we need to tell the popover element that there is a button it needs to respond to, and then tell the button that there is a popup it needs to open. For that, we give the popover element a unique ID (as all IDs should be) and then reference it on the <button> with a popovertarget attribute:
<!-- Level 0 is open by default --> <details open> <summary>🎈</summary> <button popovertarget="lvl1">🎈</button> </details> <!-- Level 1 --> <div id="lvl1" popover="manual"> <h2>Level 1 Popup</h2> </div>This is the idea when everything is wired together:
CodePen Embed Fallback Opening and closing popoversThere’s a little more work to do in that last demo. One of the downsides to the game thus far is that clicking the <button> of a popup opens more popups; click that same <button> again and they disappear. This makes the game too easy.
We can separate the opening and closing behavior by setting the popovertargetaction attribute (no, the HTML spec authors were not concerned with brevity) on the <button>. If we set the attribute value to either show or hide, the <button> will only perform that one action for that specific popover.
<!-- Level 0 is open by default --> <details open> <summary>🎈</summary> <!-- Show Level 1 Popup --> <button popovertarget="lvl1" popovertargetaction="show">🎈</button> <!-- Hide Level 1 Popup --> <button popovertarget="lvl1" popovertargetaction="hide">🎈</button> </details> <!-- Level 1 --> <div id="lvl1" popover="manual"> <h2>Level 1 Popup</h2> <!-- Open/Close Level 2 Poppup --> <button popovertarget="lvl2">🎈</button> </div> <!-- etc. -->Note, that I’ve added a new <button> inside the <div> that is set to target another <div> to pop open or close by intentionally not setting the popovertargetaction attribute on it. See how challenging (in a good way) it is to “pop” the elements:
CodePen Embed Fallback Styling balloonsNow we need to style the <summary> and <button> elements the same so that a player cannot tell which is which. Note that I said <summary> and not <details>. That’s because <summary> is the actual element we click to open and close the <details> container.
Most of this is pretty standard CSS work: setting backgrounds, padding, margin, sizing, borders, etc. But there are a couple of important, not necessarily intuitive, things to include.
- First, there’s setting the list-style-type property to none on the <summary> element to get rid of the triangular marker that indicates whether the <details> is open or closed. That marker is really useful and great to have by default, but for a game like this, it would be better to remove that hint for a better challenge.
- Safari doesn’t like that same approach. To remove the <details> marker here, we need to set a special vendor-prefixed pseudo-element, summary::-webkit-details-marker to display: none.
- It’d be good if the mouse cursor indicated that the balloons are clickable, so we can set cursor: pointer on the <summary> elements as well.
- One last detail is setting the user-select property to none on the <summary>s to prevent the balloons — which are simply emoji text — from being selected. This makes them more like objects on the page.
- And yes, it’s 2024 and we still need that prefixed -webkit-user-select property to account for Safari support. Thanks, Apple.
Putting all of that in code on a .balloon class we’ll use for the <button> and <summary> elements:
.balloon { background-color: transparent; border: none; cursor: pointer; display: block; font-size: 4em; height: 1em; list-style-type: none; margin: 0; padding: 0; text-align: center; -webkit-user-select: none; /* Safari fallback */ user-select: none; width: 1em; } CodePen Embed FallbackOne problem with the balloons is that some of them are intentionally doing nothing at all. That’s because the popovers they close are not open. The player might think they didn’t click/tap that particular balloon or that the game is broken, so let’s add a little scaling while the balloon is in its :active state of clicking:
.balloon:active { scale: 0.7; transition: 0.5s; }Bonus: Because the cursor is a hand pointing its index finger, clicking a balloon sort of looks like the hand is poking the balloon with the finger. 👉🎈💥
The way we distribute the balloons around the screen is another important thing to consider. We’re unable to position them randomly without JavaScript so that’s out. I tried a bunch of things, like making up my own “random” numbers defined as custom properties that can be used as multipliers, but I couldn’t get the overall result to feel all that “random” without overlapping balloons or establishing some sort of visual pattern.
I ultimately landed on a method that uses a class to position the balloons in different rows and columns — not like CSS Grid or Multicolumns, but imaginary rows and columns based on physical insets. It’ll look a bit Grid-like and is less “randomness” than I want, but as long as none of the balloons have the same two classes, they won’t overlap each other.
I decided on an 8×8 grid but left the first “row” and “column” empty so the balloons are clear of the browser’s left and top edges.
/* Rows */ .r1 { --row: 1; } .r2 { --row: 2; } /* all the way up to .r7 */ /* Columns */ .c1 { --col: 1; } .c2 { --col: 2; } /* all the way up to .c7 */ .balloon { /* This is how they're placed using the rows and columns */ top: calc(12.5vh * (var(--row) + 1) - 12.5vh); left: calc(12.5vw * (var(--col) + 1) - 12.5vw); } CodePen Embed Fallback Congratulating The Player (Or Not)We have most of the game pieces in place, but it’d be great to have some sort of victory dance popover to congratulate players when they successfully pop all of the balloons in time.
Everything goes back to a <details open> element. Once that element is not open, the game should be over with the last step being to pop that final balloon. So, if we give that element an ID of, say, #root, we could create a condition to hide it with display: none when it is :not() in an open state:
#root:not([open]) { display: none; }This is where it’s great that we have the :has() pseudo-selector because we can use it to select the #root element’s parent element so that when #root is closed we can select a child of that parent — a new element with an ID of #congrats — to display a faux popover displaying the congratulatory message to the player. (Yes, I’m aware of the irony.)
#game:has(#root:not([open])) #congrats { display: flex; }If we were to play the game at this point, we could receive the victory message without popping all the balloons. Again, manual popovers won’t close unless the correct button is clicked — even if we close its ancestral <details> element.
Is there a way within CSS to know that a popover is still open? Yes, enter the :popover-open pseudo-class.
The :popover-open pseudo-class selects an open popover. We can use it in combination with :has() from earlier to prevent the message from showing up if a popover is still open on the page. Here’s what it looks like to chain these things together to work like an and conditional statement.
/* If #game does *not* have an open #root * but has an element with an open popover * (i.e. the game isn't over), * then select the #congrats element... */ #game:has(#root:not([open])):has(:popover-open) #congrats { /* ...and hide it */ display: none; }Now, the player is only congratulated when they actually, you know, win.
Conversely, if a player is unable to pop all of the balloons before a timer expires, we ought to inform the player that the game is over. Since we don’t have an if() conditional statement in CSS (not yet, at least) we’ll run an animation for one minute so that this message fades in to end the game.
#fail { animation: fadein 0.5s forwards 60s; display: flex; opacity: 0; z-index: -1; } @keyframes fadein { 0% { opacity: 0; z-index: -1; } 100% { opacity: 1; z-index: 10; } }But we don’t want the fail message to trigger if the victory screen is showing, so we can write a selector that prevents the #fail message from displaying at the same time as #congrats message.
#game:has(#root:not([open])) #fail { display: none; } We need a game timerA player should know how much time they have to pop all of the balloons. We can create a rather “simple” timer with an element that takes up the screen’s full width (100vw), scaling it in the horizontal direction, then matching it up with the animation above that allows the #fail message to fade in.
#timer { width: 100vw; height: 1em; } #bar { animation: 60s timebar forwards; background-color: #e60b0b; width: 100vw; height: 1em; transform-origin: right; } @keyframes timebar { 0% { scale: 1 1; } 100% { scale: 0 1; } }Having just one point of failure can make the game a little too easy, so let’s try adding a second <details> element with a second “root” ID, #root2. Once more, we can use :has to check that neither the #root nor #root2 elements are open before displaying the #congrats message.
#game:has(#root:not([open])):has(#root2:not([open])) #congrats { display: flex; } Wrapping upThe only thing left to do is play the game!
CodePen Embed FallbackFun, right? I’m sure we could have built something more robust without the self-imposed limitation of a JavaScript-free approach, and it’s not like we gave this a good-faith accessibility pass, but pushing an API to the limit is both fun and educational, right?
I’m interested: What other wacky ideas can you think up for using popovers? Maybe you have another game in mind, some slick UI effect, or some clever way of combining popovers with other emerging CSS features, like anchor positioning. Whatever it is, please share!
Pop(over) the Balloons originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Love Notes
Read the book, Typographic Firsts
Introducing love notes for some of our favorite fonts...
The post Love Notes appeared first on I Love Typography.