Front End Web Development
Quick Hit #11
Hey look at that, the State of CSS Survey for 2024 is open and taking submissions.
Quick Hit #11 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
The Intersection of Speed and Proximity
You ever find yourself in bumper-to-bumper traffic? I did this morning on the way to work (read: whatever cafe I fancy). There’s a pattern to it, right? Stop, go, stop, go, stop… it’s almost rhythmic and harmonious in the most annoying of ways. Everyone in line follows the dance, led by some car upfront, each subsequent vehicle pressed right up to the rear of the next for the luxury of moving a few feet further before the next step.
Photo by Jakob JinHave you tried breaking the pattern? Instead of playing shadow to the car in front of me this morning, I allowed space between us. I’d gradually raise my right foot off the brake pedal and depress the gas pedal only once my neighboring car gained a little momentum. At that point, my car begins to crawl. And continue crawling. I rarely had to tap the brakes at all once I got going. In effect, I had sacrificed proximity for a smoother ride. I may not be traveling the “fastest” in line, but I was certainly gliding along with a lot less friction.
I find that many things in life are like that. Getting closest to anything comes with a cost, be it financial or consequence. Want the VIP ticket to a concert you’re stoked as heck about? Pony up some extra cash. Want the full story rather than a headline? Just enter your email address. Want up-to-the-second information in your stock ticker? Hand over some account information. Want access to all of today’s televised baseball games? Pick up an ESPN+ subscription.
Proximity and speed are the commodities, the products so to speak. Closer and faster are what’s being sold.
You may have run into the “law of diminishing returns” in some intro-level economics class you took in high school or college. It’s the basis for a large swath of economic theory but in essence, is the “too much of a good thing” principle. It’s what AMPM commercials have been preaching this whole time.
I’m embedding the clip instead of linking it up because it clearly illustrates the “problem” of having too many of what you want (or need). Dude resorted to asking two teens to reach into his front pocket for his wallet because his hands were full, creeper. But buy on, the commercial says, because the implication is that there’s never too much of a good thing, even if it ends in a not-so-great situation chockfull of friction.
The only and only thing I took away from physics in college — besides gravity force being 9.8 m/s2 — is that there’s no way to have bigger, cheaper, and faster at the same time. You can take two, but all three cannot play together. For example, you can have a spaceship that’s faster and cheaper, but chances are that it ain’t gonna be bigger than a typical spaceship. If you were to aim for bigger, it’d be a lot less cheap, not only for the extra size but also to make the dang heavy thing go as fast as possible. It’s a good rule in life. I don’t have proof of it, but I’d wager Mick Jagger lives by it, or at least did at one time.
Speed. Proximity. Faster and slower. Closer and further. I’m not going to draw any parallels to web development, UX design, or any other front-end thing. They’re already there.
The Intersection of Speed and Proximity originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Elastic Overflow Scrolling
A client asked if we could mimic the “rubber band” scrolling behavior on many mobile devices. I’m sure you know what I’m talking about. It’s a behavior that already exists and happens automatically in most browsers. In iOS Safari, for example, you’re allowed to scroll beyond the top or bottom edge of the viewport by a few hundred pixels, and letting go snaps the page back in place.
I had heard of some instances where someone might want to prevent the bounce from happening but no one had asked me to implement it, especially in a way that supports devices without a touch interface. I was actually a bit surprised there isn’t an existing CSS property for this. There’s the non-standard -webkit-overflow-scrolling property but that’s for a different type of “momentum” scrolling. Nor would I want to rely on a non-standard property that’s not on track to become part of the specifications.
OK, so what if we want to force this sort of rubber banding in our work? For starters, we’d need some sort of element acting as a container for content that requires scrolling. From there, we could reach for JavaScript, of course, but that involves adding scroll listeners or a combination of pointerDown, pointerUp, and pointerMove events, not to mention keeping track of positions, inertial movement, etc.
A CSS-only solution would be much more ideal.
Here is a container with a few child elements:
<div class="carousel"> <div class="slides"> <div class="slide">1</div> <div class="slide">2</div> <div class="slide">3</div> <div class="slide">4</div> <div class="slide">5</div> </div> </div>Let’s get some baseline styles in place, specifically to create a situation where we’re guaranteed to overflow a parent container.
/* Parent container with fixed dimensions for overflow */ .carousel { width: 200px; height: 400px; overflow-x: hidden; overflow-y: auto; } /* Wrapper for slides, stacked in a column */ .slides { display: flex; flex-direction: column; flex-wrap: wrap; width: 100%; height: fit-content; } /* Each slide is the full width of the carousel */ .slide { width: 100%; aspect-ratio: 1; }Let’s start by adding some vertical margins. If your container has only one long item, add it to the top and bottom of the child element. If the container has multiple children, you’ll want to add margin to the top of the first child element and the bottom of the last child element.
.carousel > .slides > .slide:first-child { margin-top: 100px; } .carousel > .slides > .slide:last-child { margin-bottom: 100px; }Great! We can now scroll past the edges, but we need something to snap it back after the user lifts their finger or pointer. For this, we’ll need the scroll-snap-type and scroll-snap-align properties
.carousel { scroll-snap-type: y mandatory; } .carousel > .slides > .slide { scroll-snap-align: start; } .carousel > .slides > .slide:first-child { margin-top: 100px; } .carousel > .slides > .slide:last-child { scroll-snap-align: end; margin-bottom: 100px; }Note that the same applies to a horizontally scrolling element. For that, you’d change things up so that margin is applied to the element’s left and right edges instead of its top and bottom edges. You’ll also want to change the scroll-snap-type property’s value from y mandatory to x mandatory while you’re at it.
That’s really it! Here’s the final demo:
CodePen Embed FallbackI know, I know. This isn’t some Earth-shattering or mind-blowing effect, but it does solve a very specific situation. And if you find yourself in that situation, now you have something in your back pocket to use.
Additional resources
- “The inside story of the iconic ‘rubber band’ effect that launched the iPhone” (Cult of Mac)
- “Six things I learnt about iOS Safari’s rubber band scrolling” (Special Agent Squeaky)
- “Scroll Bouncing On Your Websites” (Smashing Magazine)
Elastic Overflow Scrolling originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
RTL Styling 101
A couple of weeks ago I was super excited about publishing my first CSS-Tricks post: “Letter Spacing is Broken. Forget about that though, what’s important is the post’s topic: letter spacing is broken and doesn’t work as the CSS Specification says it should. In a nutshell, instead of spacing the characters evenly, it leaves an unpleasant space at the end of the element.
While this inconsistency between the web and the spec is just a quirk for a Spanish/English speaker like me, for speakers of right-to-left (RTL) languages like Arabic or Hebrew, an annoying space is left at the start or end of a word. Firefox (Gecko) kinda fixes it and rearranges the unnecessary space at the end (in the reading order), but Google and Safari (Blink and Webkit) leave it at the start.
Of course, I wanted to demo this major pain point, but styling RTL content was beyond my CSS power. That’s when I found this life-saver guide by Ahmad Shadeed that covers every major aspect of styling RTL content on the web and best practices to easily internationalize an LTR webpage. A resource that, I think, is a must-read if you are interested in i18n and accessibility in the web.
I may have discovered warm water since this guide goes back to 2018, but I hope those like me who didn’t know about it have fun learning something new!
RTL Styling 101 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
On the Ground at Frostapalooza
I can’t say I would have ever expected to see Jeremy Keith performing the Yeah Yeah Yeahs song “Maps”, but then again, I don’t know what I expected to happen at Frostapalooza.
The EventBrad Frost, web designer, author of Atomic Design, and an absolute maniac on the bass, celebrated his birthday by putting together a one-night-only benefit concert featuring musical performances by himself and his talented family and friends.
Frostapalooza, held at Mr. Smalls Theatre in Pittsburgh, PA, was an all-ages event where 100% of the proceeds are headed towards two great causes:
- NextStep Pittsburgh: Helping provide accessible rehabilitation for folks with spinal cord injuries and paralysis in Pittsburgh.
- Project Healthy Minds: Providing research and resources to help tackle mental health.
The variation of musical performances sprawled across the night, covering tracks by Fleetwood Mac, Radiohead, David Bowie and so much more, check out this setlist of all 31 tracks on Spotify.
I loved the performance of Pink Floyd’s classic song, “Money.” As a Floyd fan who will never get to see them live, this was easily the best rendition I could ask for, which included the full lineup of instrumental sections.
Brad was joined on stage by none other than CSS-Tricks founder, Chris Coyier. Chris picked banjo on a few songs, such as Johnny Cash’s “Folsom Prison Blues” and The Band’s “The Weight,” both fantastic.
The stage background prominently displayed visuals out of CodePen demos made by CodePen community members during the set. Check out the Frostapalooza tag on CodePen to see everything that was projected.
Another favorite moment was Brad’s version of “Wake Up” by Arcade Fire, which felt like a perfectly matched song for the evening.
MusiciansIf you haven’t caught on yet, many of the folks lending their musical talents to Frostapalooza also happen to be web designers and developers Brad has met and worked with during his career. At times it felt like the Wu-Tang Clan of CSS on stage.
Brad’s family and musicians from his other bands pitched in, such as Elby Brass. Ridiculously impressive! I had never seen a tuba-playing lead vocalist until this night.
You can see the full lineup on the event’s website. But I’ll drop a screenshot in here just for posterity.
Photos! Videos!Mike Aparicio captured a great video of a group jam on Queen’s “Bohemian Rhapsody” that you’ve got to watch on YouTube. Brian Kardell nabbed this gem of Chris pickin’ on “The Weight”:
Party boy Brad Frost shared a bunch of other photos from the event in a Google Photos album.
The endPlain and simple, this was a super fun night celebrating music and friends. Happy birthday, Brad, and thanks for putting on an awesome show!
Update (8/21/2024): You can check out Jeremy Keith’s post-event wrap, as well as Brad’s commentary on it.
On the Ground at Frostapalooza originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
All About JavaScript Loops
Every programming language has loops. Loops perform an operation (i.e., a chunk of work) a number of times, usually once for every item in an array or list, or to simply repeat an operation until a certain condition is met.
JavaScript in particular has quite a few different types of loops. I haven’t even used all of them, so for my own curiosity, I thought I’d do a high-level overview of them. And as it turns out, there are pretty good reasons I haven’t used at least a couple of the different types.
So, for now let’s spend a while exploring the different types of loops, what we can do with each of one, and why you might use one over another. (You’ll think that little play on words is absolutely hilarious by the end.)
The while and do...while loopsFirst up is the while loop. It’s the most basic type of loop and has the potential to be the easiest to read and the fastest in many cases. It’s usually used for doing something until a certain condition is met. It’s also the easiest way to make an infinite loop or a loop that never stops. There is also the do...while statement. Really, the only difference is that the condition is checked at the end versus the beginning of each iteration.
// remove the first item from an array and log it until the array is empty let queue1 = ["a", "b", "c"]; while (queue1.length) { let item = queue1.shift(); console.log(item); } // same as above but also log when the array is empty let queue2 = []; do { let item = queue2.shift() ?? "empty"; console.log(item); } while (queue2.length); The for loopNext is the for loop. It should be the go to way to do something a certain number of times. If you need to repeat an operation, say, 10 times, then use a for loop instead. This particular loop may be intimidating to those new to programming, but rewriting the same loop in the while-style loop can help illustrate the syntax make it easier to stick in your mind.
// log the numbers 1 to 5 for (let i = 1; i <= 5; i++) { console.log(i); } // same thing but as a while loop let i = 1; // the first part of a for loop // the second while (i <= 5) { console.log(i); i++; // the third } ("end"); The for...of and for await...of loopsA for...of loop is the easiest way to loop through an array.
let myList = ["a", "b", "c"]; for (let item of myList) { console.log(item); }They aren’t limited to arrays though. Technically they can iterate through anything that implements what is called an iterable protocol. There are a few built-in types that implement the protocol: arrays, maps, set, and string, to mention the most common ones, but you can implement the protocol in your own code. What you’d do is add a [Symbol.iterator] method to any object and that method should return an iterator. It’s a bit confusing, but the gist is that iterables are things with a special method that returns iterators; a factory method for iterators if you will. A special type of function called a generator is a function that returns both a iterable and iterator.
let myList = { *[Symbol.iterator]() { yield "a"; yield "b"; yield "c"; }, }; for (let item of myList) { console.log(item); }There is the async version of all the things I just mentioned: async iterables, async iterators, and async generators. You’d use an async iterable with for await...of.
async function delay(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } // this time we're not making an iterable, but a generator async function* aNumberAMinute() { let i = 0; while (true) { // an infinite loop yield i++; // pause a minute await delay(60_000); } } // it's a generator, so we need to call it ourselves for await (let i of aNumberAMinute()) { console.log(i); // stop after one hour if (i >= 59) { break; } }One unobvious thing about for await...of statement is that you can use it with non-async iterables and it will work just fine. The reverse, however, is not true; you can’t use async iterables with the for...of statement.
The forEach and map loopsWhile these are not technically loops per se, you can use them to iterate over a list.
Here is the thing about the forEach method. Historically it was much slower than using a for loop. I think in some cases that may not be true anymore, but if performance is a concern, then I would avoid using it. And now that we have for...of I’m not sure there is much reason to use it. I guess the only reason that it still may come up is if you have a function ready to use as the callback, but you could easily just call that same function from inside the body of for...of.
forEach also receives the index for each item though, so that may be a thing you need too. Ultimately, the decision to use it will probably come down to whether any other code you’re working with uses it, but I personally would avoid using it if I’m writing something new.
let myList = ["a", "b", "c"]; for (let item of myList) { console.log(item); } // but maybe if I need the index use forEach ["a", "b", "c"].forEach((item, index) => { console.log(`${index}: ${item}`); });Meanwhile, map essentially converts one array into another. It still has the same performance impact that forEach has, but it is a bit nicer to read than the alternative. It’s certainly subjective though, and just like with forEach you’ll want to do what the rest of your other code is doing. You see it a ton in React and React-inspired libraries as the primary way to loop through an array and output a list of items within JSX.
function MyList({items}) { return ( <ul> {items.map((item) => { return <li>{item}</li>; })} </ul> ); } The for...in loopThis list of loops in JavaScript wouldn’t be complete without mentioning the for...in statement because it can loop through the fields of an object. It visits fields that are inherited through the object’s prototype chain too, though, and I’ve honestly always avoided it for that reason.
That said, if you have an object literal, then for...in might be a viable way to iterate through the keys of that object. Also it’s worth noting that if you’ve been programming JavaScript for a long time, you may remember that the order of keys use to be inconsistent between browsers, but now the order is consistent. Any key that could be an array index (i.e., positive integers) will be first in ascending order, and then everything else in the order as authored.
let myObject = { a: 1, b: 2, c: 3, }; for (let k in myObject) { console.log(myObject[k]); } Wrapping upLoops are something that many programmers use every day, though we may take them for granted and not think about them too much.
But when you step back and look at all of the ways we have to loop through things in JavaScript, it turns out there are several ways to do it. Not only that, but there are significant — if not nuanced — differences between them that can and will influence your approach to scripts.
All About JavaScript Loops originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
CSSWG Minutes Telecon (2024-08-14)
I was just going over the latest CSSWG minutes (you can subscribe to them at W3C.org) and came across a few interesting nuggets I wanted to jot down for another time. The group discussed the CSS Values, CSS Easing, and Selectors modules, but what really caught my eye was adding triggered delays to CSS for things like hover, long taps, and focus states.
The idea stems from an OpenUI proposal, the same group we can thank for raising things like the Popover API and customizable select element. The concept, if I understand it right, is that anytime someone hovers, taps, or focuses on, say, a <button> for a certain amount of time, we can invoke some sort of thing. A tooltip is the perfect illustration. Hovering over the trigger element, the reasoning goes, is an expression of interest and as web authors, we can do something with that interest, like displaying a tooltip.
Whoa, right?! There’s long been chatter about CSS encroaching on JavaScript territory (isn’t it ironic, don’t you think?). Firing events in response to interaction is quite literally the only thing I use JavaScript for. There’s no mistake about that in the CSSWG, as documented in the minutes:
So. Does this belong in CSS? Or should it be elsewhere? Does the approach make sense? Are there better ideas? Most interested in the last.
[…]
Other question; does this belong in CSS or HTML… maybe this is just a javascript feature? In JS you can determine MQ state and change things so it wouldn’t necessarily be in CSS.
And shortly later:
As you were talking; one thing that I kept thinking of; should developers be customizing the delay at all? Original use case for delay is that hover shouldn’t be instant. But if we don’t allow for customizing we can align to platform delay lengths.
But there’s an excellent point to be made about the way many of us are already doing this with CSS animations (animation-delay) and transitions (transition-delay). Sometimes even applying those globally with the Universal Selector or a prefers-* query.
Things get even hairier when considering how values are defined for this. Are they explicit delays (800ms), generic keywords (none/short/medium/long), a custom property, a pseudo-class… something else? I’m glad there’re incredibly smart folks noodling on this stuff.
I think here it would be good to go with time values. CSS is a good place to put it. We have all the ergonomics. The right declarative place to put it.
Whatever the eventual case may be:
I think this sounds reasonable and I’d like to explore it. Unsure if this is the exact shape, but this space seems useful to me.
CSSWG Minutes Telecon (2024-08-14) originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
How are the `colspan` and `rowspan` attributes different?
Yes, yes. Functionally, they are different. But heck if I didn’t know about the wacky thresholds until Jens Oliver Meiert tooted a pair of quick polls.
According to the HTML Standard:
- If the current cell has a colspan attribute, then parse that attribute’s value, and let colspan be the result.
If parsing that value failed, or returned zero, or if the attribute is absent, then let colspan be 1, instead.
If colspan is greater than 1000, let it be 1000 instead. - If the current cell has a rowspan attribute, then parse that attribute’s value, and let rowspan be the result.
If parsing that value failed or if the attribute is absent, then let rowspan be 1, instead.
If rowspan is greater than 65534, let it be 65534 instead.
I saw the answers in advance and know I’d have flubbed rowspan. Apparently, 1000 table columns are plenty of columns to span at once, while 65534 is the magic number for clamping how many rows we can span at a time. Why is the sweet spot for rowspan 6,4543 spans greater than colspan? There are usually good reasons for these things.
What that reason is, darned if I know, but now I have a little nugget for cocktail chatter in my back pocket.
How are the `colspan` and `rowspan` attributes different? originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
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.