Modern CSS Magic
:has(), nesting, @scope, color-mix(), light-dark(), anchor positioning.
Every year someone says "CSS doesn't need a preprocessor anymore," and every year they get closer to being right. Native nesting, parent selectors, color manipulation, scoped styles, and light/dark theming all shipped in 2023 and 2024. If you learned CSS five years ago and haven't looked since, you missed a lot. This lesson catches you up.
:has() the parent selector we waited 20 years for
:has()finally gives CSS what every developer always wanted: the ability to style a parent based on its children. The joke used to be that CSS had no parent selector. Now it does, and it's arguably the most powerful selector in the language.
/* A card that has a video inside it gets a different border */
.card:has(video) {
border-color: #dc2626;
}
/* A form where the email field is invalid */
form:has(input[type=email]:invalid) .submit {
opacity: 0.5;
pointer-events: none;
}
/* A label whose checkbox is checked */
label:has(input:checked) {
background: #4f46e5;
color: white;
}The second example is wild: you can disable a submit button purely in CSS, based on whether a sibling input is in the :invalid state. Form validation styling that used to require JavaScript is now just selectors.
:has()first. It's usually a one-line replacement.Native CSS nesting (no preprocessor needed)
Nesting was the headline feature of Sass and Less for a decade. It finally shipped in browsers in 2023 with a similar syntax. The & symbol refers to the current selector:
.card {
background: white;
padding: 16px;
& h3 {
font-size: 18px;
margin-bottom: 8px;
}
&:hover {
background: #f8fafc;
}
@media (min-width: 640px) {
padding: 24px;
}
}Nesting is mostly sugar (it doesn't change what your CSS cando), but it keeps related styles together and shrinks the cognitive load of jumping around stylesheets. Don't go wild with it; deeply nested rules become hard to grep for. Two levels deep is usually plenty.
color-mix() and color-contrast()
Need to make a darker version of your primary color without recalculating its hex code? color-mix() blends two colors at a given percentage:
:root {
--primary: #4f46e5;
}
.button {
background: var(--primary);
}
.button:hover {
/* 80% primary, 20% black */
background: color-mix(in oklch, var(--primary) 80%, black);
}
.button:disabled {
/* 30% primary, 70% white */
background: color-mix(in srgb, var(--primary) 30%, white);
}The in oklch part is the color space. oklch gives perceptually uniform interpolation, which makes mid-blends look like what you'd expect. The old srgb space tends to produce muddy intermediate colors.
light-dark() for theming
Building a light/dark theme used to mean writing every color twice in two media queries. light-dark() lets you specify both in one line:
:root {
color-scheme: light dark; /* opts the page into auto theming */
}
body {
background: light-dark(white, #0b1020);
color: light-dark(#0f172a, #e2e8f0);
}With color-scheme: light dark, the browser auto-picks based on the user's OS setting. Form controls, scrollbars, and text fields use the right theme automatically. Combine with light-dark() for your own colors and you have a full theme in maybe 20 lines.
@scope: scoped styles without methodology
BEM, CSS Modules, scoped attributes, all of these methodologies exist to answer one question: "how do I make sure this style doesn't leak?" @scope is the native answer.
@scope (.card) to (.card-inner) {
/* Styles here apply to .card and descendants,
but stop at .card-inner (an "escape hatch"). */
h3 { color: #4f46e5; }
p { color: #475569; }
}The toclause is optional but powerful: it stops the scope from cascading further. Use it to say "style the card, but stop when you hit content I don't own."
Anchor positioning (preview)
One of the newest features (rolling out 2024-2026) is anchor positioning: tether one element's position to another's, without JavaScript. Think tooltips, popovers, dropdowns. The classic problem of "put this menu below that button, but flip it if there's no room" becomes a CSS one-liner. Worth knowing about, but support is still landing.
.trigger { anchor-name: --menu-anchor; }
.menu {
position: absolute;
position-anchor: --menu-anchor;
top: anchor(bottom);
left: anchor(left);
}:has() in action
Try this playground. The form below disables its submit button whenever any required field is empty, purely in CSS. Open the DevTools and confirm that there is no JavaScript.
:has() aggressively, but very broad uses (like body:has(.something-deep-inside)) can force layout invalidation on every change. Scope your :has() selectors as narrowly as you can.Quiz
Which CSS feature lets you style a parent based on its children?
Recap
:has()is the parent selector. Style a parent based on its descendants, including form state.- Native CSS nesting works with
&. Two levels deep max, usually. color-mix(in oklch, A 80%, B)blends colors without a preprocessor. Useoklchfor perceptual smoothness.light-dark(a, b)pluscolor-scheme: light darkgives you a theme with no media queries.@scopecontains styles without methodology overhead.- Anchor positioning is on its way and will eventually replace most tooltip/popover JS libraries.