YouTube has refreshed its iconic red with a new red-to-magenta gradient to celebrate its 20th anniversary, enhancing visual appeal and color consistency across devices. The subtle update improves accessibility, encourages user interaction, and reflects the platform’s ongoing evolution.
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 did
My 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 examples
Lea’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:
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 wrapper
One 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 grinding
Comparing 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 validation
Combine 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 Fallback
All 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:
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 up
It’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.
OnlyFans leverages a meticulously crafted UX to create a powerful sense of intimacy and exclusivity, captivating users while sparking ethical debates about manipulation and exploitation.
Bitly’s new preview page, which adds an extra click and includes ads, undermines the simplicity and efficiency that made the service popular. Users now face the choice of dealing with interruptions or paying for a premium plan, making Bitly less appealing as a URL shortening tool.
Google Maps in India is a great example of designing with real people in mind, tackling everything from landmark-based directions to language barriers and patchy internet. By listening to users and testing in the chaos of real-life streets, Google created a navigation tool that millions now rely on every day.
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 Fallback
This 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.
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 Fallback
This 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 Fallback
I 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 up
The 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:
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 all
Right 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.
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 Fallback
And just as a recap, our current hack looks like this:
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:
The 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:
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 Fallback
Unlike 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:
And now everything moves in the right direction while resizing the screen!
CodePen Embed Fallback
However, 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.
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.
Some Amazon app users are noticing a new design, with the navigation bar and the company logo now missing. Instead, a large search bar greet users, raising questions about whether this change is part of a broader redesign or a test for a future logo-less interface.
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 pattern
Let’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 , input[type="button"], and . 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.
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 layer
This 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 layers
Here 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:
So 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 , , or 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 .buttonis an 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 styles
I’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.
What 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:
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 styles
Along 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():
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 together
We can refactor any CSS property we need to modify into a CSS custom property, which gives us a lot of room for customization.
P.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.
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.”