Modern CSS for Web Apps and UIs

Framing Modern CSS and Theme-Aware Color with color-scheme and light-dark()

Stephanie sets expectations for a “what and why” tour of modern CSS, introduces three buckets (visual, functional, delightful), and emphasizes progressive enhancement for near-term features. They then demonstrate pairing color-scheme with light-dark() to adapt components to system light/dark preferences, noting that value order sets the preferred theme. By placing light first and using light-dark() in component styles, cards and other UI parts smoothly switch palettes without duplication. This segment grounds the talk’s theme: practical, well-supported CSS that improves UX with minimal overhead.

Theming Native Controls with accent-color and Blending Hues via color-mix()

Stephanie shows how accent-color instantly themes native controls (checkbox, radio, range, progress) and pairs well with light-dark() for contrast across themes. They introduce color-mix() to blend two colors in a specified color space (HSL, OKLCH), with a visual showing how spaces affect saturation and brightness. Practical recipes include mixing in transparent for opacity, and black/white for quick darken/lighten, while cautioning that design tools may yield better tuned results. This continues the visual track by giving developers reliable, composable primitives for color systems.

Aligning Components with Subgrid and Polishing Ragged Text with text-wrap

Stephanie resolves a classic alignment problem using subgrid, letting child components share a parent’s row/column tracks so card titles line up even when text wraps. A devtools view illustrates how row tracks are shared across cards, eliminating hacks like fixed heights or truncation. They then introduce text-wrap: balance and text-wrap: pretty to improve paragraph rag and avoid orphaned words, positioning “pretty” as a safe progressive enhancement. Together, these tools refine visual rhythm and readability without brittle layout tricks.

Taming Specificity with Cascade Layers and Responsive Layouts with Container Queries

Stephanie explains how @layer establishes an author-controlled cascade order—foundation, components, utilities—so later layers reliably win even over high-specificity selectors, neatly deprioritizing framework CSS. They recommend declaring layer order in the first stylesheet and adding to layers anywhere thereafter. Next, they switch to container queries, showing a card that reflows action buttons above the title when the card’s inline size is ≤ 35ch, independent of viewport width. This segment equips teams to structure CSS predictably and make components truly context-aware.

Selecting by Relationship with :has() for Smarter CSS Logic

Stephanie demonstrates :has() as a relational selector that can target parents, siblings, or scoped descendants based on what they contain. Examples include styling article if it has an image, selecting h1 only when followed by h2, and choosing subsequent paragraphs when an h2 precedes an h3. They also show compound “and” conditions to match complex structures (e.g., first-child image plus h1 followed by h2). This opens expressive, JS-free conditional styling patterns that align with the talk’s theme of powerful, modern CSS.

Overlay Reliability with Popover and Anchor Positioning, and Styling the Native Select

Stephanie introduces the Popover API, which promotes content to the browser’s top layer so tooltips, menus, and dialogs render above any stacking context—no z-index hacks required. With a simple popovertarget attribute and a matching popover element, the UI toggles open/closed, while Anchor Positioning (in active development) precisely places popovers relative to triggers and can replace heavy JS positioning libraries. They note that popovers now implicitly anchor to their triggers and that Anchor Positioning participates in Interop 2025, signaling imminent cross-browser maturity. Rounding out functional upgrades, the Custom Select proposal (appearance: base-select and new subcomponent selectors like ::picker(select)) enables full styling of native selects as an opt‑in progressive enhancement, with graceful fallback in unsupported browsers.

Animating From Hidden: Entry Animations with @starting-style and allow-discrete

Stephanie unveils Entry Animations, finally allowing transitions from display: none using @starting-style to define initial state and transition-behavior: allow-discrete to permit discrete-to-animated changes. They emphasize ordering nuances and point to a deep-dive resource to avoid common pitfalls. A popover demo animates from hidden without JS class swaps, replacing long-standing workarounds. This advances the “delightful” theme by elevating motion while keeping CSS in the driver’s seat.

Seamless UI Changes with View Transitions (Same- and Cross-Document)

Stephanie explains View Transitions, where the browser snapshots old/new states and animates between them with CSS—reducing reliance on animation libraries. They distinguish same-document transitions (great for SPA-like flows) from cross-document transitions (animated navigations across pages), noting Firefox support is in progress via Interop 2025. Developers can override the default cross-fade using keyframes and view transition pseudo-elements, with only a tiny JS wrapper around the state change. This segment shows how to deliver polished, app-like navigations with minimal code.

Scroll-Driven Animations and Closing Recap

Stephanie introduces Scroll-Driven Animations, where scrolling acts as the timeline via animation-timeline: scroll() or view(), enabling smooth, off‑main‑thread motion. They outline two timelines—Scroll Progress (container-wide) and View Progress (per-element)—with examples like a reading progress bar and image fade/scale-in on articles, and share a resource hub for patterns. Although experimental in Chrome, the API is additive and can replace JS scroll listeners for better performance. The talk concludes with a feature recap and encouragement to explore and share these modern CSS capabilities with your team.

Yeah, as John said, modern CSS, that has kind of become a term I have been associated with for a few years now, including the domain moderncss.dev.

But today we're going to focus a lot about the 'what' and the 'how' and a little bit  - or excuse me, the 'what' and the 'why' and a little bit less on how. So just some nuggets about that for those that are interested. And just to get ahead of what we're doing as well, most of the things that you'll see are very, very well supported. I will note when there's a couple of upcoming features, but I do encourage you to consider those as progressive enhancements. So let's get into it.

We have three areas to look at these today. The first one is visual, or those that make a visual impact, functional, where they impact architecture or behavior, and then delightful, those that are going to bring joy to the UX or DX for your projects. And just a little heads up, might want to have your camera ready. I will have some QR codes for resource links throughout. So starting with visual, let's take a look at this feature pairing of color-scheme and light-dark(). Using these two properties allows you to gracefully adapt your app and component styles to handle light and dark themes.

First, you want to define the color-scheme property with the values of light and dark. This property responds to the operating system preference for a user's device, and the order matters. So in this case, we've listed light first, which means that our app has a priority preference for a light theme, but it also will work well with a dark theme. Next, within your components, for any color related value that you're using, you can now adopt the light-dark function which accepts two values.

The first one that you provide is going to be what renders in a light theme, and the second in a dark theme.

So this enables components like this card to smoothly adapt between light and dark themes.

Developers love this feature pairing, particularly light-dark, because it has the ability to remove duplication, and possible current separation between these color theming related styles. Our next property is accent-color, which allows you to easily add color theming to native form controls, such as those seen here, which include the radio and checkbox when those are checked, the range slider, and progress meter filled portions.

And it's also very complimentary for use with color-scheme and light-dark. So in our code snippet here, we're using the light-dark function for accent-color. And this helps us ensure that we have better contrast of our preferred color within those two color schemes.

Keeping on our color theme, let's take a look at color- mix(). So color-mix() accepts two colors.

and mixes them by the provided amount. It's kind of like when you're in primary school and you were mixing two colors together, right? Figuring what that combo would be. It does require a 'colorspace', which is kind of a scary word. Stick with me.

Such as in HSL or in OKLCH.

And just as an example for this current code snippet, where we're mixing cyan and lime, where we're mixing lime in by 25%. And this expanded graphic mixes that in by increasing amounts and shows the difference between a few of the available color spaces. You can see how some are more muted and some are brighter. So put that in your notes to look up color-spaces on the web at a different time because it's too much to go into today.

Color-mix() can also be used to give color opacity.

by mixing in transparent. This is really exciting.

Might not sound exciting, but this is a feature I definitely have wanted for a long time. The color space when you're doing something like this is still required, but it's not going to be as impactful when you're mixing in transparent. So just pick one if this is your goal. You can also mix in black to darken colors and white to lighten the colors.

But just kind of be aware that these are okay in a pinch, but might not be your ideal results for which you'll possibly want an actual design editor to compute.

All right, it is time to leave color behind and consider a classic layout problem that we can resolve using 'Subgrid', a feature of CSS Grid.

Usually, subgrid is desired to fix a classic layout problem.

which is when designers want things to align visually across separate components or template areas.

Prior to subgrid being available, we had to explain to designers that this either wasn't possible or use hacks like fixed heights or truncation. But with subgrid, we can direct elements to participate in a parent's layout grid by sharing the alignment and space allotment for either rows, columns, or both.

So in this group of cards, the alignment mismatch is noticeable when that title begins to wrap. So in this case, we'd like to share row alignment across our components.

Since we're interested in that row alignment, we're going to check out the current grid layout for our card, and we can see that we have three rows for the card in this layout.

Once we have the sub-grid assignment in place, when we select the parent layout grid, we can now see how those grid rows are shared across all of our card elements. And this results in that fix for the visual alignment of those titles, even though each card is a separate component.

Another property that fixes a long-standing web design issue is 'text-wrap'. And this directs the browser to either balance lines of text to improve visual appearance or make text blocks pretty by preventing hanging words, which are sometimes called orphans.

Previously, this was only possible by inserting manual line breaks, or adjusting specific widths, and that wasn't really friendly for either responsive design or international text concerns. The difference in these values is shown through these text block examples, where the first uses 'text-wrap: balance;', and the second uses 'text-wrap: pretty;'.

Pretty is currently only available in Chrome and Edge.

However, this is one of ours that is definitely in progressive enhancement category, meaning if you apply it and it doesn't work in a user's browser, there's minimal impact and no impact to the functionality. This is just a visual improvement.

Next up, let's look at some functional features that impact CSS architecture or behavioral aspects.

This first tool is meant to improve CSS architecture.

and this is Cascade Layers or the @layer rule.

Cascade Layers gives CSS authors control over specificity of rule sets. I know, I'm getting into some scary words here. Put another way, this is the ability to direct the priority of your foundational styles versus component styles versus utilities. And a key benefit is that it enables deprioritizing third-party or framework styles in favor of your application styles without hacks around that or any headaches that can ensue. The recommended way to use @layer is to define that order in your first loaded stylesheet. And the reason for this is because layers will continue to be evaluated against that initial order. Then, throughout your style sheets that follow, you can add styles to those layers. You can do this as many times as you want.

That order does not have to match that initial order.

And the key here is that rules from later defined layers will always win over rules in earlier defined layers. And that is the superpower of cascade layers. So even classically weak specificity selectors, such as the 'focus-visible' one in the states layer I have here, will beat out styles in earlier layers, including those with more classically strong specificity, such as IDs.

One of the most exciting tools we've received to handle responsive design concerns is Container Queries. And to see why these will be helpful, here's a current card in a large and comfortable space where everything fits pretty nicely in this layout. But when it starts to squeeze, get narrower, having the action buttons in a column next to the title is at risk of causing overflow.

So we're going to employ container queries to adjust the underlying grid layout template and rearrange the action buttons and the title into a stacked layout for narrower spaces. Container Queries respond to an element's available inline space.

And these enable dynamic changes across template areas that are independent of the actual browser or device size. And that is the difference between media queries, which are responsive to the viewport size or browser width. So here's a snippet of what we'll write for the updated card styles. Based on a parent container, we'll check if the card's computed inline size is less than or equal to 35 characters. And if it is, our container query will apply and adjust the card template for the narrower view. So here's the update comparing the large card to the narrower card, where again, for the narrower card, the action buttons have been moved to stack above the title.

A few more suggestions on what container queries can do include adjusting the font size and spacing values, really anywhere.

And for those, I'd also suggest you consider using container query units, which we didn't get to look at today. So write that one down too. You can do things like flipping from an inline menu to a dropdown or changing your layout to use a background image to instead an inline image and really just any other layout mutations to help you relieve problems from overflow potential or other space-related considerations.

A selector that had folks pretty excited when it came out and was once thought by browser engineers to actually be impossible is ':has()', and we have now had it available to use since December 2023. ':has()' is also referred to as the parent selector. But that's just one of its capabilities.

It is the one demonstrated here though, which shows selecting a parent element based on the presence of some sort of selected content. So in this example, we're styling the article element if it has an image. We can also check sibling conditions.

So in this example, we're selecting the h1 only if it is followed by an h2. And we can use it with compounded selectors to select something other than the parent element, as is in this example, which looks if an h2 is followed by an h3, and if so, we're selecting the paragraphs that follow.

We can even create 'and 'conditions for identifying complex groupings. So this selector only selects the middle article because it contains an image as the first direct child and it contains an h1 followed by an h2. We honestly could spend all day looking at cool ways to use ':has()', but I hope this quick list has inspired you to check it out.

Next up is the pretty new Popover API.

This feature is cross-browser and is considered baseline newly available. But this is an HTML API.

So why am I bringing it up in a CSS talk? And to understand that, let's learn what CSS problem it helps solve.

The Popover API gives us a guaranteed way to beat any element layering order, which is by promoting its contents to a fairly new browser feature of the top layer. Promoting an element to the top layer breaks it free of any stacking context it may otherwise exist within. In other words, wherever the popover actually lives in the DOM, this top layer access guarantees it will be visible over everything else when the popover is open.

This is important because it removes the needs for hacks that were previously required to guarantee visibility of overlays, such as tooltips, dropdown menus, custom dialogs, pop-out search, teaching callouts, the list goes on.

To create a popover in the most simple HTML-only way, you first add the attribute 'popovertarget' to a button with a value that matches the ID of an element with the popover attribute.

And just like that, you have a toggleable popover.

The popover will be hidden until it is toggled open, and the default style from the browser is fixed position in the center of the viewport. So that gives us the remaining problem of positioning our popover. And to resolve that, we'll bring in Anchor Positioning, which is one of the most modernist of the CSS features.

The spec is still evolving, but partial implementations are available in Chromium and Safari technical preview, unless that changed in the last week that I pulled that.

It's also part of interop 2025, which means that browsers are actively working on either implementing or improving their implementations.

So it is coming as a guarantee. And if you're looking to use it soon, you might want to have a fallback library, but let's learn what it does.

Anchor positioned elements have their position set relative to the defined anchor element.

So in this example, the popover is anchored to the favorite button.

Anchor Positioning enables positioning any element, not just popovers, relative to another element.

And the use of anchor positioning will allow your team to remove JavaScript dependencies on heavier positioning libraries that exist on the market today. It also comes with features you expect, like repositioning based on edge detection during scroll or resize.

This demo video is from Una and it shows how directing the anchored element's position requires just this one property. There are a couple of other steps involved to create a positioning relationship, unless you're using a popover.

A more recent change is that popovers now have an implicit anchoring relationship with their trigger, so you only need this property.

Our last functionality update is for Custom Select, and it's also a work in progress. The full capabilities here are currently only in Chrome and Edge.

But Custom Select will allow you to customize the '