Front End Web Development
The What If Machine: Bringing the “Iffy” Future of CSS into the Present
Geoff’s post about the CSS Working Group’s decision to work on inline conditionals inspired some drama in the comments section. Some developers are excited, but it angers others, who fear it will make the future of CSS, well, if-fy. Is this a slippery slope into a hellscape overrun with rogue developers who abuse CSS by implementing excessive logic in what was meant to be a styling language? Nah. Even if some jerk did that, no mainstream blog would ever publish the ramblings of that hypothetical nutcase who goes around putting crazy logic into CSS for the sake of it. Therefore, we know the future of CSS is safe.
You say the whole world’s ending — honey, it already didMy thesis for today’s article offers further reassurance that inline conditionals are probably not the harbinger of the end of civilization: I reckon we can achieve the same functionality right now with style queries, which are gaining pretty good browser support.
If I’m right, Lea’s proposal is more like syntactic sugar which would sometimes be convenient and allow cleaner markup. It’s amusing that any panic-mongering about inline conditionals ruining CSS might be equivalent to catastrophizing adding a ternary operator for a language that already supports if statements.
Indeed, Lea says of her proposed syntax, “Just like ternaries in JS, it may also be more ergonomic for cases where only a small part of the value varies.” She also mentions that CSS has always been conditional. Not that conditionality was ever verboten in CSS, but CSS isn’t always very good at it.
Sold! I want a conditional oompa loompa now!Me too. And many other people, as proven by Lea’s curated list of amazingly complex hacks that people have discovered for simulating inline conditionals with current CSS. Some of these hacks are complicated enough that I’m still unsure if I understand them, but they certainly have cool names. Lea concludes: “If you’re aware of any other techniques, let me know so I can add them.”
Hmm… surely I was missing something regarding the problems these hacks solve. I noted that Lea has a doctorate whereas I’m an idiot. So I scrolled back up and reread, but I couldn’t stop thinking: Are these people doing all this work to avoid putting an extra div around their widgets and using style queries?
It’s fair if people want to avoid superfluous elements in the DOM, but Lea’s list of hacks shows that the alternatives are super complex, so it’s worth a shot to see how far style queries with wrapper divs can take us.
Motivating examplesLea’s motivating examples revolve around setting a “variant” property on a callout, noting we can almost achieve what she wants with style queries, but this hypothetical syntax is sadly invalid:
.callout { @container (style(--variant: success)) { border-color: var(--color-success-30); background-color: var(--color-success-95); &::before { content: var(--icon-success); color: var(--color-success-05); } } }She wants to set styles on both the container itself and its descendants based on --variant. Now, in this specific example, I could get away with hacking the ::after pseudo-element with z-index to give the illusion that it’s the container. Then I could style the borders and background of that. Unfortunately, this solution is as fragile as my ego, and in this other motivating example, Lea wants to set flex-flow of the container based on the variant. In that situation, my pseudo-element solution is not good enough.
Remember, the acceptance of Lea’s proposal into the CSS spec came as her birthday gift from the universe, so it’s not fair to try to replace her gift with one of those cheap fake containers I bought on Temu. She deserves an authentic container.
Let’s try again.
Busting out the gangsta wrapperOne of the comments on Lea’s proposal mentions type grinding but calls it “a very (I repeat, very) convoluted but working” approach to solving the problem that inline conditionals are intended to solve. That’s not quite fair. Type grinding took me a bit to get my head around, but I think it is more approachable with fewer drawbacks than other hacks. Still, when you look at the samples, this kind of code in production would get annoying. Therefore, let’s bite the bullet and try to build an alternate version of Lea’s flexbox variant sample. My version doesn’t use type grinding or any hack, but “plain old” (not so old) style queries together with wrapper divs, to work around the problem that we can’t use style queries to style the container itself.
CodePen Embed Fallback The wrapper battles type grindingComparing the code from Lea’s sample and my version can help us understand the differences in complexity.
Here are the two versions of the CSS:
And here are the two versions of the markup:
So, simpler CSS and slightly more markup. Maybe we are onto something.
What I like about style queries is that Lea’s proposal uses the style() function, so if and when her proposal makes it into browsers then migrating style queries to inline conditionals and removing the wrappers seems doable. This wouldn’t be a 2025 article if I didn’t mention that migrating this kind of code could be a viable use case for AI. And by the time we get inline conditionals, maybe AI won’t suck.
But we’re getting ahead of ourselves. Have you ever tried to adopt some whizz-bang JavaScript framework that looks elegant in the “to-do list” sample? If so, you will know that solutions that appear compelling in simplistic examples can challenge your will to live in a realistic example. So, let’s see how using style queries in the above manner works out in a more realistic example.
Seeking validationCombine my above sample with this MDN example of HTML5 Validation and Seth Jeffery’s cool demo of morphing pure CSS icons, then feed it all into the “What If” Machine to get the demo below.
CodePen Embed FallbackAll the changes you see to the callout if you make the form valid are based on one custom property. This property is never directly used in CSS property values for the callout but controls the style queries that set the callout’s border color, icon, background color, and content. We set the --variant property at the .callout-wrapper level. I am setting it using CSS, like this:
@property --variant { syntax: "error | success"; initial-value: error; inherits: true; } body:has(:invalid) .callout-wrapper { --variant: error; } body:not(:has(:invalid)) .callout-wrapper { --variant: success; }However, the variable could be set by JavaScript or an inline style in the HTML, like Lea’s samples. Form validation is just my way of making the demo more interactive to show that the callout can change dynamically based on --variant.
Wrapping upIt’s off-brand for me to write an article advocating against hacks that bend CSS to our will, and I’m all for “tricking” the language into doing what we want. But using wrappers with style queries might be the simplest thing that works till we get support for inline conditionals. If we want to feel more like we are living in the future, we could use the above approach as a basis for a polyfill for inline conditionals, or some preprocessor magic using something like a Parcel plugin or a PostCSS plugin — but my trigger finger will always itch for the Delete key on such compromises. Lea acknowledges, “If you can do something with style queries, by all means, use style queries — they are almost certainly a better solution.”
I have convinced myself with the experiments in this article that style queries remain a cromulent option even in Lea’s motivating examples — but I still look forward to inline conditionals. In the meantime, at least style queries are easy to understand compared to the other known workarounds. Ironically, I agree with the comments questioning the need for the inline conditionals feature, not because it will ruin CSS but because I believe we can already achieve Lea’s examples with current modern CSS and without hacks. So, we may not need inline conditionals, but they could allow us to write more readable, succinct code. Let me know in the comment section if you can think of examples where we would hit a brick wall of complexity using style queries instead of inline conditionals.
The What If Machine: Bringing the “Iffy” Future of CSS into the Present originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Steven Heller’s Font of the Month: Steam
Read the book, Typographic Firsts
This month, Steven Heller takes a closer look at the layer font family, Steam by Type Forward.
The post Steven Heller’s Font of the Month: Steam appeared first on I Love Typography.
Handwriting an SVG Heart, With Our Hearts
According to local grocery stores, it’s the Valentine’s Day season again, and what better way to express our love than with the symbol of love: a heart. A while back on CSS-Tricks, we shared several ways to draw hearts, and the response was dreamy. Check out all these amazing, heart-filled submissions in this collection on CodePen:
Temani Afif’s CSS Shapes site offers a super modern heart using only CSS:
CodePen Embed FallbackNow, to show my love, I wanted to do something personal, something crafty, something with a mild amount of effort.
L is for Love LinesHandwriting a love note is a classic romantic gesture, but have you considered handwriting an SVG? We won’t need some fancy vector drawing tool to express our love. Instead, we can open a blank HTML document and add an <svg> tag:
<svg> </svg>We’ll need a way to see what we are doing inside the “SVG realm” (as I like to call it), which is what the viewBox attribute provides. The 2D plane upon which vector graphics render is as infinite as our love, quite literally, complete with an x- and y-axis and all (like from math class).
We’ll set the start coordinates as 0 0 and end coordinates as 10 10 to make a handsome, square viewBox. Oh, and by the way, we don’t concern ourselves over pixels, rem values, or any other unit types; this is vector graphics, and we play by our own rules.
We add in these coordinates to the viewBox as a string of values:
<svg viewBox="0 0 10 10"> </svg>Now we can begin drawing our heart, with our heart. Let’s make a line. To do that, we’ll need to know a lot more about coordinates, and where to stick ’em. We’re able to draw a line with many points using the <path> element, which defines paths using the d attribute. SVG path commands are difficult to memorize, but the effort means you care. The path commands are:
- MoveTo: M, m
- LineTo: L, l, H, h, V, v
- Cubic Bézier curve: C, c, S, s
- Quadratic Bézier Curve: Q, q, T, t
- Elliptical arc curve: A, a
- ClosePath: Z, z
We’re only interested in drawing line segments for now, so together we’ll explore the first two: MoveTo and LineTo. MDN romantically describes MoveTo as picking up a drawing instrument, such as a pen or pencil: we aren’t yet drawing anything, just moving our pen to the point where we want to begin our confession of love.
We’ll MoveTo (M) the coordinates of (2,2) represented in the d attribute as M2,2:
<svg viewBox="0 0 10 10"> <path d="M2,2" /> </svg>Not surprising then to find that LineTo is akin to putting pen to paper and drawing from one point to another. Let’s draw the first segment of our heart by drawing a LineTo (L) with coordinates (4,4), represented as L2,2 next in the d attribute:
<svg viewBox="0 0 10 10"> <path d="M2,2 L4,4" /> </svg>We’ll add a final line segment as another LineTo L with coordinates (6,2), again appended to the d attribute as L6,2:
<svg viewBox="0 0 10 10"> <path d="M2,2 L4,4 L6,2" /> </svg>If you stop to preview what we’ve accomplished so far, you may be confused as it renders an upside-down triangle; that’s not quite a heart yet, Let’s fix that.
SVG shapes apply a fill by default, which we can remove with fill="none":
<svg viewBox="0 0 10 10"> <path d="M2,2 L4,4 L6,2" fill="none" /> </svg>Rather than filling in the shape, instead, let’s display our line path by adding a stroke, adding color to our heart.
<svg viewBox="0 0 10 10"> <path d="M2,2 L4,4 L6,2" fill="none" stroke="rebeccapurple" /> </svg>Next, add some weight to the stroke by increasing the stroke-width:
<svg viewBox="0 0 10 10"> <path d="M2,2 L4,4 L6,2" fill="none" stroke="rebeccapurple" stroke-width="4" /> </svg>Finally, apply a stroke-linecap of round (sorry, no time for butt jokes) to round off the start and end points of our line path, giving us that classic symbol of love:
<svg viewBox="0 0 10 10"> <path d="M2,2 L4,4 L6,2" fill="none" stroke="rebeccapurple" stroke-width="4" stroke-linecap="round" /> </svg> CodePen Embed FallbackPerfection. Now all that’s left to do is send it to that special someone.
💜
Handwriting an SVG Heart, With Our Hearts originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Scroll Driven Animations Notebook
Adam’s such a mad scientist with CSS. He’s been putting together a series of “notebooks” that make it easy for him to demo code. He’s got one for gradient text, one for a comparison slider, another for accordions, and the list goes on.
One of his latest is a notebook of scroll-driven animations. They’re all impressive as heck, as you’d expect from Adam. But it’s the simplicity of the first few examples that I love most. Here I am recreating two of the effects in a CodePen, which you’ll want to view in the latest version of Chrome for support.
CodePen Embed FallbackThis is a perfect example of how a scroll-driven animation is simply a normal CSS animation, just tied to scrolling instead of the document’s default timeline, which starts on render. We’re talking about the same set of keyframes:
@keyframes slide-in-from-left { from { transform: translateX(-100%); } }All we have to do to trigger scrolling is call the animation and assign it to the timeline:
li { animation: var(--animation) linear both; animation-timeline: view(); }Notice how there’s no duration set on the animation. There’s no need to since we’re dealing with a scroll-based timeline instead of the document’s timeline. We’re using the view() function instead of the scroll() function, which acts sort of like JavsScript’s Intersection Observer where scrolling is based on where the element comes into view and intersects the scrollable area.
It’s easy to drop your jaw and ooo and ahh all over Adam’s demos, especially as they get more advanced. But just remember that we’re still working with plain ol’ CSS animations. The difference is the timeline they’re on.
Scroll Driven Animations Notebook originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Typecasting and Viewport Transitions in CSS With tan(atan2())
We’ve been able to get the length of the viewport in CSS since… checks notes… 2013! Surprisingly, that was more than a decade ago. Getting the viewport width is as easy these days as easy as writing 100vw, but what does that translate to, say, in pixels? What about the other properties, like those that take a percentage, an angle, or an integer?
Think about changing an element’s opacity, rotating it, or setting an animation progress based on the screen size. We would first need the viewport as an integer — which isn’t currently possible in CSS, right?
What I am about to say isn’t a groundbreaking discovery, it was first described amazingly by Jane Ori in 2023. In short, we can use a weird hack (or feature) involving the tan() and atan2() trigonometric functions to typecast a length (such as the viewport) to an integer. This opens many new layout possibilities, but my first experience was while writing an Almanac entry in which I just wanted to make an image’s opacity responsive.
Resize the CodePen and the image will get more transparent as the screen size gets smaller, of course with some boundaries, so it doesn’t become invisible:
CodePen Embed FallbackThis is the simplest we can do, but there is a lot more. Take, for example, this demo I did trying to combine many viewport-related effects. Resize the demo and the page feels alive: objects move, the background changes and the text smoothly wraps in place.
CodePen Embed FallbackI think it’s really cool, but I am no designer, so that’s the best my brain could come up with. Still, it may be too much for an introduction to this typecasting hack, so as a middle-ground, I’ll focus only on the title transition to showcase how all of it works:
CodePen Embed Fallback Setting things upThe idea behind this is to convert 100vw to radians (a way to write angles) using atan2(), and then back to its original value using tan(), with the perk of coming out as an integer. It should be achieved like this:
:root { --int-width: tan(atan2(100vw, 1px)); }But! Browsers aren’t too keep on this method, so a lot more wrapping is needed to make it work across all browsers. The following may seem like magic (or nonsense), so I recommend reading Jane’s post to better understand it, but this way it will work in all browsers:
@property --100vw { syntax: "<length>"; initial-value: 0px; inherits: false; } :root { --100vw: 100vw; --int-width: calc(10000 * tan(atan2(var(--100vw), 10000px))); }Don’t worry too much about it. What’s important is our precious --int-width variable, which holds the viewport size as an integer!
CodePen Embed Fallback Wideness: One number to rule them allRight now we have the viewport as an integer, but that’s just the first step. That integer isn’t super useful by itself. We oughta convert it to something else next since:
- different properties have different units, and
- we want each property to go from a start value to an end value.
Think about an image’s opacity going from 0 to 1, an object rotating from 0deg to 360deg, or an element’s offset-distance going from 0% to 100%. We want to interpolate between these values as --int-width gets bigger, but right now it’s just an integer that usually ranges between 0 to 1600, which is inflexible and can’t be easily converted to any of the end values.
The best solution is to turn --int-width into a number that goes from 0 to 1. So, as the screen gets bigger, we can multiply it by the desired end value. Lacking a better name, I call this “0-to-1” value --wideness. If we have --wideness, all the last examples become possible:
/* If `--wideness is 0.5 */ .element { opacity: var(--wideness); /* is 0.5 */ translate: rotate(calc(wideness(400px, 1200px) * 360deg)); /* is 180deg */ offset-distance: calc(var(--wideness) * 100%); /* is 50% */ }So --wideness is a value between 0 to 1 that represents how wide the screen is: 0 represents when the screen is narrow, and 1 represents when it’s wide. But we still have to set what those values mean in the viewport. For example, we may want 0 to be 400px and 1 to be 1200px, our viewport transitions will run between these values. Anything below and above is clamped to 0 and 1, respectively.
In CSS, we can write that as follows:
:root { /* Both bounds are unitless */ --lower-bound: 400; --upper-bound: 1200; --wideness: calc( (clamp(var(--lower-bound), var(--int-width), var(--upper-bound)) - var(--lower-bound)) / (var(--upper-bound) - var(--lower-bound)) ); }Besides easy conversions, the --wideness variable lets us define the lower and upper limits in which the transition should run. And what’s even better, we can set the transition zone at a middle spot so that the user can see it in its full glory. Otherwise, the screen would need to be 0px so that --wideness reaches 0 and who knows how wide to reach 1.
CodePen Embed Fallback We got the --wideness. What’s next?For starters, the title’s markup is divided into spans since there is no CSS-way to select specific words in a sentence:
<h1><span>Resize</span> and <span>enjoy!</span></h1>And since we will be doing the line wrapping ourselves, it’s important to unset some defaults:
h1 { position: absolute; /* Keeps the text at the center */ white-space: nowrap; /* Disables line wrapping */ }The transition should work without the base styling, but it’s just too plain-looking. They are below if you want to copy them onto your stylesheet:
CodePen Embed FallbackAnd just as a recap, our current hack looks like this:
@property --100vw { syntax: "<length>"; initial-value: 0px; inherits: false; } :root { --100vw: 100vw; --int-width: calc(10000 * tan(atan2(var(--100vw), 10000px))); --lower-bound: 400; --upper-bound: 1200; --wideness: calc( (clamp(var(--lower-bound), var(--int-width), var(--upper-bound)) - var(--lower-bound)) / (var(--upper-bound) - var(--lower-bound)) ); }OK, enough with the set-up. It’s time to use our new values and make the viewport transition. We first gotta identify how the title should be rearranged for smaller screens: as you saw in the initial demo, the first span goes up and right, while the second span does the opposite and goes down and left. So, the end position for both spans translates to the following values:
h1 { span:nth-child(1) { display: inline-block; /* So transformations work */ position: relative; bottom: 1.2lh; left: 50%; transform: translate(-50%); } span:nth-child(2) { display: inline-block; /* So transformations work */ position: relative; bottom: -1.2lh; left: -50%; transform: translate(50%); } }Before going forward, both formulas are basically the same, but with different signs. We can rewrite them at once bringing one new variable: --direction. It will be either 1 or -1 and define which direction to run the transition:
h1 { span { display: inline-block; position: relative; bottom: calc(1.2lh * var(--direction)); left: calc(50% * var(--direction)); transform: translate(calc(-50% * var(--direction))); } span:nth-child(1) { --direction: 1; } span:nth-child(2) { --direction: -1; } } CodePen Embed FallbackThe next step would be bringing --wideness into the formula so that the values change as the screen resizes. However, we can’t just multiply everything by --wideness. Why? Let’s see what happens if we do:
span { display: inline-block; position: relative; bottom: calc(var(--wideness) * 1.2lh * var(--direction)); left: calc(var(--wideness) * 50% * var(--direction)); transform: translate(calc(var(--wideness) * -50% * var(--direction))); }As you’ll see, everything is backwards! The words wrap when the screen is too wide, and unwrap when the screen is too narrow:
CodePen Embed FallbackUnlike our first examples, in which the transition ends as --wideness increases from 0 to 1, we want to complete the transition as --wideness decreases from 1 to 0, i.e. while the screen gets smaller the properties need to reach their end value. This isn’t a big deal, as we can rewrite our formula as a subtraction, in which the subtracting number gets bigger as --wideness increases:
span { display: inline-block; position: relative; bottom: calc((1.2lh - var(--wideness) * 1.2lh) * var(--direction)); left: calc((50% - var(--wideness) * 50%) * var(--direction)); transform: translate(calc((-50% - var(--wideness) * -50%) * var(--direction))); }And now everything moves in the right direction while resizing the screen!
CodePen Embed FallbackHowever, you will notice how words move in a straight line and some words overlap while resizing. We can’t allow this since a user with a specific screen size may get stuck at that point in the transition. Viewport transitions are cool, but not at the expense of ruining the experience for certain screen sizes.
Instead of moving in a straight line, words should move in a curve such that they pass around the central word. Don’t worry, making a curve here is easier than it looks: just move the spans twice as fast in the x-axis as they do in the y-axis. This can be achieved by multiplying --wideness by 2, although we have to cap it at 1 so it doesn’t overshoot past the final value.
span { display: inline-block; position: relative; bottom: calc((1.2lh - var(--wideness) * 1.2lh) * var(--direction)); left: calc((50% - min(var(--wideness) * 2, 1) * 50%) * var(--direction)); transform: translate(calc((-50% - min(var(--wideness) * 2, 1) * -50%) * var(--direction))); }Look at that beautiful curve, just avoiding the central text:
CodePen Embed Fallback This is just the beginning!It’s surprising how powerful having the viewport as an integer can be, and what’s even crazier, the last example is one of the most basic transitions you could make with this typecasting hack. Once you do the initial setup, I can imagine a lot more possible transitions, and --widenesss is so useful, it’s like having a new CSS feature right now.
I expect to see more about “Viewport Transitions” in the future because they do make websites feel more “alive” than adaptive.
Typecasting and Viewport Transitions in CSS With tan(atan2()) originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Organizing Design System Component Patterns With CSS Cascade Layers
I’m trying to come up with ways to make components more customizable, more efficient, and easier to use and understand, and I want to describe a pattern I’ve been leaning into using CSS Cascade Layers.
I enjoy organizing code and find cascade layers a fantastic way to organize code explicitly as the cascade looks at it. The neat part is, that as much as it helps with “top-level” organization, cascade layers can be nested, which allows us to author more precise styles based on the cascade.
The only downside here is your imagination, nothing stops us from over-engineering CSS. And to be clear, you may very well consider what I’m about to show you as a form of over-engineering. I think I’ve found a balance though, keeping things simple yet organized, and I’d like to share my findings.
The anatomy of a CSS component patternLet’s explore a pattern for writing components in CSS using a button as an example. Buttons are one of the more popular components found in just about every component library. There’s good reason for that popularity because buttons can be used for a variety of use cases, including:
- performing actions, like opening a drawer,
- navigating to different sections of the UI, and
- holding some form of state, such as focus or hover.
And buttons come in several different flavors of markup, like <button>, input[type="button"], and <a class="button">. There are even more ways to make buttons than that, if you can believe it.
On top of that, different buttons perform different functions and are often styled accordingly so that a button for one type of action is distinguished from another. Buttons also respond to state changes, such as when they are hovered, active, and focused. If you have ever written CSS with the BEM syntax, we can sort of think along those lines within the context of cascade layers.
.button {} .button-primary {} .button-secondary {} .button-warning {} /* etc. */Okay, now, let’s write some code. Specifically, let’s create a few different types of buttons. We’ll start with a .button class that we can set on any element that we want to be styled as, well, a button! We already know that buttons come in different flavors of markup, so a generic .button class is the most reusable and extensible way to select one or all of them.
.button { /* Styles common to all buttons */ } Using a cascade layerThis is where we can insert our very first cascade layer! Remember, the reason we want a cascade layer in the first place is that it allows us to set the CSS Cascade’s reading order when evaluating our styles. We can tell CSS to evaluate one layer first, followed by another layer, then another — all according to the order we want. This is an incredible feature that grants us superpower control over which styles “win” when applied by the browser.
We’ll call this layer components because, well, buttons are a type of component. What I like about this naming is that it is generic enough to support other components in the future as we decide to expand our design system. It scales with us while maintaining a nice separation of concerns with other styles we write down the road that maybe aren’t specific to components.
/* Components top-level layer */ @layer components { .button { /* Styles common to all buttons */ } } Nesting cascade layersHere is where things get a little weird. Did you know you can nest cascade layers inside classes? That’s totally a thing. So, check this out, we can introduce a new layer inside the .button class that’s already inside its own layer. Here’s what I mean:
/* Components top-level layer */ @layer components { .button { /* Component elements layer */ @layer elements { /* Styles */ } } }This is how the browser interprets that layer within a layer at the end of the day:
@layer components { @layer elements { .button { /* button styles... */ } } }This isn’t a post just on nesting styles, so I’ll just say that your mileage may vary when you do it. Check out Andy Bell’s recent article about using caution with nested styles.
Structuring stylesSo far, we’ve established a .button class inside of a cascade layer that’s designed to hold any type of component in our design system. Inside that .button is another cascade layer, this one for selecting the different types of buttons we might encounter in the markup. We talked earlier about buttons being <button>, <input>, or <a> and this is how we can individually select style each type.
We can use the :is() pseudo-selector function as that is akin to saying, “If this .button is an <a> element, then apply these styles.”
/* Components top-level layer */ @layer components { .button { /* Component elements layer */ @layer elements { /* styles common to all buttons */ &:is(a) { /* <a> specific styles */ } &:is(button) { /* <button> specific styles */ } /* etc. */ } } } Defining default button stylesI’m going to fill in our code with the common styles that apply to all buttons. These styles sit at the top of the elements layer so that they are applied to any and all buttons, regardless of the markup. Consider them default button styles, so to speak.
/* Components top-level layer */ @layer components { .button { /* Component elements layer */ @layer elements { background-color: darkslateblue; border: 0; color: white; cursor: pointer; display: grid; font-size: 1rem; font-family: inherit; line-height: 1; margin: 0; padding-block: 0.65rem; padding-inline: 1rem; place-content: center; width: fit-content; } } } Defining button state stylesWhat should our default buttons do when they are hovered, clicked, or in focus? These are the different states that the button might take when the user interacts with them, and we need to style those accordingly.
I’m going to create a new cascade sub-layer directly under the elements sub-layer called, creatively, states:
/* Components top-level layer */ @layer components { .button { /* Component elements layer */ @layer elements { /* Styles common to all buttons */ } /* Component states layer */ @layer states { /* Styles for specific button states */ } } }Pause and reflect here. What states should we target? What do we want to change for each of these states?
Some states may share similar property changes, such as :hover and :focus having the same background color. Luckily, CSS gives us the tools we need to tackle such problems, using the :where() function to group property changes based on the state. Why :where() instead of :is()? :where() comes with zero specificity, meaning it’s a lot easier to override than :is(), which takes the specificity of the element with the highest specificity score in its arguments. Maintaining low specificity is a virtue when it comes to writing scalable, maintainable CSS.
/* Component states layer */ @layer states { &:where(:hover, :focus-visible) { /* button hover and focus state styles */ } }But how do we update the button’s styles in a meaningful way? What I mean by that is how do we make sure that the button looks like it’s hovered or in focus? We could just slap a new background color on it, but ideally, the color should be related to the background-color set in the elements layer.
So, let’s refactor things a bit. Earlier, I set the .button element’s background-color to darkslateblue. I want to reuse that color, so it behooves us to make that into a CSS variable so we can update it once and have it apply everywhere. Relying on variables is yet another virtue of writing scalable and maintainable CSS.
I’ll create a new variable called --button-background-color that is initially set to darkslateblue and then set it on the default button styles:
/* Component elements layer */ @layer elements { --button-background-color: darkslateblue; background-color: var(--button-background-color); border: 0; color: white; cursor: pointer; display: grid; font-size: 1rem; font-family: inherit; line-height: 1; margin: 0; padding-block: 0.65rem; padding-inline: 1rem; place-content: center; width: fit-content; }Now that we have a color stored in a variable, we can set that same variable on the button’s hovered and focused states in our other layer, using the relatively new color-mix() function to convert darkslateblue to a lighter color when the button is hovered or in focus.
Back to our states layer! We’ll first mix the color in a new CSS variable called --state-background-color:
/* Component states layer */ @layer states { &:where(:hover, :focus-visible) { /* custom property only used in state */ --state-background-color: color-mix( in srgb, var(--button-background-color), white 10% ); } }We can then apply that color as the background color by updating the background-color property.
/* Component states layer */ @layer states { &:where(:hover, :focus-visible) { /* custom property only used in state */ --state-background-color: color-mix( in srgb, var(--button-background-color), white 10% ); /* applying the state background-color */ background-color: var(--state-background-color); } } Defining modified button stylesAlong with elements and states layers, you may be looking for some sort of variation in your components, such as modifiers. That’s because not all buttons are going to look like your default button. You might want one with a green background color for the user to confirm a decision. Or perhaps you want a red one to indicate danger when clicked. So, we can take our existing default button styles and modify them for those specific use cases
If we think about the order of the cascade — always flowing from top to bottom — we don’t want the modified styles to affect the styles in the states layer we just made. So, let’s add a new modifiers layer in between elements and states:
/* Components top-level layer */ @layer components { .button { /* Component elements layer */ @layer elements { /* etc. */ } /* Component modifiers layer */ @layer modifiers { /* new layer! */ } /* Component states layer */ @layer states { /* etc. */ } }Similar to how we handled states, we can now update the --button-background-color variable for each button modifier. We could modify the styles further, of course, but we’re keeping things fairly straightforward to demonstrate how this system works.
We’ll create a new class that modifies the background-color of the default button from darkslateblue to darkgreen. Again, we can rely on the :is() selector because we want the added specificity in this case. That way, we override the default button style with the modifier class. We’ll call this class .success (green is a “successful” color) and feed it to :is():
/* Component modifiers layer */ @layer modifiers { &:is(.success) { --button-background-color: darkgreen; } }If we add the .success class to one of our buttons, it becomes darkgreen instead darkslateblue which is exactly what we want. And since we already do some color-mix()-ing in the states layer, we’ll automatically inherit those hover and focus styles, meaning darkgreen is lightened in those states.
/* Components top-level layer */ @layer components { .button { /* Component elements layer */ @layer elements { --button-background-color: darkslateblue; background-color: var(--button-background-color); /* etc. */ /* Component modifiers layer */ @layer modifiers { &:is(.success) { --button-background-color: darkgreen; } } /* Component states layer */ @layer states { &:where(:hover, :focus) { --state-background-color: color-mix( in srgb, var(--button-background-color), white 10% ); background-color: var(--state-background-color); } } } } Putting it all togetherWe can refactor any CSS property we need to modify into a CSS custom property, which gives us a lot of room for customization.
/* Components top-level layer */ @layer components { .button { /* Component elements layer */ @layer elements { --button-background-color: darkslateblue; --button-border-width: 1px; --button-border-style: solid; --button-border-color: transparent; --button-border-radius: 0.65rem; --button-text-color: white; --button-padding-inline: 1rem; --button-padding-block: 0.65rem; background-color: var(--button-background-color); border: var(--button-border-width) var(--button-border-style) var(--button-border-color); border-radius: var(--button-border-radius); color: var(--button-text-color); cursor: pointer; display: grid; font-size: 1rem; font-family: inherit; line-height: 1; margin: 0; padding-block: var(--button-padding-block); padding-inline: var(--button-padding-inline); place-content: center; width: fit-content; } /* Component modifiers layer */ @layer modifiers { &:is(.success) { --button-background-color: darkgreen; } &:is(.ghost) { --button-background-color: transparent; --button-text-color: black; --button-border-color: darkslategray; --button-border-width: 3px; } } /* Component states layer */ @layer states { &:where(:hover, :focus) { --state-background-color: color-mix( in srgb, var(--button-background-color), white 10% ); background-color: var(--state-background-color); } } } } CodePen Embed FallbackP.S. Look closer at that demo and check out how I’m adjusting the button’s background using light-dark() — then go read Sara Joy’s “Come to the light-dark() Side” for a thorough rundown of how that works!
What do you think? Is this something you would use to organize your styles? I can see how creating a system of cascade layers could be overkill for a small project with few components. But even a little toe-dipping into things like we just did illustrates how much power we have when it comes to managing — and even taming — the CSS Cascade. Buttons are deceptively complex but we saw how few styles it takes to handle everything from the default styles to writing the styles for their states and modified versions.
Organizing Design System Component Patterns With CSS Cascade Layers originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Make Any File a Template Using This Hidden macOS Tool
From MacRumors:
Stationery Pad is a handy way to nix a step in your workflow if you regularly use document templates on your Mac. The long-standing Finder feature essentially tells a file’s parent application to open a copy of it by default, ensuring that the original file remains unedited.
This works for any kind of file, including HTML, CSS, JavaScriprt, or what have you. You can get there with CMD+i or right-click and select “Get info.”
Make Any File a Template Using This Hidden macOS Tool originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Container query units: cqi and cqb
A little gem from Kevin Powell’s “HTML & CSS Tip of the Week” website, reminding us that using container queries opens up container query units for sizing things based on the size of the queried container.
cqi and cqb are similar to vw and vh, but instead of caring about the viewport, they care about their containers size.
cqi is your inline-size unit (usually width in horizontal writing modes), while cqbhandles block-size (usually height).
So, 1cqi is equivalent to 1% of the container’s inline size, and 1cqb is equal to 1% of the container’s block size. I’d be remiss not to mention the cqmin and cqmax units, which evaluate either the container’s inline or block size. So, we could say 50cqmax and that equals 50% of the container’s size, but it will look at both the container’s inline and block size, determine which is greater, and use that to calculate the final computed value.
That’s a nice dash of conditional logic. It can help maintain proportions if you think the writing mode might change on you, such as moving from horizontal to vertical.
Container query units: cqi and cqb originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Chrome 133 Goodies
I often wonder what it’s like working for the Chrome team. You must get issued some sort of government-level security clearance for the latest browser builds that grants you permission to bash on them ahead of everyone else and come up with these rad demos showing off the latest features. No, I’m, not jealous, why are you asking?
Totally unrelated, did you see the release notes for Chrome 133? It’s currently in beta, but the Chrome team has been publishing a slew of new articles with pretty incredible demos that are tough to ignore. I figured I’d round those up in one place.
attr() for the masses!We’ve been able to use HTML attributes in CSS for some time now, but it’s been relegated to the content property and only parsed strings.
<h1 data-color="orange">Some text</h1> h1::before { content: ' (Color: ' attr(data-color) ') '; }Bramus demonstrates how we can now use it on any CSS property, including custom properties, in Chrome 133. So, for example, we can take the attribute’s value and put it to use on the element’s color property:
h1 { color: attr(data-color type(<color>), #fff) }This is a trite example, of course. But it helps illustrate that there are three moving pieces here:
- the attribute (data-color)
- the type (type(<color>))
- the fallback value (#fff)
We make up the attribute. It’s nice to have a wildcard we can insert into the markup and hook into for styling. The type() is a new deal that helps CSS know what sort of value it’s working with. If we had been working with a numeric value instead, we could ditch that in favor of something less verbose. For example, let’s say we’re using an attribute for the element’s font size:
<div data-size="20">Some text</div>Now we can hook into the data-size attribute and use the assigned value to set the element’s font-size property, based in px units:
h1 { color: attr(data-size px, 16); } CodePen Embed FallbackThe fallback value is optional and might not be necessary depending on your use case.
Scroll states in container queries!This is a mind-blowing one. If you’ve ever wanted a way to style a sticky element when it’s in a “stuck” state, then you already know how cool it is to have something like this. Adam Argyle takes the classic pattern of an alphabetical list and applies styles to the letter heading when it sticks to the top of the viewport. The same is true of elements with scroll snapping and elements that are scrolling containers.
In other words, we can style elements when they are “stuck”, when they are “snapped”, and when they are “scrollable”.
Quick little example that you’ll want to open in a Chromium browser:
CodePen Embed FallbackThe general idea (and that’s all I know for now) is that we register a container… you know, a container that we can query. We give that container a container-type that is set to the type of scrolling we’re working with. In this case, we’re working with sticky positioning where the element “sticks” to the top of the page.
.sticky-nav { container-type: scroll-state; }A container can’t query itself, so that basically has to be a wrapper around the element we want to stick. Menus are a little funny because we have the <nav> element and usually stuff it with an unordered list of links. So, our <nav> can be the container we query since we’re effectively sticking an unordered list to the top of the page.
<nav class="sticky-nav"> <ul> <li><a href="#">Home</a></li> <li><a href="#">About</a></li> <li><a href="#">Blog</a></li> </ul> </nav>We can put the sticky logic directly on the <nav> since it’s technically holding what gets stuck:
.sticky-nav { container-type: scroll-state; /* set a scroll container query */ position: sticky; /* set sticky positioning */ top: 0; /* stick to the top of the page */ }I supposed we could use the container shorthand if we were working with multiple containers and needed to distinguish one from another with a container-name. Either way, now that we’ve defined a container, we can query it using @container! In this case, we declare the type of container we’re querying:
@container scroll-state() { }And we tell it the state we’re looking for:
@container scroll-state(stuck: top) {If we were working with a sticky footer instead of a menu, then we could say stuck: bottom instead. But the kicker is that once the <nav> element sticks to the top, we get to apply styles to it in the @container block, like so:
.sticky-nav { border-radius: 12px; container-type: scroll-state; position: sticky; top: 0; /* When the nav is in a "stuck" state */ @container scroll-state(stuck: top) { border-radius: 0; box-shadow: 0 3px 10px hsl(0 0 0 / .25); width: 100%; } }It seems to work when nesting other selectors in there. So, for example, we can change the links in the menu when the navigation is in its stuck state:
.sticky-nav { /* Same as before */ a { color: #000; font-size: 1rem; } /* When the nav is in a "stuck" state */ @container scroll-state(stuck: top) { /* Same as before */ a { color: orangered; font-size: 1.5rem; } } }So, yeah. As I was saying, it must be pretty cool to be on the Chrome developer team and get ahead of stuff like this, as it’s released. Big ol’ thanks to Bramus and Adam for consistently cluing us in on what’s new and doing the great work it takes to come up with such amazing demos to show things off.
Chrome 133 Goodies originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
The Mistakes of CSS
Surely you have seen a CSS property and thought “Why?” For example:
Why doesn’t z-index work on all elements, and why is it “-index” anyways?
Or:
Why do we need interpolate-size to animate to auto?
You are not alone. CSS was born in 1996 (it can legally order a beer, you know!) and was initially considered a way to style documents; I don’t think anyone imagined everything CSS would be expected to do nearly 30 years later. If we had a time machine, many things would be done differently to match conventions or to make more sense. Heck, even the CSS Working Group admits to wanting a time-traveling contraption… in the specifications!
NOTE: If we had a time machine, this property wouldn’t need to exist.
CSS Values and Units Module Level 5, Section 10.4If by some stroke of opportunity, I was given free rein to rename some things in CSS, a couple of ideas come to mind, but if you want more, you can find an ongoing list of mistakes made in CSS… by the CSS Working Group! Take, for example, background-repeat:
Not quite a mistake, because it was a reasonable default for the 90s, but it would be more helpful since then if background-repeat defaulted to no-repeat.
Right now, people are questioning if CSS Flexbox and CSS Grid should share more syntax in light of the upcoming CSS Masonry layout specifications.
Why not fix them? Sadly, it isn’t as easy as fixing something. People already built their websites with these quirks in mind, and changing them would break those sites. Consider it technical debt.
This is why I think the CSS Working Group deserves an onslaught of praise. Designing new features that are immutable once shipped has to be a nerve-wracking experience that involves inexact science. It’s not that we haven’t seen the specifications change or evolve in the past — they most certainly have — but the value of getting things right the first time is a beast of burden.
The Mistakes of CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Steven Heller’s Font of the Month: Vibro
Read the book, Typographic Firsts
This month, Steven Heller takes a closer look at the Vibro font family.
The post Steven Heller’s Font of the Month: Vibro appeared first on I Love Typography.
What on Earth is the `types` Descriptor in View Transitions?
Have you ever stumbled upon something new and went to research it just to find that there is little-to-no information about it? It’s a mixed feeling: confusing and discouraging because there is no apparent direction, but also exciting because it’s probably new to lots of people, not just you. Something like that happened to me while writing an Almanac’s entry for the @view-transition at-rule and its types descriptor.
You may already know about Cross-Document View Transitions: With a few lines of CSS, they allow for transitions between two pages, something that in the past required a single-app framework with a side of animation library. In other words, lots of JavaScript.
To start a transition between two pages, we have to set the @view-transition at-rule’s navigation descriptor to auto on both pages, and that gives us a smooth cross-fade transition between the two pages. So, as the old page fades out, the new page fades in.
@view-transition { navigation: auto; }That’s it! And navigation is the only descriptor we need. In fact, it’s the only descriptor available for the @view-transition at-rule, right? Well, turns out there is another descriptor, a lesser-known brother, and one that probably envies how much attention navigation gets: the types descriptor.
What do people say about types?Cross-Documents View Transitions are still fresh from the oven, so it’s normal that people haven’t fully dissected every aspect of them, especially since they introduce a lot of new stuff: a new at-rule, a couple of new properties and tons of pseudo-elements and pseudo-classes. However, it still surprises me the little mention of types. Some documentation fails to even name it among the valid @view-transition descriptors. Luckily, though, the CSS specification does offer a little clarification about it:
The types descriptor sets the active types for the transition when capturing or performing the transition.
To be more precise, types can take a space-separated list with the names of the active types (as <custom-ident>), or none if there aren’t valid active types for that page.
- Name: types
- For: @view-transition
- Value: none | <custom-ident>+
- Initial: none
So the following values would work inside types:
@view-transition { navigation: auto; types: bounce; } /* or a list */ @view-transition { navigation: auto; types: bounce fade rotate; }Yes, but what exactly are “active” types? That word “active” seems to be doing a lot of heavy lifting in the CSS specification’s definition and I want to unpack that to better understand what it means.
Active types in view transitionsThe problem: A cross-fade animation for every page is good, but a common thing we need to do is change the transition depending on the pages we are navigating between. For example, on paginated content, we could slide the content to the right when navigating forward and to the left when navigating backward. In a social media app, clicking a user’s profile picture could persist the picture throughout the transition. All this would mean defining several transitions in our CSS, but doing so would make them conflict with each other in one big slop. What we need is a way to define several transitions, but only pick one depending on how the user navigates the page.
The solution: Active types define which transition gets used and which elements should be included in it. In CSS, they are used through :active-view-transition-type(), a pseudo-class that matches an element if it has a specific active type. Going back to our last example, we defined the document’s active type as bounce. We could enclose that bounce animation behind an :active-view-transition-type(bounce), such that it only triggers on that page.
/* This one will be used! */ html:active-view-transition-type(bounce) { &::view-transition-old(page) { /* Custom Animation */ } &::view-transition-new(page) { /* Custom Animation */ } }This prevents other view transitions from running if they don’t match any active type:
/* This one won't be used! */ html:active-view-transition-type(slide) { &::view-transition-old(page) { /* Custom Animation */ } &::view-transition-new(page) { /* Custom Animation */ } }I asked myself whether this triggers the transition when going to the page, when out of the page, or in both instances. Turns out it only limits the transition when going to the page, so the last bounce animation is only triggered when navigating toward a page with a bounce value on its types descriptor, but not when leaving that page. This allows for custom transitions depending on which page we are going to.
The following demo has two pages that share a stylesheet with the bounce and slide view transitions, both respectively enclosed behind an :active-view-transition-type(bounce) and :active-view-transition-type(slide) like the last example. We can control which page uses which view transition through the types descriptor.
The first page uses the bounce animation:
@view-transition { navigation: auto; types: bounce; }The second page uses the slide animation:
@view-transition { navigation: auto; types: slide; }You can visit the demo here and see the full code over at GitHub.
The types descriptor is used more in JavaScriptThe main problem is that we can only control the transition depending on the page we’re navigating to, which puts a major cap on how much we can customize our transitions. For instance, the pagination and social media examples we looked at aren’t possible just using CSS, since we need to know where the user is coming from. Luckily, using the types descriptor is just one of three ways that active types can be populated. Per spec, they can be:
- Passed as part of the arguments to startViewTransition(callbackOptions)
- Mutated at any time, using the transition’s types
- Declared for a cross-document view transition, using the types descriptor.
The first option is when starting a view transition from JavaScript, but we want to trigger them when the user navigates to the page by themselves (like when clicking a link). The third option is using the types descriptor which we already covered. The second option is the right one for this case! Why? It lets us set the active transition type on demand, and we can perform that change just before the transition happens using the pagereveal event. That means we can get the user’s start and end page from JavaScript and then set the correct active type for that case.
I must admit, I am not the most experienced guy to talk about this option, so once I demo the heck out of different transitions with active types I’ll come back with my findings! In the meantime, I encourage you to read about active types here if you are like me and want more on view transitions:
- View transition types in cross-document view transitions (Bramus)
- Customize the direction of a view transition with JavaScript (Umar Hansa)
What on Earth is the `types` Descriptor in View Transitions? originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Revisiting CSS Multi-Column Layout
Honestly, it’s difficult for me to come to terms with, but almost 20 years have passed since I wrote my first book, Transcending CSS. In it, I explained how and why to use what was the then-emerging Multi-Column Layout module.
Hint: I published an updated version, Transcending CSS Revisited, which is free to read online.
Perhaps because, before the web, I’d worked in print, I was over-excited at the prospect of dividing content into columns without needing extra markup purely there for presentation. I’ve used Multi-Column Layout regularly ever since. Yet, CSS Columns remains one of the most underused CSS layout tools. I wonder why that is?
Holes in the specificationFor a long time, there were, and still are, plenty of holes in Multi-Column Layout. As Rachel Andrew — now a specification editor — noted in her article five years ago:
“The column boxes created when you use one of the column properties can’t be targeted. You can’t address them with JavaScript, nor can you style an individual box to give it a background colour or adjust the padding and margins. All of the column boxes will be the same size. The only thing you can do is add a rule between columns.”
She’s right. And that’s still true. You can’t style columns, for example, by alternating background colours using some sort of :nth-column() pseudo-class selector. You can add a column-rule between columns using border-style values like dashed, dotted, and solid, and who can forget those evergreen groove and ridge styles? But you can’t apply border-image values to a column-rule, which seems odd as they were introduced at roughly the same time. The Multi-Column Layout is imperfect, and there’s plenty I wish it could do in the future, but that doesn’t explain why most people ignore what it can do today.
Patchy browser implementation for a long timeLegacy browsers simply ignored the column properties they couldn’t process. But, when Multi-Column Layout was first launched, most designers and developers had yet to accept that websites needn’t look the same in every browser.
Early on, support for Multi-Column Layout was patchy. However, browsers caught up over time, and although there are still discrepancies — especially in controlling content breaks — Multi-Column Layout has now been implemented widely. Yet, for some reason, many designers and developers I speak to feel that CSS Columns remain broken. Yes, there’s plenty that browser makers should do to improve their implementations, but that shouldn’t prevent people from using the solid parts today.
Readability and usability with scrollingMaybe the main reason designers and developers haven’t embraced Multi-Column Layout as they have CSS Grid and Flexbox isn’t in the specification or its implementation but in its usability. Rachel pointed this out in her article:
“One reason we don’t see multicol used much on the web is that it would be very easy to end up with a reading experience which made the reader scroll in the block dimension. That would mean scrolling up and down vertically for those of us using English or another vertical writing mode. This is not a good reading experience!”
That’s true. No one would enjoy repeatedly scrolling up and down to read a long passage of content set in columns. She went on:
“Neither of these things is ideal, and using multicol on the web is something we need to think about very carefully in terms of the amount of content we might be aiming to flow into our columns.”
But, let’s face it, thinking very carefully is what designers and developers should always be doing.
Sure, if you’re dumb enough to dump a large amount of content into columns without thinking about its design, you’ll end up serving readers a poor experience. But why would you do that when headlines, images, and quotes can span columns and reset the column flow, instantly improving readability? Add to that container queries and newer unit values for text sizing, and there really isn’t a reason to avoid using Multi-Column Layout any longer.
A brief refresher on properties and valuesLet’s run through a refresher. There are two ways to flow content into multiple columns; first, by defining the number of columns you need using the column-count property:
CodePen Embed FallbackSecond, and often best, is specifying the column width, leaving a browser to decide how many columns will fit along the inline axis. For example, I’m using column-width to specify that my columns are over 18rem. A browser creates as many 18rem columns as possible to fit and then shares any remaining space between them.
CodePen Embed FallbackThen, there is the gutter (or column-gap) between columns, which you can specify using any length unit. I prefer using rem units to maintain the gutters’ relationship to the text size, but if your gutters need to be 1em, you can leave this out, as that’s a browser’s default gap.
CodePen Embed FallbackThe final column property is that divider (or column-rule) to the gutters, which adds visual separation between columns. Again, you can set a thickness and use border-style values like dashed, dotted, and solid.
CodePen Embed FallbackThese examples will be seen whenever you encounter a Multi-Column Layout tutorial, including CSS-Tricks’ own Almanac. The Multi-Column Layout syntax is one of the simplest in the suite of CSS layout tools, which is another reason why there are few reasons not to use it.
Multi-Column Layout is even more relevant todayWhen I wrote Transcending CSS and first explained the emerging Multi-Column Layout, there were no rem or viewport units, no :has() or other advanced selectors, no container queries, and no routine use of media queries because responsive design hadn’t been invented.
We didn’t have calc() or clamp() for adjusting text sizes, and there was no CSS Grid or Flexible Box Layout for precise control over a layout. Now we do, and all these properties help to make Multi-Column Layout even more relevant today.
Now, you can use rem or viewport units combined with calc() and clamp() to adapt the text size inside CSS Columns. You can use :has() to specify when columns are created, depending on the type of content they contain. Or you might use container queries to implement several columns only when a container is large enough to display them. Of course, you can also combine a Multi-Column Layout with CSS Grid or Flexible Box Layout for even more imaginative layout designs.
Using Multi-Column Layout today Patty Meltt is an up-and-coming country music sensation. She’s not real, but the challenges of designing and developing websites like hers are.My challenge was to implement a flexible article layout without media queries which adapts not only to screen size but also whether or not a <figure> is present. To improve the readability of running text in what would potentially be too-long lines, it should be set in columns to narrow the measure. And, as a final touch, the text size should adapt to the width of the container, not the viewport.
Article with no <figure> element. What would potentially be too-long lines of text are set in columns to improve readability by narrowing the measure. Article containing a <figure> element. No column text is needed for this narrower measure.The HTML for this layout is rudimentary. One <section>, one <main>, and one <figure> (or not:)
<section> <main> <h1>About Patty</h1> <p>…</p> </main> <figure> <img> </figure> </section>I started by adding Multi-Column Layout styles to the <main> element using the column-width property to set the width of each column to 40ch (characters). The max-width and automatic inline margins reduce the content width and center it in the viewport:
main { margin-inline: auto; max-width: 100ch; column-width: 40ch; column-gap: 3rem; column-rule: .5px solid #98838F; }Next, I applied a flexible box layout to the <section> only if it :has() a direct descendant which is a <figure>:
section:has(> figure) { display: flex; flex-wrap: wrap; gap: 0 3rem; }This next min-width: min(100%, 30rem) — applied to both the <main> and <figure> — is a combination of the min-width property and the min() CSS function. The min() function allows you to specify two or more values, and a browser will choose the smallest value from them. This is incredibly useful for responsive layouts where you want to control the size of an element based on different conditions:
section:has(> figure) main { flex: 1; margin-inline: 0; min-width: min(100%, 30rem); } section:has(> figure) figure { flex: 4; min-width: min(100%, 30rem); }What’s efficient about this implementation is that Multi-Column Layout styles are applied throughout, with no need for media queries to switch them on or off.
Adjusting text size in relation to column width helps improve readability. This has only recently become easy to implement with the introduction of container queries, their associated values including cqi, cqw, cqmin, and cqmax. And the clamp() function. Fortunately, you don’t have to work out these text sizes manually as ClearLeft’s Utopia will do the job for you.
My headlines and paragraph sizes are clamped to their minimum and maximum rem sizes and between them text is fluid depending on their container’s inline size:
h1 { font-size: clamp(5.6526rem, 5.4068rem + 1.2288cqi, 6.3592rem); } h2 { font-size: clamp(1.9994rem, 1.9125rem + 0.4347cqi, 2.2493rem); } p { font-size: clamp(1rem, 0.9565rem + 0.2174cqi, 1.125rem); }So, to specify the <main> as the container on which those text sizes are based, I applied a container query for its inline size:
main { container-type: inline-size; }Open the final result in a desktop browser, when you’re in front of one. It’s a flexible article layout without media queries which adapts to screen size and the presence of a <figure>. Multi-Column Layout sets text in columns to narrow the measure and the text size adapts to the width of its container, not the viewport.
CodePen Embed Fallback Modern CSS is solving many prior problems Structure content with spanning elements which will restart the flow of columns and prevent people from scrolling long distances. Prevent figures from dividing their images and captions between columns.Almost every article I’ve ever read about Multi-Column Layout focuses on its flaws, especially usability. CSS-Tricks’ own Geoff Graham even mentioned the scrolling up and down issue when he asked, “When Do You Use CSS Columns?”
“But an entire long-form article split into columns? I love it in newspapers but am hesitant to scroll down a webpage to read one column, only to scroll back up to do it again.”
Fortunately, the column-span property — which enables headlines, images, and quotes to span columns, resets the column flow, and instantly improves readability — now has solid support in browsers:
h1, h2, blockquote { column-span: all; }But the solution to the scrolling up and down issue isn’t purely technical. It also requires content design. This means that content creators and designers must think carefully about the frequency and type of spanning elements, dividing a Multi-Column Layout into shallower sections, reducing the need to scroll and improving someone’s reading experience.
Another prior problem was preventing headlines from becoming detached from their content and figures, dividing their images and captions between columns. Thankfully, the break-after property now also has widespread support, so orphaned images and captions are now a thing of the past:
figure { break-after: column; }Open this final example in a desktop browser:
CodePen Embed Fallback You should take a fresh look at Multi-Column LayoutMulti-Column Layout isn’t a shiny new tool. In fact, it remains one of the most underused layout tools in CSS. It’s had, and still has, plenty of problems, but they haven’t reduced its usefulness or its ability to add an extra level of refinement to a product or website’s design. Whether you haven’t used Multi-Column Layout in a while or maybe have never tried it, now’s the time to take a fresh look at Multi-Column Layout.
Revisiting CSS Multi-Column Layout originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Positioning Text Around Elements With CSS Offset
When it comes to positioning elements on a page, including text, there are many ways to go about it in CSS — the literal position property with corresponding inset-* properties, translate, margin, anchor() (limited browser support at the moment), and so forth. The offset property is another one that belongs in that list.
The offset property is typically used for animating an element along a predetermined path. For instance, the square in the following example traverses a circular path:
<div class="circle"> <div class="square"></div> </div> @property --p { syntax: '<percentage>'; inherits: false; initial-value: 0%; } .square { offset: top 50% right 50% circle(50%) var(--p); transition: --p 1s linear; /* Equivalent to: offset-position: top 50% right 50%; offset-path: circle(50%); offset-distance: var(--p); */ /* etc. */ } .circle:hover .square{ --p: 100%; } CodePen Embed FallbackA registered CSS custom property (--p) is used to set and animate the offset distance of the square element. The animation is possible because an element can be positioned at any point in a given path using offset. and maybe you didn’t know this, but offset is a shorthand property comprised of the following constituent properties:
- offset-position: The path’s starting point
- offset-path: The shape along which the element can be moved
- offset-distance: A distance along the path on which the element is moved
- offset-rotate: The rotation angle of an element relative to its anchor point and offset path
- offset-anchor: A position within the element that’s aligned to the path
The offset-path property is the one that’s important to what we’re trying to achieve. It accepts a shape value — including SVG shapes or CSS shape functions — as well as reference boxes of the containing element to create the path.
Reference boxes? Those are an element’s dimensions according to the CSS Box Model, including content-box, padding-box, border-box, as well as SVG contexts, such as the view-box, fill-box, and stroke-box. These simplify how we position elements along the edges of their containing elements. Here’s an example: all the small squares below are placed in the default top-left corner of their containing elements’ content-box. In contrast, the small circles are positioned along the top-right corner (25% into their containing elements’ square perimeter) of the content-box, border-box, and padding-box, respectively.
<div class="big"> <div class="small circle"></div> <div class="small square"></div> <p>She sells sea shells by the seashore</p> </div> <div class="big"> <div class="small circle"></div> <div class="small square"></div> <p>She sells sea shells by the seashore</p> </div> <div class="big"> <div class="small circle"></div> <div class="small square"></div> <p>She sells sea shells by the seashore</p> </div> .small { /* etc. */ position: absolute; &.square { offset: content-box; border-radius: 4px; } &.circle { border-radius: 50%; } } .big { /* etc. */ contain: layout; /* (or position: relative) */ &:nth-of-type(1) { .circle { offset: content-box 25%; } } &:nth-of-type(2) { border: 20px solid rgb(170 232 251); .circle { offset: border-box 25%; } } &:nth-of-type(3) { padding: 20px; .circle { offset: padding-box 25%; } } } CodePen Embed FallbackNote: You can separate the element’s offset-positioned layout context if you don’t want to allocated space for it inside its containing parent element. That’s how I’ve approached it in the example above so that the paragraph text inside can sit flush against the edges. As a result, the offset positioned elements (small squares and circles) are given their own contexts using position: absolute, which removes them from the normal document flow.
This method, positioning relative to reference boxes, makes it easy to place elements like notification dots and ornamental ribbon tips along the periphery of some UI module. It further simplifies the placement of texts along a containing block’s edges, as offset can also rotate elements along the path, thanks to offset-rotate. A simple example shows the date of an article placed at a block’s right edge:
<article> <h1>The Irreplaceable Value of Human Decision-Making in the Age of AI</h1> <!-- paragraphs --> <div class="date">Published on 11<sup>th</sup> Dec</div> <cite>An excerpt from the HBR article</cite> </article> article { container-type: inline-size; /* etc. */ } .date { offset: padding-box 100cqw 90deg / left 0 bottom -10px; /* Equivalent to: offset-path: padding-box; offset-distance: 100cqw; (100% of the container element's width) offset-rotate: 90deg; offset-anchor: left 0 bottom -10px; */ } CodePen Embed FallbackAs we just saw, using the offset property with a reference box path and container units is even more efficient — you can easily set the offset distance based on the containing element’s width or height. I’ll include a reference for learning more about container queries and container query units in the “Further Reading” section at the end of this article.
There’s also the offset-anchor property that’s used in that last example. It provides the anchor for the element’s displacement and rotation — for instance, the 90 degree rotation in the example happens from the element’s bottom-left corner. The offset-anchor property can also be used to move the element either inward or outward from the reference box by adjusting inset-* values — for instance, the bottom -10px arguments pull the element’s bottom edge outwards from its containing element’s padding-box. This enhances the precision of placements, also demonstrated below.
<figure> <div class="big">4</div> <div class="small">number four</div> </figure> .small { width: max-content; offset: content-box 90% -54deg / center -3rem; /* Equivalent to: offset-path: content-box; offset-distance: 90%; offset-rotate: -54deg; offset-anchor: center -3rem; */ font-size: 1.5rem; color: navy; } CodePen Embed FallbackAs shown at the beginning of the article, offset positioning is animateable, which allows for dynamic design effects, like this:
<article> <figure> <div class="small one">17<sup>th</sup> Jan. 2025</div> <span class="big">Seminar<br>on<br>Literature</span> <div class="small two">Tickets Available</div> </figure> </article> @property --d { syntax: "<percentage>"; inherits: false; initial-value: 0%; } .small { /* other style rules */ offset: content-box var(--d) 0deg / left center; /* Equivalent to: offset-path: content-box; offset-distance: var(--d); offset-rotate: 0deg; offset-anchor: left center; */ transition: --d .2s linear; &.one { --d: 2%; } &.two { --d: 70%; } } article:hover figure { .one { --d: 15%; } .two { --d: 80%; } } CodePen Embed Fallback Wrapping upWhether for graphic designs like text along borders, textual annotations, or even dynamic texts like error messaging, CSS offset is an easy-to-use option to achieve all of that. We can position the elements along the reference boxes of their containing parent elements, rotate them, and even add animation if needed.
Further reading- The CSS offset-path property: CSS-Tricks, MDN
- The CSS offset-anchor property: CSS-Tricks, MDN
- Container query length units: CSS-Tricks, MDN
- The @property at-rule: CSS-Tricks, web.dev
- The CSS Box Model: CSS-Tricks
- SVG Reference Boxes: W3C
Positioning Text Around Elements With CSS Offset originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Creating a “Starred” Feed
Chris wrote about “Likes” pages a long while back. The idea is rather simple: “Like” an item in your RSS reader and display it in a feed of other liked items. The little example Chris made is still really good.
CodePen Embed FallbackThere were two things Chris noted at the time. One was that he used a public CORS proxy that he wouldn’t use in a production environment. Good idea to nix that, security and all. The other was that he’d consider using WordPress transients to fetch and cache the data to work around CORS.
I decided to do that! The result is this WordPress block I can drop right in here. I’ll plop it in a <details> to keep things brief.
Open Starred Feed Link on 1/16/2025Don’t Wrap Figure in a Linkadrianroselli.comIn my post Brief Note on Figure and Figcaption Support I demonstrate how, when encountering a figure with a screen reader, you won’t hear everything announced at once:
No screen reader combo treats the caption as the accessible name nor accessible description, not even for an…Link on 1/15/2025Learning HTML is the best investment I ever didchristianheilmann.comOne of the running jokes and/or discussion I am sick and tired of is people belittling HTML. Yes, HTML is not a programming language. No, HTML should not just be a compilation target. Learning HTML is a solid investment and not hard to do.
I am not…Link on 1/14/2025Open Props UInerdy.dev
Presenting Open Props UI!…Link on 1/12/2025Gotchas in Naming CSS View Transitionsblog.jim-nielsen.com
I’m playing with making cross-document view transitions work on this blog.
Nothing fancy. Mostly copying how Dave Rupert does it on his site where you get a cross-fade animation on the whole page generally, and a little position animation on the page title specifically.
Link on 1/6/2025The :empty pseudo-classhtml-css-tip-of-the-week.netlify.app
We can use the :empty pseudo-class as a way to style elements on your webpage that are empty.
You might wonder why you’d want to style something that’s empty. Let’s say you’re creating a todo list.
You want to put your todo items in a list, but what about when you don’t…Link on 1/8/2025CSS Wish List 2025meyerweb.com
Back in 2023, I belatedly jumped on the bandwagon of people posting their CSS wish lists for the coming year. This year I’m doing all that again, less belatedly! (I didn’t do it last year because I couldn’t even. Get it?)
I started this post by looking at what I…Link on 1/9/2025aria-description Does Not Translateadrianroselli.com
It does, actually. In Firefox. Sometimes.
A major risk of using ARIA to define text content is it typically gets overlooked in translation. Automated translation services often do not capture it. Those who pay for localization services frequently miss content in ARIA attributes when sending text strings to localization vendors.
Content buried…Link on 1/8/2025Let’s Standardize Async CSS!scottjehl.com
6 years back I posted the Simplest Way to Load CSS Asynchronously to document a hack we’d been using for at least 6 years prior to that. The use case for this hack is to load CSS files asynchronously, something that HTML itself still does not support, even though…Link on 1/9/2025Tight Mode: Why Browsers Produce Different Performance Resultssmashingmagazine.com
This article is a sponsored by DebugBear
I was chatting with DebugBear’s Matt Zeunert and, in the process, he casually mentioned this thing called Tight Mode when describing how browsers fetch and prioritize resources. I wanted to nod along like I knew what he was talking about…Link on 12/19/2024Why I’m excited about text-box-trim as a designerpiccalil.li
I’ve been excited by the potential of text-box-trim, text-edge and text-box for a while. They’re in draft status at the moment, but when more browser support is available, this capability will open up some exciting possibilities for improving typesetting in the browser, as well as giving us more…
It’s a little different. For one, I’m only fetching 10 items at a time. We could push that to infinity but that comes with a performance tax, not to mention I have no way of organizing the items for them to be grouped and filtered. Maybe that’ll be a future enhancement!
The Chris demo provided the bones and it does most of the heavy lifting. The “tough” parts were square-pegging the thing into a WordPress block architecture and then getting transients going. This is my first time working with transients, so I thought I’d share the relevant code and pick it apart.
function fetch_and_store_data() { $transient_key = 'fetched_data'; $cached_data = get_transient($transient_key); if ($cached_data) { return new WP_REST_Response($cached_data, 200); } $response = wp_remote_get('https://feedbin.com/starred/a22c4101980b055d688e90512b083e8d.xml'); if (is_wp_error($response)) { return new WP_REST_Response('Error fetching data', 500); } $body = wp_remote_retrieve_body($response); $data = simplexml_load_string($body, 'SimpleXMLElement', LIBXML_NOCDATA); $json_data = json_encode($data); $array_data = json_decode($json_data, true); $items = []; foreach ($array_data['channel']['item'] as $item) { $items[] = [ 'title' => $item['title'], 'link' => $item['link'], 'pubDate' => $item['pubDate'], 'description' => $item['description'], ]; } set_transient($transient_key, $items, 12 * HOUR_IN_SECONDS); return new WP_REST_Response($items, 200); } add_action('rest_api_init', function () { register_rest_route('custom/v1', '/fetch-data', [ 'methods' => 'GET', 'callback' => 'fetch_and_store_data', ]); });Could this be refactored and written more efficiently? All signs point to yes. But here’s how I grokked it:
function fetch_and_store_data() { }The function’s name can be anything. Naming is hard. The first two variables:
$transient_key = 'fetched_data'; $cached_data = get_transient($transient_key);The $transient_key is simply a name that identifies the transient when we set it and get it. In fact, the $cached_data is the getter so that part’s done. Check!
I only want the $cached_data if it exists, so there’s a check for that:
if ($cached_data) { return new WP_REST_Response($cached_data, 200); }This also establishes a new response from the WordPress REST API, which is where the data is cached. Rather than pull the data directly from Feedbin, I’m pulling it and caching it in the REST API. This way, CORS is no longer an issue being that the starred items are now locally stored on my own domain. That’s where the wp_remote_get() function comes in to form that response from Feedbin as the origin:
$response = wp_remote_get('https://feedbin.com/starred/a22c4101980b055d688e90512b083e8d.xml');Similarly, I decided to throw an error if there’s no $response. That means there’s no freshly $cached_data and that’s something I want to know right away.
if (is_wp_error($response)) { return new WP_REST_Response('Error fetching data', 500); }The bulk of the work is merely parsing the XML data I get back from Feedbin to JSON. This scours the XML and loops through each item to get its title, link, publish date, and description:
$body = wp_remote_retrieve_body($response); $data = simplexml_load_string($body, 'SimpleXMLElement', LIBXML_NOCDATA); $json_data = json_encode($data); $array_data = json_decode($json_data, true); $items = []; foreach ($array_data['channel']['item'] as $item) { $items[] = [ 'title' => $item['title'], 'link' => $item['link'], 'pubDate' => $item['pubDate'], 'description' => $item['description'], ]; }“Description” is a loaded term. It could be the full body of a post or an excerpt — we don’t know until we get it! So, I’m splicing and trimming it in the block’s Edit component to stub it at no more than 50 words. There’s a little risk there because I’m rendering the HTML I get back from the API. Security, yes. But there’s also the chance I render an open tag without its closing counterpart, muffing up my layout. I know there are libraries to address that but I’m keeping things simple for now.
Now it’s time to set the transient once things have been fetched and parsed:
set_transient($transient_key, $items, 12 * HOUR_IN_SECONDS);The WordPress docs are great at explaining the set_transient() function. It takes three arguments, the first being the $transient_key that was named earlier to identify which transient is getting set. The other two:
- $value: This is the object we’re storing in the named transient. That’s the $items object handling all the parsing.
- $expiration: How long should this transient last? It wouldn’t be transient if it lingered around forever, so we set an amount of time expressed in seconds. Mine lingers for 12 hours before it expires and then updates the next time a visitor hits the page.
OK, time to return the items from the REST API as a new response:
return new WP_REST_Response($items, 200);That’s it! Well, at least for setting and getting the transient. The next thing I realized I needed was a custom REST API endpoint to call the data. I really had to lean on the WordPress docs to get this going:
add_action('rest_api_init', function () { register_rest_route('custom/v1', '/fetch-data', [ 'methods' => 'GET', 'callback' => 'fetch_and_store_data', ]); });That’s where I struggled most and felt like this all took wayyyyy too much time. Well, that and sparring with the block itself. I find it super hard to get the front and back end components to sync up and, honestly, a lot of that code looks super redundant if you were to scope it out. That’s another story altogether.
Enjoy reading what we’re reading! I put a page together that pulls in the 10 most recent items with a link to subscribe to the full feed.
Creating a “Starred” Feed originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Fancy Menu Navigation Using Anchor Positioning
You have for sure heard about the new CSS Anchor Positioning, right? It’s a feature that allows you to link any element from the page to another one, i.e., the anchor. It’s useful for all the tooltip stuff, but it can also create a lot of other nice effects.
In this article, we will study menu navigation where I rely on anchor positioning to create a nice hover effect on links.
CodePen Embed FallbackCool, right? We have a sliding effect where the blue rectangle adjusts to fit perfectly with the text content over a nice transition. If you are new to anchor positioning, this example is perfect for you because it’s simple and allows you to discover the basics of this new feature. We will also study another example so stay until the end!
Note that only Chromium-based browsers fully support anchor positioning at the time I’m writing this. You’ll want to view the demos in a browser like Chrome or Edge until the feature is more widely supported in other browsers.
The initial configurationLet’s start with the HTML structure which is nothing but a nav element containing an unordered list of links:
<nav> <ul> <li><a href="#">Home</a></li> <li class="active"><a href="#">About</a></li> <li><a href="#">Projects</a></li> <li><a href="#">Blog</a></li> <li><a href="#">Contact</a></li> </ul> </nav>We will not spend too much time explaining this structure because it can be different if your use case is different. Simply ensure the semantic is relevant to what you are trying to do. As for the CSS part, we will start with some basic styling to create a horizontal menu navigation.
ul { padding: 0; margin: 0; list-style: none; display: flex; gap: .5rem; font-size: 2.2rem; } ul li a { color: #000; text-decoration: none; font-weight: 900; line-height: 1.5; padding-inline: .2em; display: block; }Nothing fancy so far. We remove some default styling and use Flexbox to align the elements horizontally.
CodePen Embed Fallback Sliding effectFirst off, let’s understand how the effect works. At first glance, it looks like we have one rectangle that shrinks to a small height, moves to the hovered element, and then grows to full height. That’s the visual effect, but in reality, more than one element is involved!
Here is the first demo where I am using different colors to better see what is happening.
CodePen Embed FallbackEach menu item has its own “element” that shrinks or grows. Then we have a common “element” (the one in red) that slides between the different menu items. The first effect is done using a background animation and the second one is where anchor positioning comes into play!
The background animationWe will animate the height of a CSS gradient for this first part:
/* 1 */ ul li { background: conic-gradient(lightblue 0 0) bottom/100% 0% no-repeat; transition: .2s; } /* 2 */ ul li:is(:hover,.active) { background-size: 100% 100%; transition: .2s .2s; } /* 3 */ ul:has(li:hover) li.active:not(:hover) { background-size: 100% 0%; transition: .2s; }We’ve defined a gradient with a 100% width and 0% height, placed at the bottom. The gradient syntax may look strange, but it’s the shortest one that allows me to have a single-color gradient.
Related: “How to correctly define a one-color gradient”
Then, if the menu item is hovered or has the .active class, we make the height equal to 100%. Note the use of the delay here to make sure the growing happens after the shrinking.
Finally, we need to handle a special case with the .active item. If we hover any item (that is not the active one), then the .active item gets the shirking effect (the gradient height is equal to 0%). That’s the purpose of the third selector in the code.
CodePen Embed FallbackOur first animation is done! Notice how the growing begins after the shrinking completes because of the delay we defined in the second selector.
The anchor positioning animationThe first animation was quite easy because each item had its own background animation, meaning we didn’t have to care about the text content since the background automatically fills the whole space.
We will use one element for the second animation that slides between all the menu items while adapting its width to fit the text of each item. This is where anchor positioning can help us.
Let’s start with the following code:
ul:before { content:""; position: absolute; position-anchor: --li; background: red; transition: .2s; } ul li:is(:hover, .active) { anchor-name: --li; } ul:has(li:hover) li.active:not(:hover) { anchor-name: none; }To avoid adding an extra element, I will prefer using a pseudo-element on the ul. It should be absolutely-positioned and we will rely on two properties to activate the anchor positioning.
We define the anchor with the anchor-name property. When a menu item is hovered or has the .active class, it becomes the anchor element. We also have to remove the anchor from the .active item if another item is in a hovered state (hence, the last selector in the code). In other words, only one anchor is defined at a time.
Then we use the position-anchor property to link the pseudo-element to the anchor. Notice how both use the same notation --li. It’s similar to how, for example, we define @keyframes with a specific name and later use it inside an animation property. Keep in mind that you have to use the <dashed-indent> syntax, meaning the name must always start with two dashes (--).
CodePen Embed FallbackThe pseudo-element is correctly placed but nothing is visible because we didn’t define any dimension! Let’s add the following code:
ul:before { bottom: anchor(bottom); left: anchor(left); right: anchor(right); height: .2em; }The height property is trivial but the anchor() is a newcomer. Here’s how Juan Diego describes it in the Almanac:
The CSS anchor() function takes an anchor element’s side and resolves to the <length> where it is positioned. It can only be used in inset properties (e.g. top, bottom, bottom, left, right, etc.), normally to place an absolute-positioned element relative to an anchor.
Let’s check the MDN page as well:
The anchor() CSS function can be used within an anchor-positioned element’s inset property values, returning a length value relative to the position of the edges of its associated anchor element.
Usually, we use left: 0 to place an absolute element at the left edge of its containing block (i.e., the nearest ancestor having position: relative). The left: anchor(left) will do the same but instead of the containing block, it will consider the associated anchor element.
That’s all — we are done! Hover the menu items in the below demo and see how the pseudo-element slides between them.
CodePen Embed FallbackEach time you hover over a menu item it becomes the new anchor for the pseudo-element (the ul:before). This also means that the anchor(...) values will change creating the sliding effect! Let’s not forget the use of the transition which is important otherwise, we will have an abrupt change.
We can also write the code differently like this:
ul:before { content:""; position: absolute; inset: auto anchor(right, --li) anchor(bottom, --li) anchor(left, --li); height: .2em; background: red; transition: .2s; }In other words, we can rely on the inset shorthand instead of using physical properties like left, right, and bottom, and instead of defining position-anchor, we can include the anchor’s name inside the anchor() function. We are repeating the same name three times which is probably not optimal here but in some situations, you may want your element to consider multiple anchors, and in such cases, this syntax will make sense.
Combining both effectsNow, we combine both effects and, tada, the illusion is perfect!
CodePen Embed FallbackPay attention to the transition values where the delay is important:
ul:before { transition: .2s .2s; } ul li { transition: .2s; } ul li:is(:hover,.active) { transition: .2s .4s; } ul:has(li:hover) li.active:not(:hover) { transition: .2s; }We have a sequence of three animations — shrink the height of the gradient, slide the pseudo-element, and grow the height of the gradient — so we need to have delays between them to pull everything together. That’s why for the sliding of the pseudo-element we have a delay equal to the duration of one animation (transition: .2 .2s) and for the growing part the delay is equal to twice the duration (transition: .2s .4s).
Bouncy effect? Why not?!Let’s try another fancy animation in which the highlight rectangle morphs into a small circle, jumps to the next item, and transforms back into a rectangle again!
CodePen Embed FallbackI won’t explain too much for this example as it’s your homework to dissect the code! I’ll offer a few hints so you can unpack what’s happening.
Like the previous effect, we have a combination of two animations. For the first one, I will use the pseudo-element of each menu item where I will adjust the dimension and the border-radius to simulate the morphing. For the second animation, I will use the ul pseudo-element to create a small circle that I move between the menu items.
Here is another version of the demo with different coloration and a slower transition to better visualize each animation:
CodePen Embed FallbackThe tricky part is the jumping effect where I am using a strange cubic-bezier() but I have a detailed article where I explain the technique in my CSS-Tricks article “Advanced CSS Animation Using cubic-bezier()”.
ConclusionI hope you enjoyed this little experimentation using the anchor positioning feature. We only looked at three properties/values but it’s enough to prepare you for this new feature. The anchor-name and position-anchor properties are the mandatory pieces for linking one element (often called a “target” element in this context) to another element (what we call an “anchor” element in this context). From there, you have the anchor() function to control the position.
Related: CSS Anchor Positioning Guide
Fancy Menu Navigation Using Anchor Positioning originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Web-Slinger.css: Across the Swiper-Verse
My previous article warned that horizontal motion on Tinder has irreversible consequences. I’ll save venting on that topic for a different blog, but at first glance, swipe-based navigation seems like it could be a job for Web-Slinger.css, your friendly neighborhood experimental pure CSS Wow.js replacement for one-way scroll-triggered animations. I haven’t managed to fit that description into a theme song yet, but I’m working on it.
In the meantime, can Web-Slinger.css swing a pure CSS Tinder-style swiping interaction to indicate liking or disliking an element? More importantly, will this experiment give me an excuse to use an image of Spider Pig, in response to popular demand in the bustling comments section of my previous article? Behold the Spider Pig swiper, which I propose as a replacement for captchas because every human with a pulse loves Spider Pig. With that unbiased statement in mind, swipe left or right below (only Chrome and Edge for now) to reveal a counter showing how many people share your stance on Spider Pig.
CodePen Embed Fallback Broaden your horizonsThe crackpot who invented Web-Slinger.css seems not to have considered horizontal scrolling, but we can patch that maniac’s monstrous creation like so:
[class^="scroll-trigger-"] { view-timeline-axis: x; }This overrides the default behavior for marker elements with class names using the Web-Slinger convention of scroll-trigger-n, which activates one-way, scroll-triggered animations. By setting the timeline axis to x, the scroll triggers only run when they are revealed by scrolling horizontally rather than vertically (which is the default). Otherwise, the triggers would run straightaway because although they are out of view due to the container’s width, they will all be above the fold vertically when we implement our swiper.
My steps in laying the foundation for the above demo were to fork this awesome JavaScript demo of Tinder-style swiping by Nikolay Talanov, strip out the JavaScript and all the cards except for one, then import Web-Slinger.css and introduce the horizontal patch explained above. Next, I changed the card’s container to position: fixed, and introduced three scroll-snapping boxes side-by-side, each the height and width of the viewport. I set the middle slide to scroll-align: center so that the user starts in the middle of the page and has the option to scroll backwards or forwards.
Sidenote: When unconventionally using scroll-driven animations like this, a good mindset is that the scrollable element needn’t be responsible for conventionally scrolling anything visible on the page. This approach is reminiscent of how the first thing you do when using checkbox hacks is hide the checkbox and make the label look like something else. We leverage the CSS-driven behaviors of a scrollable element, but we don’t need the default UI behavior.
I put a div marked with scroll-trigger-1 on the third slide and used it to activate a rejection animation on the card like this:
<div class="demo__card on-scroll-trigger-1 reject"> <!-- HTML for the card --> </div> <main> <div class="slide"> </div> <div id="middle" class="slide"> </div> <div class="slide"> <div class="scroll-trigger-1"></div> </div> </main>It worked the way I expected! I knew this would be easy! (Narrator: it isn’t, you’ll see why next.)
<div class="on-scroll-trigger-2 accept"> <div class="demo__card on-scroll-trigger-2 reject"> <!-- HTML for the card --> </div> </div> <main> <div class="slide"> <div class="scroll-trigger-2"></div> </div> <div id="middle" class="slide"> </div> <div class="slide"> <div class="scroll-trigger-1"></div> </div> </main>After adding this, Spider Pig is automatically ”liked” when the page loads. That would be appropriate for a card that shows a person like myself who everybody automatically likes — after all, a middle-aged guy who spends his days and nights hacking CSS is quite a catch. By contrast, it is possible Spider Pig isn’t everyone’s cup of tea. So, let’s understand why the swipe right implementation would behave differently than the swipe left implementation when we thought we applied the same principles to both implementations.
Take a step backThis bug drove home to me what view-timeline does and doesn’t do. The lunatic creator of Web-Slinger.css relied on tech that wasn’t made for animations which run only when the user scrolls backwards.
This visualizer shows that no matter what options you choose for animation-range, the subject wants to complete its animation after it has crossed the viewport in the scrolling direction — which is exactly what we do not want to happen in this particular case.
Fortunately, our friendly neighborhood Bramus from the Chrome Developer Team has a cool demo showing how to detect scroll direction in CSS. Using the clever --scroll-direction CSS custom property Bramus made, we can ensure Spider Pig animates at the right time rather than on load. The trick is to control the appearance of .scroll-trigger-2 using a style query like this:
:root { animation: adjust-slide-index 3s steps(3, end), adjust-pos 1s; animation-timeline: scroll(root x); } @property --slide-index { syntax: "<number>"; inherits: true; initial-value: 0; } @keyframes adjust-slide-index { to { --slide-index: 3; } } .scroll-trigger-2 { display: none; } @container style(--scroll-direction: -1) and style(--slide-index: 0) { .scroll-trigger-2 { display: block; } }That style query means that the marker with the .scroll-trigger-2 class will not be rendered until we are on the previous slide and reach it by scrolling backward. Notice that we also introduced another variable named --slide-index, which is controlled by a three-second scroll-driven animation with three steps. It counts the slide we are on, and it is used because we want the user to swipe decisively to activate the dislike animation. We don’t want just any slight breeze to trigger a dislike.
When the swipe has been concluded, one more like (I’m superhuman)As mentioned at the outset, measuring how many CSS-Tricks readers dislike Spider Pig versus how many have a soul is important. To capture this crucial stat, I’m using a third-party counter image as a background for the card underneath the Spider Pig card. It is third-party, but hopefully, it will always work because the website looks like it has survived since the dawn of the internet. I shouldn’t complain because the price is right. I chose the least 1990s-looking counter and used it like this:
@container style(--scroll-trigger-1: 1) { .result { background-image: url('https://counter6.optistats.ovh/private/freecounterstat.php?c=qbgw71kxx1stgsf5shmwrb2aflk5wecz'); background-repeat: no-repeat; background-attachment: fixed; background-position: center; } .counter-description::after { content: 'who like spider pig'; } .scroll-trigger-2 { display: none; } } @container style(--scroll-trigger-2: 1) { .result { background-image: url('https://counter6.optistats.ovh/private/freecounterstat.php?c=abtwsn99snah6wq42nhnsmbp6pxbrwtj'); background-repeat: no-repeat; background-attachment: fixed; background-position: center; } .counter-description::after { content: 'who dislike spider pig'; } .scroll-trigger-1 { display: none; } } Scrolls of wisdom: Lessons learnedThis hack turned out more complex than I expected, mostly because of the complexity of using scroll-triggered animations that only run when you meet an element by scrolling backward which goes against assumptions made by the current API. That’s a good thing to know and understand. Still, it’s amazing how much power is hidden in the current spec. We can style things based on extremely specific scrolling behaviors if we believe in ourselves. The current API had to be hacked to unlock that power, but I wish we could do something like:
[class^="scroll-trigger-"] { view-timeline-axis: x; view-timeline-direction: backwards; /* <-- this is speculative. do not use! */ }With an API like that allowing the swipe-right scroll trigger to behave the way I originally imagined, the Spider Pig swiper would not require hacking.
I dream of wider browser support for scroll-driven animations. But I hope to see the spec evolve to give us more flexibility to encourage designers to build nonlinear storytelling into the experiences they create. If not, once animation timelines land in more browsers, it might be time to make Web-Slinger.css more complete and production-ready, to make the more advanced scrolling use cases accessible to the average CSS user.
Web-Slinger.css: Across the Swiper-Verse originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
A Few Ways That Cloudways Makes Running This Site a Little Easier
(This is a sponsored post.)
It’s probably no surprise to you that CSS-Tricks is (proudly) hosted on Cloudways, DigitalOcean’s managed hosting arm. Given both CSS-Tricks and Cloudways are part of DigitalOcean, it was just a matter of time before we’d come together this way. And here we are!
We were previously hosted on Flywheel which was a fairly boutique WordPress hosting provider until WP Engine purchased it years back. And, to be very honest and up-front, Flywheel served us extremely well. There reached a point when it became pretty clear that CSS-Tricks was simply too big for Flywheel to scale along. That might’ve led us to try out WP Engine in the absence of Cloudways… but it’s probably good that never came to fruition considering recent events.
Anyway, moving hosts always means at least a smidge of contest-switching. Different server names with different configurations with different user accounts with different controls.
We’re a pretty low-maintenance operation around here, so being on a fully managed host is a benefit because I see very little of the day-to-day nuance that happens on our server. The Cloudways team took care of all the heavy lifting of migrating us and making sure we were set up with everything we needed, from SFTP accounts and database access to a staging environment and deployment points.
Our development flow used to go something like this:
- Fire up Local (Flywheel’s local development app)
- Futz around with local development
- Push to main
- Let a CI/CD pipeline publish the changes
I know, ridiculously simple. But it was also riddled with errors because we didn’t always want to publish changes on push. There was a real human margin of error in there, especially when handling WordPress updates. We could have (and should have) had some sort of staging environment rather than blindly trusting what was working locally. But again, we’re kinduva a ragtag team despite the big corporate backing.
The flow now looks like this:
- Fire up Local (we still use it!)
- Futz around with local development
- Push to main
- Publish to staging
- Publish to production
This is something we could have set up in Flywheel but was trivial with Cloudways. I gave up some automation for quality assurance’s sake. Switching environments in Cloudways is a single click and I like a little manual friction to feel like I have some control in the process. That might not scale well for large teams on an enterprise project, but that’s not really what Cloudways is all about — that’s why we have DigitalOcean!
See that baseline-status-widget branch in the dropdown? That’s a little feature I’m playing with (and will post about later). I like that GitHub is integrated directly into the Cloudways UI so I can experiment with it in whatever environment I want, even before merging it with either the staging or master branches. It makes testing a whole lot easier and way less error-prone than triggering auto-deployments in every which way.
Here’s another nicety: I get a good snapshot of the differences between my environments through Cloudways monitoring. For example, I was attempting to update our copy of the Gravity Forms plugin just this morning. It worked locally but triggered a fatal in staging. I went in and tried to sniff out what was up with the staging environment, so I headed to the Vulnerability Scanner and saw that staging was running an older version of WordPress compared to what was running locally and in production. (We don’t version control WordPress core, so that was an easy miss.)
I hypothesized that the newer version of Gravity Forms had a conflict with the older version of WordPress, and this made it ridiculously easy to test my assertion. Turns out that was correct and I was confident that pushing to production was safe and sound — which it was.
That little incident inspired me to share a little about what I’ve liked about Cloudways so far. You’ll notice that we don’t push our products too hard around here. Anytime you experience something delightful — whatever it is — is a good time to blog about it and this was clearly one of those times.
I’d be remiss if I didn’t mention that Cloudways is ideal for any size or type of WordPress site. It’s one of the few hosts that will let you BOYO cloud, so to speak, where you can hold your work on a cloud server (like a DigitalOcean droplet, for instance) and let Cloudways manage the hosting, giving you all the freedom to scale when needed on top of the benefits of having a managed host. So, if you need a fully managed, autoscaling hosting solution for WordPress like we do here at CSS-Tricks, Cloudways has you covered.
A Few Ways That Cloudways Makes Running This Site a Little Easier originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
How to Wait for the sibling-count() and sibling-index() Functions
New features don’t just pop up in CSS (but I wish they did). Rather, they go through an extensive process of discussions and considerations, defining, writing, prototyping, testing, shipping handling support, and many more verbs that I can’t even begin to imagine. That process is long, and despite how much I want to get my hands on a new feature, as an everyday developer, I can only wait.
I can, however, control how I wait: do I avoid all possible interfaces or demos that are possible with that one feature? Or do I push the boundaries of CSS and try to do them anyway?
As ambitious and curious developers, many of us choose the latter option. CSS would grow stagnant without that mentality. That’s why, today, I want to look at two upcoming functions: sibling-count() and sibling-index(). We’re waiting for them — and have been for several years — so I’m letting my natural curiosity get the best of me so I can get a feel for what to be excited about. Join me!
The tree-counting functionsAt some point, you’ve probably wanted to know the position of an element amongst its siblings or how many children an element has to calculate something in CSS, maybe for some staggering animation in which each element has a longer delay, or perhaps for changing an element’s background-color depending on its number of siblings. This has been a long-awaited deal on my CSS wishlists. Take this CSSWG GitHub Issue from 2017:
Feature request. It would be nice to be able to use the counter() function inside of calc() function. That would enable new possibilities on layouts.
However, counters work using strings, rendering them useless inside a calc() function that deals with numbers. We need a set of similar functions that return as integers the index of an element and the count of siblings. This doesn’t seem too much to ask. We can currently query an element by its tree position using the :nth-child() pseudo-selector (and its variants), not to mention query an element based on how many items it has using the :has() pseudo-selector.
Luckily, this year the CSSWG approved implementing the sibling-count() and sibling-index() functions! And we already have something in the spec written down:
The sibling-count() functional notation represents, as an <integer>, the total number of child elements in the parent of the element on which the notation is used.
The sibling-index() functional notation represents, as an <integer>, the index of the element on which the notation is used among the children of its parent. Like :nth-child(), sibling-index() is 1-indexed.
How much time do we have to wait to use them? Earlier this year Adam Argyle said that “a Chromium engineer mentioned wanting to do it, but we don’t have a flag to try it out with yet. I’ll share when we do!” So, while I am hopeful to get more news in 2025, we probably won’t see them shipped soon. In the meantime, let’s get to what we can do right now!
Rubbing two sticks togetherThe closest we can get to tree counting functions in terms of syntax and usage is with custom properties. However, the biggest problem is populating them with the correct index and count. The simplest and longest method is hardcoding each using only CSS: we can use the nth-child() selector to give each element its corresponding index:
li:nth-child(1) { --sibling-index: 1; } li:nth-child(2) { --sibling-index: 2; } li:nth-child(3) { --sibling-index: 3; } /* and so on... */Setting the sibling-count() equivalent has a bit more nuance since we will need to use quantity queries with the :has() selector. A quantity query has the following syntax:
.container:has(> :last-child:nth-child(m)) { }…where m is the number of elements we want to target. It works by checking if the last element of a container is also the nth element we are targeting; thus it has only that number of elements. You can create your custom quantity queries using this tool by Temani Afif. In this case, our quantity queries would look like the following:
ol:has(> :nth-child(1)) { --sibling-count: 1; } ol:has(> :last-child:nth-child(2)) { --sibling-count: 2; } ol:has(> :last-child:nth-child(3)) { --sibling-count: 3; } /* and so on... */This example is intentionally light on the number of elements for brevity, but as the list grows it will become unmanageable. Maybe we could use a preprocessor like Sass to write them for us, but we want to focus on a vanilla CSS solution here. For example, the following demo can support up to 12 elements, and you can already see how ugly it gets in the code.
CodePen Embed FallbackThat’s 24 rules to know the index and count of 12 elements for those of you keeping score. It surely feels like we could get that number down to something more manageable, but if we hardcode each index we are bound increase the amount of code we write. The best we can do is rewrite our CSS so we can nest the --sibling-index and --sibling-count properties together. Instead of writing each property by itself:
li:nth-child(2) { --sibling-index: 2; } ol:has(> :last-child:nth-child(2)) { --sibling-count: 2; }We could instead nest the --sibling-count rule inside the --sibling-index rule.
li:nth-child(2) { --sibling-index: 2; ol:has(> &:last-child) { --sibling-count: 2; } }While it may seem wacky to nest a parent inside its children, the following CSS code is completely valid; we are selecting the second li element, and inside, we are selecting an ol element if its second li element is also the last, so the list only has two elements. Which syntax is easier to manage? It’s up to you.
CodePen Embed FallbackBut that’s just a slight improvement. If we had, say, 100 elements we would still need to hardcode the --sibling-index and --sibling-count properties 100 times. Luckily, the following method will increase rules in a logarithmic way, specifically base-2. So instead of writing 100 rules for 100 elements, we will be writing closer to 10 rules for around 100 elements.
Flint and steelThis method was first described by Roman Komarov in October last year, in which he prototypes both tree counting functions and the future random() function. It’s an amazing post, so I strongly encourage you to read it.
This method also uses custom properties, but instead of hardcoding each one, we will be using two custom properties that will build up the --sibling-index property for each element. Just to be consistent with Roman’s post, we will call them --si1 and --si2, both starting at 0:
li { --si1: 0; --si2: 0; }The real --sibling-index will be constructed using both properties and a factor (F) that represents an integer greater or equal to 2 that tells us how many elements we can select according to the formula sqrt(F) - 1. So…
- For a factor of 2, we can select 3 elements.
- For a factor of 3, we can select 8 elements.
- For a factor of 5, we can select 24 elements.
- For a factor of 10, we can select 99 elements.
- For a factor of 25, we can select 624 elements.
As you can see, increasing the factor by one will give us exponential gains on how many elements we can select. But how does all this translate to CSS?
The first thing to know is that the formula for calculating the --sibling-index property is calc(F * var(--si2) + var(--si1)). If we take a factor of 3, it would look like the following:
li { --si1: 0; --si2: 0; /* factor of 3; it's a harcoded number */ --sibling-index: calc(3 * var(--si2) + var(--si1)); }The following selectors may be random but stay with me here. For the --si1 property, we will write rules selecting elements that are multiples of the factor and offset them by one 1 until we reach F - 1, then set --si1 to the offset. This translates to the following CSS:
li:nth-child(Fn + 1) { --si1: 1; } li:nth-child(Fn + 2) { --si1: 2; } /* ... */ li:nth-child(Fn+(F-1)) { --si1: (F-1) }So if our factor is 3, we will write the following rules until we reach F-1, so 2 rules:
li:nth-child(3n + 1) { --si1: 1; } li:nth-child(3n + 2) { --si1: 2; }For the --si2 property, we will write rules selecting elements in batches of the factor (so if our factor is 3, we will select 3 elements per rule), going from the last possible index (in this case 8) backward until we simply are unable to select more elements in batches. This is a little more convoluted to write in CSS:
li:nth-child(n + F*1):nth-child(-n + F*1-1){--si2: 1;} li:nth-child(n + F*2):nth-child(-n + F*2-1){--si2: 2;} /* ... */ li:nth-child(n+(F*(F-1))):nth-child(-n+(F*F-1)) { --si2: (F-1) }Again, if our factor is 3, we will write the following two rules:
li:nth-child(n + 3):nth-child(-n + 5) { --si2: 1; } li:nth-child(n + 6):nth-child(-n + 8) { --si2: 2; }And that’s it! By only setting those two values for --si1 and --si2 we can count up to 8 total elements. The math behind how it works seems wacky at first, but once you visually get it, it all clicks. I made this interactive demo in which you can see how all elements can be reached using this formula. Hover over the code snippets to see which elements can be selected, and click on each snippet to combine them into a possible index.
CodePen Embed FallbackIf you crank the elements and factor to the max, you can see that we can select 48 elements using only 14 snippets!
Wait, one thing is missing: the sibling-count() function. Luckily, we will be reusing all we have learned from prototyping --sibling-index. We will start with two custom properties: --sc1 and --sc1 at the container, both starting at 0 as well. The formula for calculating --sibling-count is the same.
ol { --sc1: 0; --sc2: 0; /* factor of 3; also a harcoded number */ --sibling-count: calc(3 * var(--sc2) + var(--sc1)); }Roman’s post also explains how to write selectors for the --sibling-count property by themselves, but we will use the :has() selection method from our first technique so we don’t have to write extra selectors. We can cram those --sc1 and --sc2 properties into the rules where we defined the sibling-index() properties:
/* --si1 and --sc1 */ li:nth-child(3n + 1) { --si1: 1; ol:has(> &:last-child) { --sc1: 1; } } li:nth-child(3n + 2) { --si1: 2; ol:has(> &:last-child) { --sc1: 2; } } /* --si2 and --sc2 */ li:nth-child(n + 3):nth-child(-n + 5) { --si2: 1; ol:has(> &:last-child) { --sc2: 1; } } li:nth-child(n + 6):nth-child(-n + 8) { --si2: 2; ol:has(> &:last-child) { --sc2: 2; } }This is using a factor of 3, so we can count up to eight elements with only four rules. The following example has a factor of 7, so we can count up to 48 elements with only 14 rules.
CodePen Embed FallbackThis method is great, but may not be the best fit for everyone due to the almost magical way of how it works, or simply because you don’t find it aesthetically pleasing. While for avid hands lighting a fire with flint and steel is a breeze, many won’t get their fire started.
Using a flamethrowerFor this method, we will use once again custom properties to mimic the tree counting functions, and what’s best, we will write less than 20 lines of code to count up to infinity—or I guess to 1.7976931348623157e+308, which is the double precision floating point limit!
We will be using the Mutation Observer API, so of course it takes JavaScript. I know that’s like admitting defeat for many, but I disagree. If the JavaScript method is simpler (which it is, by far, in this case), then it’s the most appropriate choice. Just as a side note, if performance is your main worry, stick to hard-coding each index in CSS or HTML.
First, we will grab our container from the DOM:
const elements = document.querySelector("ol");Then we’ll create a function that sets the --sibling-index property in each element and the --sibling-count in the container (it will be available to its children due to the cascade). For the --sibling-index, we have to loop through the elements.children, and we can get the --sibling-count from elements.children.length.
const updateCustomProperties = () => { let index = 1; for (element of elements.children) { element.style.setProperty("--sibling-index", index); index++; } elements.style.setProperty("--sibling-count", elements.children.length); };Once we have our function, remember to call it once so we have our initial tree counting properties:
updateCustomProperties();Lastly, the Mutation Observer. We need to initiate a new observer using the MutationObserver constructor. It takes a callback that gets invoked each time the elements change, so we write our updateCustomProperties function. With the resulting observer object, we can call its observe() method which takes two parameters:
- the element we want to observe, and
- a config object that defines what we want to observe through three boolean properties: attributes, childList, and subtree. In this case, we just want to check for changes in the child list, so we set that one to true:
That would be all we need! Using this method we can count many elements, in the following demo I set the max to 100, but it can easily reach tenfold:
CodePen Embed FallbackSo yeah, that’s our flamethrower right there. It definitely gets the fire started, but it’s plenty overkill for the vast majority of use cases. But that’s what we have while we wait for the perfect lighter.
What to do next?I don’t have a time machine, so I can’t say when the sibling-index() and sibling-count() functions may be released. However, the CSSWG has already written down something in the spec, and browsers (mainly Chromium) intention to ship things has been on fire lately, so I am confident we will be seeing both functions soon!
Luckily, once they are shipped and support is acceptable, all you have to do to change from one of these methods is set those custom properties to their functional homonyms. That is, of course, if you don’t want to go around your CSS changing each custom property:
ol{ --sibling-count: sibling-count(); } li{ --sibling-index: sibling-index(); } More information and tutorials- Possible Future CSS: Tree-Counting Functions and Random Values (Roman Komarov)
- View Transitions Staggering (Chris Coyier)
- Element Indexes (Chris Coyier)
- Enable the use of counter() inside calc() #1026
- Proposal: add sibling-count() and sibling-index() #4559
- Extend sibling-index() and sibling-count() with a selector argument #9572
- Proposal: children-count() function #11068
- Proposal: descendant-count() function #11069
How to Wait for the sibling-count() and sibling-index() Functions originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Tight Mode: Why Browsers Produce Different Performance Results
I wrote a post for Smashing Magazine that was published today about this thing that Chrome and Safari have called “Tight Mode” and how it impacts page performance. I’d never heard the term until DebugBear’s Matt Zeunert mentioned it in a passing conversation, but it’s a not-so-new deal and yet there’s precious little documentation about it anywhere.
So, Matt shared a couple of resources with me and I used those to put some notes together that wound up becoming the article that was published. In short:
Tight Mode discriminates resources, taking anything and everything marked as High and Medium priority. Everything else is constrained and left on the outside, looking in until the body is firmly attached to the document, signaling that blocking scripts have been executed. It’s at that point that resources marked with Low priority are allowed in the door during the second phase of loading.
The implications are huge, as it means resources are not treated equally at face value. And yet the way Chrome and Safari approach it is wildly different, meaning the implications are wildly different depending on which browser is being evaluated. Firefox doesn’t enforce it, so we’re effectively looking at three distinct flavors of how resources are fetched and rendered on the page.
It’s no wonder web performance is a hard discipline when we have these moving targets. Sure, it’s great that we now have a consistent set of metrics for evaluating, diagnosing, and discussing performance in the form of Core Web Vitals — but those metrics will never be consistent from browser to browser when the way resources are accessed and prioritized varies.
Tight Mode: Why Browsers Produce Different Performance Results originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
