Current CSS (2025)

Popovers And Dialogs

Popovers and dialogs are the new html hotness. Having content in a layer above everything else, without all the overhead of setting that up and maintaining z-indexes is amazing. They give a lot of interactivity that would have needed to be created in javascript and now just works with a couple html attributes.

But... one of their best and worst features is that they automatically apply a display:none; when going in and out. This means they don't affect anything when they're gone, which is optimal. But it also means that we have never had a good way of dealing with animating the concept of display. So the standards that be have been scrambling to finalize a concept called discrete transitioning which will allow us to transition between existence and nothingness. Truly we am become god.

It's just that as of the creation of this document, it's not all the way there yet. So here's the thing. The out animations will currently not work right in Firefox. But I grow less and less interested in worrying about that as time moves on.

So here we are. This document works in safari and chrome. But it has to use animations to transition in and out of a state of being. Ah well. We'll get there soon.

In case you don't know how these work, dialogs are a great semantic solution for modals, and popover is an API to allow anything to have the same functionality. For example, you can do some code like this.

<button popovertarget="pop">Click Me</button> <div popover id="pop">I'm poppin, ma</div>

Which would produce something like this.

I'm poppin, ma

Check out how simple that is. Does it require any JS? No. Does it have a little css? Yes, but if you've never looked this up before, you might be incredibly surprised how little.

[popover] { background-color: var(--bg); padding: 1rem; border-width: 0; border-radius: 0.5rem; &::backdrop { background-color: rgb(0 0 0 / 0.8); } }

But as soon as you try to add some transitions to that, it gets harder. For the moment, the best solution is to use some animation frames and to set the display transition to discrete. In practical application that ends up looking something like this.

Pop Pop!

This does in fact require quite a bit of css, but it's not as bad as it seems at first. Half of it is just applying the animations, but the part at the beginning to set the transition for display and overlay is actually required for many animations to work right for both in and out.

<button popovertarget="poppop">Click Me</button> <span popover animated id="poppop">Pop Pop!</span> [animated] { transition: display 0.3s allow-discrete, overlay 0.3s allow-discrete; animation: pop-slideOut 0.3s forwards; &:popover-open { animation: pop-slideIn 0.3s forwards; &::backdrop { animation: pop-fadeIn 0.3s forwards; } } &::backdrop { animation: pop-fadeOut 0.3s forwards; } } @keyframes pop-slideIn { from { translate: 0 -1em; opacity: 0; } to { translate: 0 0; opacity: 1; } } @keyframes pop-slideOut { from { translate: 0 0; opacity: 1; } to { translate: 0 1em; opacity: 0; } } @keyframes pop-fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes pop-fadeOut { from { opacity: 1; } to { opacity: 0; } }

The only other thing of major note here is that I am applying an overflow hidden to the html element when a popover is present, along with some other fun values.

html { scroll-behavior: smooth; scrollbar-color: var(--bg) var(--bg-dark); &:has(:popover-open) { overflow-y: hidden; scrollbar-gutter: stable; } }

OKLCH

In case you're not paying attention to the color meta these days, rgb is out, and hex is WRONG. OKLCH is the way to go. And while obviously any sith that would deal with those statements as absolute is wrong, it is definitely worth boning up on oklch and the new changes to color functions.

Color functions used to be separated into alpha and not alpha, but these days we don't write like this rgba(0, 0, 0, 0.5) anymore. We can wrap all that up in the single color function like so oklch(0 0 0 / 0.5). The alpha functions will all be deprecated out of use within a couple years.

While people have long been singing the praises of HSL over RGB or especially HEX, OKLCH has come along recently and dethroned it as the current intuitive color creation method. It does have its detractors, since some would argue that the lightness and chroma values aren't that intuitive. But what it does do is produce much better colors across a spectrum that all match lightness and saturation better.

With OKLCH, we get a lightness from 0 to 1, or 0% to 100%. We also get a chroma which is basically saturation, and ranges from 0 to 0.4, or 0% to 100%. And we get hue, which ranges from 0 to 360, but has essentially a much smaller green range than hsl has. Using these value becomes somewhat irrelevant when you realize that the real strength of oklch is taking another color and moving it along the hue wheel while maintaining the same relative lightness and chroma, eg. oklch(from hotpink l c 200) which causes the color to go from     to    . And if you notice, those two colors have VERY similar lightness and contrast, and that is where OKLCH shines. Doing the same with HSL would produce     to    . You can probably see it produces a different result.

It's not the only color function you'll ever need again. The fact is it doesn't work perfect for every color, and sometimes HSL is in fact the right tool to use, you'll have to add it in to your arsenal and figure out from time to time.

Color Functions

So let's just discuss the latest happenings around color functions in general.

  1. Color functions don't suggest using commas anymore, eg. rgb(255 255 255)
  2. Color functions that don't use commas can add opacity/alpha at the end with a slash, eg. rgb(0 0 0 / 0.5)
  3. Color functions have a from command that allows any color to derive from another. This is then followed by three letters, or replace any of them with an appropriate value, eg.
    • rgb(from blue r g b)
    • hsl(from #0f0 290 s l / 0.5)
    • oklch(from var(--bg) l calc(c + 0.1) h)

COLOR-SCHEME

There are currently two main ways to handle color scheme in CSS. And you kind of have to pick one or the other. If you want to use the media query, you can do that with two separate blocks of code.

:root { --bg: red; --text: blue; @media (prefers-color-scheme: dark) { --bg: blue; --text: red; } }

On the other hand, I have opted for the color-scheme method. With this, we can set which direction the light-dark functions think, and then set up our colors in one block.

:root { color-scheme: light dark; --bg: light-dark(white, black); --text: light-dark(black, white); }

Neither of these methods are right or wrong, but they should be picked depending on whether you just need to change a few light or dark colors, or whether you're setting up every color as light or dark. You can also add in a prefers-color-scheme query after the light-dark functions, but they should probably not be used together on the same project unless well documented.

Here's some radio buttons to switch the color-scheme. You'll see a lot of talk online about javascript solutions for manipulating the color-scheme, but this page doesn't use any javascript and the solution is really quite elegant when using the light-dark functions. This of course wouldn't be as easy to do with the prefers-color-scheme queries, because those are checking browser / os settings, and the color-scheme just has a manipulatable value.

<div class="radio-group"> <input type="radio" name="wants-color-scheme" id="color-scheme-auto" checked /> <input type="radio" name="wants-color-scheme" id="color-scheme-light" /> <input type="radio" name="wants-color-scheme" id="color-scheme-dark" /> <label for="color-scheme-auto">Auto</label> <label for="color-scheme-light">Light</label> <label for="color-scheme-dark">Dark</label> </div> :root { color-scheme: light dark; &:has(#color-scheme-light:checked) { color-scheme: light; } &:has(#color-scheme-dark:checked) { color-scheme: dark; } }

Padder

I've always struggled with the idea of a container class. Making it simple enough causes too many edge cases, and making it work for everything always adds a new thing it doesn't work for. With some of the new css values and functions, we can do things that weren't really viable before. One of the main things I want from a container is to be able to break out of it. I also would rather the container be on one parent, rather than on all the children, and when allowing things to break out of a standard container, you start using them on the children.

So rather than compete with people's understanding of a .container, here's a .padder. It allows a parent to set container like sizing on its children, but it uses padding instead of margin. This actually has a number of benefits, and doesn't explicitly remove the use of container as the opposite margin solution. But it does mean that the parent no longer needs a set width.

.padder { --padding: max(1rem, (100vw - 800px) / 2); padding-inline: var(--padding); container-type: inline-size; }

Then a .breakout child can be added that uses the width of the parent and the padding of the parent, to push itself out and then push back in its own children.

.breakout { width: calc(100cqi + (2 * var(--padding))); margin-inline-start: calc(-1 * var(--padding)); padding-inline: var(--padding); }

Math functions like calc max and min didn't exist when the original container classes were being thought up. And container queries themselves have only just started to get widespread use and adoption. With all of this added together we can make something that not only gives us the centering of the original container classes, it gives us default padding at smaller sizes, a simple way to breakout, and the ability for this element to be put into any part of the page and work from there.

Position Sticky

Position sticky isn't new at all, but I don't find people using it enough, and I believe a lot of people still find it kind of mistifying. There are some gotchas with it. The basics are: you want something to stick to the top of the page only when, and if, it reaches the top of the page. You can have a whole list of things that replace each other as they each reach the top.

[sticky] { position: sticky; top: 0px; }

In practice this can be done with one item, like a header, but it really shines with a list of elements that don't start at the top. This list has a number of headers that will replace each other as they reach the top.

Title

Header 1

Lorem ipsum dolor sit, amet consectetur adipisicing elit. Deserunt, quibusdam. Doloribus, vitae? Consequatur, doloremque! Necessitatibus ut enim quae animi atque.

Header 2

Odit ab aliquam a soluta reprehenderit, corrupti libero dignissimos consequuntur, fuga ipsa rem asperiores ut nemo recusandae at, unde sequi.

Header 3

Corporis, deleniti! Temporibus nisi id rem provident quisquam aperiam adipisci fuga molestiae placeat! Fugit id, eum voluptatem sequi aspernatur iste?

Header 4

Facere fugiat debitis, ipsa officiis omnis qui rem a perferendis doloremque, veniam vitae dicta reprehenderit laborum. Sed quas id sit?

Header 5

Porro laudantium sit, alias officiis vitae minus nostrum atque magni possimus consequuntur, dolorem veritatis quibusdam suscipit id tempore cum delectus!

<h2 sticky>...</h2> <p>...</p> <h2 sticky>...</h2> <p>...</p>

Notice that each element slides over the one previous. This might not be what you're looking for. If you want each one to push the previous out of the way, make each sticky element part of another container inside the scrolling element.

Title

Header 1

Lorem ipsum dolor sit amet consectetur adipisicing elit. Repellat deleniti explicabo natus officiis veniam ducimus itaque sunt magni id non?

Header 2

Eveniet sed reprehenderit obcaecati, ab facere, praesentium amet animi voluptate ut odio officia labore. Tenetur rerum praesentium perspiciatis quo molestias!

Header 3

Minima deserunt inventore tempore quibusdam similique molestiae sit culpa, cupiditate tenetur eos, voluptas nihil et debitis voluptatum, dignissimos quisquam! Nostrum!

Header 4

Esse voluptatem voluptate cum nam voluptatum et, voluptatibus atque, velit quam fuga quos debitis earum asperiores eius amet, cumque minima!

Header 5

Labore vero eos nemo magnam numquam minima, quis maxime quod suscipit voluptatem, ab illum est veritatis excepturi atque fugiat impedit.

<div> <h2 sticky>...</h2> <p>...</p> </div> <div> <h2 sticky>...</h2> <p>...</p> </div>

Title

Header 1

Lorem ipsum dolor sit amet consectetur adipisicing elit. Repellat deleniti explicabo natus officiis veniam ducimus itaque sunt magni id non?

Header 2

Eveniet sed reprehenderit obcaecati, ab facere, praesentium amet animi voluptate ut odio officia labore. Tenetur rerum praesentium perspiciatis quo molestias!

Header 3

Minima deserunt inventore tempore quibusdam similique molestiae sit culpa, cupiditate tenetur eos, voluptas nihil et debitis voluptatum, dignissimos quisquam! Nostrum!

Header 4

Esse voluptatem voluptate cum nam voluptatum et, voluptatibus atque, velit quam fuga quos debitis earum asperiores eius amet, cumque minima!

Header 5

Labore vero eos nemo magnam numquam minima, quis maxime quod suscipit voluptatem, ab illum est veritatis excepturi atque fugiat impedit.

In this case, I also ended up adding a sticky to the h1, increasing its z-index (to put it in front of the scrolling h2s, and offsetting each h2 by the h1 line-height.

[sticky] { position: sticky; top: 0; /* This selects an h2 sticky if it comes after an h1 sticky */ & ~ div > [sticky] { top: 3rem; z-index: 1; } /* This selects the h1 sticky if it has any h2 stickys */ &:has( ~ div > [sticky]) { z-index: 2; } }

In Conclusion

What have we learned here today? God, I hope I can just remember all this stuff. This wasn't even for you. I just don't remember things unless I write them down.

https://codepen.io/bronkula/pen/ZYWxVjK