Modern CSS Techniques Worth Knowing
Note: This is a test, not an actual blog
The CSS of 2026 barely resembles what we were writing in 2015. Where we once reached for JavaScript to do layout, detect container size, or animate on scroll — CSS now handles these natively. This post covers the techniques I reach for most.
Container Queries
Container queries let you style an element based on the size of its parent, not the viewport. This is the missing piece that makes truly reusable components possible.
.card-container {
container-type: inline-size;
container-name: card;
}
.card {
display: grid;
grid-template-columns: 1fr;
}
@container card (min-width: 400px) {
.card {
grid-template-columns: auto 1fr;
}
}
The card doesn’t know or care about the viewport. It reacts to whatever space it’s placed in. Drop it in a narrow sidebar or a full-width content area — it adapts.
Cascade Layers
Before @layer, specificity was the final arbiter of all style conflicts. This led to specificity wars and the proliferation of !important. Cascade layers give you explicit control over the order of precedence.
@layer reset, base, components, utilities;
@layer reset {
* { box-sizing: border-box; margin: 0; }
}
@layer components {
.button {
background: var(--color-accent);
padding: 0.5rem 1rem;
}
}
/* Utilities always win — no !important needed */
@layer utilities {
.hidden { display: none; }
}
Unlayered styles (written outside any @layer) outrank all layers by default. This means incrementally adopting layers in an existing codebase is safe.
Logical Properties
Logical properties decouple your CSS from physical directions (top, right, bottom, left) and tie it to writing-mode-relative directions (block-start, inline-end, etc.).
/* Instead of this: */
.element {
margin-left: 1rem;
padding-top: 0.5rem;
border-right: 1px solid;
}
/* Write this: */
.element {
margin-inline-start: 1rem;
padding-block-start: 0.5rem;
border-inline-end: 1px solid;
}
For most western-language sites, the output is identical. But your styles now work correctly in RTL layouts without a separate stylesheet.
Scroll-Driven Animations
This is the one that felt like magic when I first used it. Animate elements in sync with scroll position — purely in CSS, no JavaScript, no IntersectionObserver.
@keyframes reveal {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.section {
animation: reveal linear both;
animation-timeline: view();
animation-range: entry 0% entry 30%;
}
The animation-timeline: view() links the animation progress to how much of the element has entered the viewport. The animation-range controls which portion of that journey triggers the animation.
For page-level scroll progress bars, use scroll() instead:
.progress-bar {
transform-origin: left;
animation: grow-progress auto linear;
animation-timeline: scroll(root);
}
@keyframes grow-progress {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
Always pair with @media (prefers-reduced-motion: reduce) to respect user preferences.
Custom Properties with @property
The @property rule lets you register custom properties with a type, initial value, and inheritance behavior. The payoff: you can animate custom properties.
@property --hue {
syntax: '<number>';
initial-value: 120;
inherits: false;
}
.element {
background: hsl(var(--hue), 60%, 50%);
transition: --hue 0.4s ease;
}
.element:hover {
--hue: 200;
}
Without @property, the browser doesn’t know --hue is a number — it can’t interpolate between values. With it, you get smooth animation.
CSS is no longer a secondary concern patched over by JavaScript. It’s a capable, expressive language. These features reward the time spent learning them.