๐ง Hey, quick story
Last week I watched a senior dev write 47 lines of JavaScript.
To center a div.
He wasn't joking.
look, CSS has changed. A LOT. Most tutorials still teach you the 2019 way of doing things. The stuff I'm about to show you? It makes old approaches look like cave paintings.
let's go.
๐ฏ 1) :has() โ The selector CSS never had (until now)
This is the one people lose their minds over.
:has() lets a parent react to its children. CSS couldn't do this before. At all.
/* Style the card ONLY if it contains an image */
.card:has(img) {
padding: 0;
}
/* Highlight a form group that has an invalid input */
.form-group:has(input:invalid) {
border: 2px solid red;
}
/* Style a list item that contains a highlighted span */
li:has(span.highlight) {
background: yellow;
}
๐ง What's actually happening here?
Before :has(), you needed JavaScript. You'd do something like:
// Old way โ painful
document.querySelectorAll('.card').forEach(card => {
if (card.querySelector('img')) {
card.style.padding = '0';
}
});
Now? One CSS line. No JS. No DOM queries. The browser handles it.
Real use case: You have a notification bar. Only show it if there's actual content inside:
.notification-bar:has(p) {
display: flex;
}
No display: none toggle logic. The CSS knows.
โก 2) container queries โ Responsive without the viewport
Media queries check the screen size. Container queries check the parent size.
That's a massive difference.
/* The container */
.card-wrapper {
container-type: inline-size;
container-name: card;
}
/* When the CONTAINER is wider than 400px */
@container card (min-width: 400px) {
.card {
display: flex;
gap: 1rem;
}
}
/* When the CONTAINER is narrower */
@container card (max-width: 399px) {
.card {
flex-direction: column;
}
}
๐ Real-world example:
Imagine a dashboard. Sidebar is 300px. Main area stretches. You have the same widget component in BOTH areas.
With media queries, the widget only knows about the screen. It doesn't care if it's sitting in a 300px sidebar or a 900px main area.
With container queries, the widget adapts to wherever it's placed. Same component. Different layouts. Automatically.
โโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ โ
โ widget โ widget โ
โ (stacked) โ (side-by-side) โ
โ โ โ
โ 300px wide โ 900px wide โ
โโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
This is the future of responsive design. Start using it today.
๐งน 3) text-wrap: balance โ Fix ugly headings
You know when a heading wraps and one line has 3 words and the next has 1? It looks terrible.
/* Before: uneven lines */
h1 {
/* "How to Build a Modern" */
/* "Website" */
}
/* After: balanced! */
h1 {
text-wrap: balance;
/* "How to Build a" */
/* "Modern Website" */
}
That's it. One property. Your headings instantly look like they were designed by someone who cares.
Bonus: text-wrap: pretty does something similar for paragraph text โ it avoids orphaned words at the end of paragraphs.
p {
text-wrap: pretty;
}
No more JavaScript libraries for this. No more hacky tricks. Native CSS.
๐จ 4) accent-color โ Style native checkboxes and radios
You've probably built entire custom checkbox components in React. 200 lines of code. For a checkmark.
/* Just... do this instead */
input[type="checkbox"] {
accent-color: #6c5ce7;
width: 20px;
height: 20px;
}
input[type="radio"] {
accent-color: #00b894;
}
input[type="range"] {
accent-color: #e17055;
}
๐ค Why this matters
Native form elements are:
- Accessible by default (keyboard, screen readers)
- Fast (no re-renders)
- Consistent across platforms
Custom components break all of this unless you're extremely careful.
accent-color gives you the branding you want WITHOUT rebuilding accessibility from scratch.
Works on checkboxes, radio buttons, range sliders, and progress bars.
๐ฅ 5) aspect-ratio โ No more padding hacks
For YEARS, maintaining aspect ratios required this monstrosity:
/* The old padding-top hack โ don't do this */
.video-wrapper {
position: relative;
padding-bottom: 56.25%; /* 16:9 */
height: 0;
}
.video-wrapper iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
Now?
.video-wrapper {
aspect-ratio: 16 / 9;
width: 100%;
}
/* Or for a square */
.avatar {
aspect-ratio: 1;
width: 100px;
}
/* Or auto (uses the image's natural ratio) */
img {
aspect-ratio: auto;
width: 100%;
}
One line. Works on any element. Images, videos, divs, iframes โ anything.
Use it for: thumbnail grids, video embeds, profile photos, card images, canvas elements.
๐ญ 6) scroll-driven animations โ No JavaScript scroll listeners
This one is brand new and it's wild.
You can now create scroll-based animations purely in CSS. No addEventListener('scroll'). No Intersection Observer. No requestAnimationFrame.
/* Element fades in as you scroll it into view */
.fade-in {
animation: fadeIn linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Progress bar that fills as you scroll the page */
.progress-bar {
position: fixed;
top: 0;
left: 0;
height: 4px;
background: #6c5ce7;
transform-origin: left;
scaleX(0);
animation: growBar linear both;
animation-timeline: scroll();
}
@keyframes growBar {
to { scaleX(1); }
}
๐ง What's happening here?
animation-timeline: view() ties the animation to when the element is visible in the viewport. As you scroll it into view, the animation plays.
animation-timeline: scroll() ties it to the overall page scroll position.
animation-range controls WHEN during that scroll the animation starts and ends.
Before this, you'd need something like GSAP ScrollTrigger or AOS.js. Now it's native. In CSS. The performance is insane because the browser handles it compositor-side.
๐ก๏ธ 7) color-mix() โ Dynamic colors without a preprocessor
Need a lighter version of your brand color? A darker hover state? An overlay?
/* Mix your brand color with white (lighter) */
.card:hover {
background: color-mix(in srgb, #6c5ce7, white 30%);
}
/* Mix with black (darker) */
.button:active {
background: color-mix(in srgb, #00b894, black 20%);
}
/* Mix two colors together */
.mixed {
color: color-mix(in srgb, #e17055, #6c5ce7 50%);
}
/* Mix with transparency */
.overlay {
background: color-mix(in srgb, #2d3436, transparent 50%);
}
๐ง Why this is a big deal
Before color-mix(), you either:
- Used Sass/Less (extra build step)
- Manually calculated hex values
- Used
hsl()and did mental math
Now? You just say "give me this color mixed 30% with white." The browser calculates it. No preprocessor needed.
Pro tip: Use it with CSS custom properties:
:root {
--brand: #6c5ce7;
}
.card {
background: color-mix(in srgb, var(--brand), white 80%);
}
.card:hover {
background: color-mix(in srgb, var(--brand), white 60%);
}
.card:active {
background: color-mix(in srgb, var(--brand), black 10%);
}
One brand color. Three shades. Zero hex math.
๐ 8) subgrid โ Align children across nested grids
This one solves a problem that's been driving layout nerds insane for years.
You have a grid of cards. Each card has a title, description, and button. You want the titles to align across all cards, even if they're different lengths.
/* Parent grid */
.cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
}
/* Each card is ALSO a grid โ but uses subgrid for rows */
.card {
display: grid;
grid-template-rows: subgrid;
grid-row: span 3; /* title + description + button */
}
/* Now ALL titles align. ALL descriptions align. ALL buttons align. */
๐ Visual comparison
Without subgrid:
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโ
โ Short Title โ โ A Much Longer Title โ
โ โ โ That Wraps โ
โ desc... โ โ desc... โ
โ โ โ โ
โ [Button] โ โ [Button] โ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโ
โ misaligned โ buttons don't line up
With subgrid:
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโ
โ Short Title โ โ A Much Longer Title โ
โ โ โ That Wraps โ
โโโโโโโโโโโโโโโโค โโโโโโโโโโโโโโโโโโโโโโโโค โ all titles same height
โ desc... โ โ desc... โ
โ โ โ โ
โโโโโโโโโโโโโโโโค โโโโโโโโโโโโโโโโโโโโโโโโค โ all descriptions same height
โ [Button] โ โ [Button] โ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโ โ all buttons aligned
The child grid inherits the parent's row tracks. Everything lines up. No magic numbers. No flex hacks.
๐งช 9) @layer โ Control specificity without fighting it
The #1 cause of !important abuse? Specificity wars.
@layer lets you declare which stylesheets take priority.
/* Define layer order โ first = lowest priority */
@layer reset, base, components, utilities;
/* Reset layer โ lowest priority */
@layer reset {
* { margin: 0; padding: 0; }
}
/* Base layer */
@layer base {
body { font-family: system-ui; color: #333; }
}
/* Components layer */
@layer components {
.button {
background: #6c5ce7;
color: white;
padding: 0.5rem 1rem;
}
}
/* Utilities layer โ highest priority */
@layer utilities {
.bg-red {
background: red !important; /* this actually works cleanly now */
}
}
๐ง What this means
Styles outside any layer ALWAYS beat styles inside layers. So your framework's styles (in a layer) can't accidentally override your custom styles (outside layers).
/* This beats everything in @layer โ even if the layer rule uses !important */
.my-button {
background: black;
}
Perfect for: third-party CSS frameworks, design systems, team projects where people keep overriding each other's styles.
๐ช 10) @scope โ Truly scoped CSS (no more BEM wars)
BEM. CSS Modules. CSS-in-JS. Tailwind. All of these exist because CSS doesn't scope by default.
Now it does.
@scope (.card) {
:scope {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
}
h2 {
font-size: 1.25rem;
color: #2d3436;
}
p {
color: #636e72;
line-height: 1.6;
}
}
๐ค What's happening?
Everything inside @scope (.card) only applies to elements inside .card. The h2 rule won't leak out and style every h2 on the page.
:scope refers to the .card element itself.
You can even set scope boundaries:
/* Don't style anything inside .card .sidebar */
@scope (.card) to (.sidebar) {
p { color: #333; }
/* This p style WON'T apply inside .card .sidebar */
}
No build step. No naming convention. No runtime cost. Just actual scoped CSS. Finally.
๐ง TL;DR Cheat Sheet
| Trick | What it kills | Support |
|---|---|---|
:has() |
JS parent selectors | โ All modern browsers |
container queries |
Media query hacks | โ All modern browsers |
text-wrap: balance |
Ugly heading wraps | โ Chrome, Edge, Safari |
accent-color |
Custom checkbox libraries | โ All modern browsers |
aspect-ratio |
Padding-top hack | โ All modern browsers |
scroll-driven animations |
GSAP/AOS scroll JS | โ Chrome, Edge (Safari soon) |
color-mix() |
Sass color functions | โ All modern browsers |
subgrid |
Flexbox alignment hacks | โ All modern browsers |
@layer |
!important abuse | โ All modern browsers |
@scope |
BEM / CSS Modules | โ Chrome, Edge (Safari soon) |
๐ Stop writing JavaScript for CSS problems
Half the "CSS is broken" takes come from people who learned CSS in 2018 and never looked back.
CSS in 2026 is a different language. Scoped styles. Container awareness. Scroll animations. Parent selectors. Color math. All native. Zero dependencies.
The next time you reach for a JavaScript library to do something visual... check if CSS already does it.
It probably does.
Top comments (0)