Have you ever found yourself sobbing in a corner after attempting to create layouts with float
.
Thankfully nowadays, we have access to properties such as flex
and grid
, and CSS has been getting progressively better every year.
If you haven't been keeping up, you might have missed some powerful new features that can make your life as a developer even easier.
Let's explore five modern CSS properties that deserve a spot in your toolkit!
:has()
– The parent we always wanted
Back in the days of jQuery, we had the .parent()
method. It took a while, but finally CSS has it's own native solution.
Simply speaking, :has()
is a way to style elements based on their children.
So, how do we use it?
Syntax:
element:has(selector) {
/* CSS rules */
}
Example:
article:has(img) {
border: 2px solid #ff6347; /* Tomato-colored border */
}
In this example, any <article>
containing an <img>
will have a tomato-colored border.
Real world examples:
/* Blog articles get a spicy border if they contain any image */
article:has(img) { border: 2px solid #ff6347; }
/* Forms blush red when inputs go rogue */
form:has(input:invalid) { border: 2px solid red; }
/* Navigation items glow when their link is active */
nav li:has(a.active) { background: #f0f0f0; }
@container
– Responsive components that make sense
Imagine you’ve built a beautiful .card
component. It looks perfect in your main content area, but when you drop it into a sidebar or a smaller grid, it breaks. Why? Because media queries only care about the viewport size, not the container size.
A .card might look great at full width (1200px viewport), but when placed in a sidebar (300px wide), it’s cramped and unusable.
The old way: Media Queries
To fix this, you’d typically write a bunch of media queries or override styles for every possible container:
/* Media query for viewport */
@media (max-width: 768px) {
.card { /* Styles for smaller viewports */ }
}
/* Override for sidebar */
.sidebar .card { /* Styles for sidebar */ }
/* Override for grid */
.grid .card { /* Styles for grid */ }
This approach is brittle and hard to maintain. If you change the layout or add new containers, you’ll need to write even more overrides.
The new way: Container Queries
With container queries, your components adapt to their own container’s size – not the viewport. No more endless overrides!
/* Step 1: Make the parent a container */
.card-container {
container-type: inline-size; /* Watch the container's width */
}
/* Step 2: Style the card based on its container's size */
@container (min-width: 400px) {
.card {
display: flex; /* Switch to flex layout when there's enough space */
gap: 1rem;
align-items: center;
}
.card img {
width: 150px; /* Give the image more room */
}
}
So, what's happening here?
If the .card-container
is narrow (less than 400px), the card stacks vertically.
If the container is wide enough (400px or more), the card switches to a horizontal layout, with the image and text side by side.
view-transition-name
– Silky smooth transitions without JavaScript
Tired of wrestling with JavaScript libraries just to add page transitions? Meet view-transition-name
, your new best friend for creating seamless, native animations between pages or states.
With this property, you can effortlessly animate elements as they change – no JavaScript required. It’s like magic, and with better browser support.
Example: Fading in a card
.card {
view-transition-name: fade-in;
}
When the .card
element changes (e.g., moves, appears, or disappears), the browser automatically creates a smooth transition.
Elements with the same view-transition-name
are linked, allowing for seamless animations between pages or states.
text-wrap: balance
– Smarter line wrapping
Ever seen a heading where the last word awkwardly sits all by itself on a new line? It’s like the text got cut off halfway through, making it look a little unfinished?
This property solves that by evenly distributing text across lines, ensuring your headings look sharp and professional – no manual tweaks or extra <br>
tags needed.
Example:
h1 {
text-wrap: balance; /* Distributes text evenly across lines */
}
The browser intelligently balances the text, avoiding those pesky single-word orphans.
Your headings look cleaner and more polished, whether they’re short or long.
/* Balance headings for a polished look */
h1, h2, h3 {
text-wrap: balance;
}
/* Balance blockquotes for better readability */
blockquote {
text-wrap: balance;
font-style: italic;
}
Use text-wrap: balance
for headings, blockquotes, or any text where readability and aesthetics matter. It’s a small change that makes a big difference.
scroll-timeline
– Scroll-linked animations made easy
Remember when creating scroll-triggered animations meant diving into JavaScript and wrestling with complex libraries? Those days are over. Meet scroll-timeline
, the CSS property that lets you sync animations with scrolling – no JavaScript required!
Example:
@keyframes fade-in {
from { opacity: 0; } /* Start invisible */
to { opacity: 1; } /* End fully visible */
}
.scroll-element {
animation: fade-in linear; /* Apply the animation */
animation-timeline: scroll(root); /* Sync with viewport scroll */
animation-range: cover 0% cover 100%; /* Control animation timing */
}
/* Fallback for browsers that don't support scroll animations */
@supports not (animation-timeline: scroll()) {
.scroll-element {
opacity: 1;
}
}
In the above snippet, as you scroll down the page the .scroll-element
fades in smoothly. The animation is tied to the viewport's scroll position using scroll(root)
, which means:
animation-timeline: scroll(root)
tracks scrolling relative to the viewport rather than a specific elementanimation-range: cover 0% cover 100%
defines when the animation starts and ends based on the element's visibility in the viewport- The animation progresses linearly as you scroll, giving you smooth, precise control
PS. Those with eagle eyes may have noticed that the Subscribe button on this page changes color as you scroll. That cool effect is created easily with scroll-timeline
:)
Pro tip: You can create more complex scroll-driven animations by adjusting the animation range:
/* Start animation when element is 30% in view, end when fully visible */
.scroll-element {
animation: fade-in linear;
animation-timeline: scroll(root);
animation-range: cover 30% cover 100%;
}
Until next time! 👋