CSS has changed a lot in the past few years. What used to require JavaScript or a preprocessor can now be done natively, sometimes with a single line of CSS. If you haven’t looked closely at the newer features recently, it’s worth catching up. The tools are powerful! Here are ten modern CSS tricks that can simplify your front-end work and make your UI feel more polished.
Styling parents based on their children
For a long time, CSS could only look down the DOM tree. You could style a child based on its parent, but not the other way around. The :has() pseudo-class finally fixes that. It lets you style an element if it contains something specific.
.fieldset:has(.checkbox:checked) {
border-color: deeppink;
}
You can highlight a menu when one of its items is selected, or restyle a form section when a checkbox is checked, all without touching the DOM.
It also works sideways. You can do things like:
label:has(+ input:focus) {
color: hotpink;
}
This line targets the label if the input next to it is focused.
Browser support is excellent (around 93%), and it instantly makes CSS feel reactive. If you think in “component states,” :has() will feel natural.
Container Queries – Components that adapt to their container
Media queries are great… until your layout changes. You can’t always predict how wide a component will be when it’s reused in different contexts. That’s where container queries come in. They make elements respond to their container size, not the viewport.
.card {
container-type: inline-size;
}
@container (max-width: 600px) {
.card img {
display: none;
}
}
This example hides the image when the card becomes narrow, no matter what screen size it’s on. Container queries turn CSS into something modular again, your components finally behave independently.
Browser support is already around 93%, so you can safely start using them today.
If you want to learn more, I wrote a small interactive article about it.
Perfect alignment across components
If you’ve ever used CSS Grid for complex layouts, you’ve probably run into this: nested grids that don’t line up because each one defines its own columns. Subgrid solves that by letting child elements inherit their parent’s grid tracks.
.grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
}
.card-list {
display: grid;
grid-template-columns: subgrid;
}
Now your cards align perfectly with the main grid, even though they’re nested. No more pixel-pushing or magic numbers.
Browser support is about 89%.
Here’s a codepen to see it live.
Readable styles without a preprocessor
Nesting used to be a Sass feature. Now it’s built right into CSS, and it’s much cleaner than most people expect.
.card {
padding: 1rem;
&:hover {
background-color: grey;
}
h2 {
margin: 0;
}
}
No compilation step, no special syntax to learn. Native nesting reduces boilerplate and makes component styles feel self-contained again.
Browser support is already around 91%, and most modern browsers handle it smoothly. Once you start using it, you’ll never want to go back to writing ten separate selectors for one component.
Cascade Layers – Predictable CSS priority
The cascade has always been powerful but sometimes painful. Between resets, frameworks, and overrides, it’s easy to lose control of what wins. Cascade layers (@layer) bring order to the chaos. They let you define clear “sections” of your stylesheet, ordered by importance before specificity even kicks in.
@layer reset, components, utilities;
@layer utilities {
.hidden {
display: none;
}
}
@layer reset {
* {
box-sizing: border-box;
margin: 0;
}
}
@layer components {
.btn {
padding: 0.5rem 1rem;
}
}
Now you can structure your CSS like code modules, not a random pile of overrides. No more !important wars.
Browser support is around 94%, so it’s safe to use almost everywhere.
Typed Custom Properties – Real variables you can animate
CSS variables (–my-color, –size) are great, but they’re “untyped.” You can’t animate them smoothly, and browsers don’t know how to interpolate their values. @property changes that. It lets you declare typed custom properties with default values and syntax hints.
@property --angle {
syntax: '<angle>';
initial-value: 0deg;
inherits: false;
}
.loader {
animation: spin 3s linear infinite;
}
@keyframes spin {
to {
--angle: 360deg;
}
}
You can now animate the variable itself and use it anywhere, like in gradients or transforms. This opens up subtle effects like rotating borders, gradient animations, or theming systems that feel smooth without any JavaScript.
Browser support: about 94%, and totally worth trying.
Here’s a codepen to see it live.
View Transitions API – Page transitions with CSS only
Page changes on the web have traditionally felt abrupt. You click a link, and the old page instantly disappears while the new one appears. It works, but it lacks the kind of smoothness you get in native apps. The View Transitions API changes that. It lets the browser capture a snapshot of your content before and after navigation, then animate between them automatically. Under the hood, the browser freezes the old content, layers the new one on top, and runs your defined transition. By default, it cross-fades between the two states, but you can style it however you like.
Here’s the cool part: you can now do all of this entirely in CSS, without a single line of JavaScript.
@view-transition {
navigation: auto;
}
#content {
view-transition-name: content;
}
::view-transition-old(content) {
animation: translate-out 400ms linear both;
}
::view-transition-new(content) {
animation: translate-in 400ms linear both;
}
@keyframes translate-out {
to {
translate: 100vw 0;
}
}
@keyframes translate-in {
from {
translate: -100vw 0;
}
}
That’s all you need.
As long as both pages share the same layout and element names, the browser links them and runs the transition automatically. The DOM itself changes instantly, but users see a seamless slide, fade, or animation layered on top.
Browser support is improving fast (around 82%), and the feature degrades gracefully where unsupported. It’s one of the most exciting additions to CSS in years!
Fixing awkward entry animations
Have you ever used a transition and seen a weird “jump” on page load? That’s because CSS didn’t know what the element looked like before it appeared. @starting-style solves that by defining the first frame explicitly.
dialog {
opacity: 0;
translate: 0 -25%;
transition: opacity 0.6s, translate 0.6s;
}
dialog[open] {
opacity: 1;
translate: 0;
@starting-style {
opacity: 0;
translate: 0 -25%;
}
}
Now the dialog fades and slides in smoothly, with no flicker or sudden jump. It’s a small feature but makes animations feel professional.
Browser support is around 87%, and it gracefully degrades if not available.
Here’s a codepen to see it live.
Fluid sizing in one line
You’ve probably seen this one around, but it’s worth repeating. Clamp() lets you set a value that grows with the viewport but stays within limits.
p {
font-size: clamp(1rem, 2vw + 0.5rem, 2rem);
}
That single line replaces a dozen media queries. The text grows on large screens, shrinks on small ones, and never gets unreadable.
You can use the same trick for margins, padding, or any numeric property.
Browser support is nearly universal (~95%), so there’s no reason not to use it.
Mask Effects – Show images through shapes or logos
Masking lets you control which parts of an element are visible using another image as a stencil. It’s like cutting out a shape and letting only what’s inside that shape show through. One simple but striking example is using a silhouette or logo to reveal a background photo underneath.
<div class="banner">
<div class="content">
<h1>Great mask effect</h1>
</div>
</div>
/* The masked artwork lives behind as ::before */
.banner::before {
content: '';
position: absolute;
background-image: url('photo.jpg');
background-size: cover;
background-position: top;
/* Key: reveal the photo only where the PNG is opaque */
-webkit-mask-image: url('mask.png');
mask-image: url('mask.png');
mask-size: cover;
mask-position: center;
}
In this setup, the image fills the entire background, but it’s only visible inside the shape defined by your mask image (for example, a logo, a mascot, or a letterform). You can swap the PNG for any silhouette or SVG, the photo never moves or reflows, the mask simply decides what’s visible. It’s a beautiful trick for hero banners, posters, or highlight sections where you want a bit of visual drama without extra markup or heavy graphics.
Browser support is excellent with almost 96%. But it still need a -webkit- prefix to work properly.
Here’s a codepen to see it live.
Wrapping Up
CSS has grown into a true design language. What used to need JavaScript or external tools is now built directly into the platform, often with better performance and cleaner syntax. If you had stopped keeping up with CSS for a few years, features like :has(), container queries, and cascade layers might feel like superpowers. And the best part is that you can start using almost all of them today. What’s even more exciting is that this list barely scratches the surface. There are dozens of other new features, motion paths, scroll-linked animations, popovers, anchor positioning, and so many more. The language keeps evolving fast, and the best way to keep up is to experiment with these tools as soon as they land. The web finally feels modern again, and it’s written in plain CSS.
About Me
I’m Theo Soti, a front-end developer and designer who loves CSS. I write about web development on theosoti.com, where I share practical tutorials and experiments with new CSS features.
If you enjoyed this article, you might also like my ebook You Don’t Need JavaScript, a practical guide to building modern, interactive websites using only CSS.
Subscribe to our newsletter!

+ There are no comments
Add yours