I refactored a React dashboard last week. Removed 140 lines of JavaScript. Added 35 lines of CSS. The app got faster, the code got simpler, and every interaction that previously caused a re-render now runs entirely in the browser's CSS engine.
Modern CSS is not the CSS you learned. Here are five features that are production ready right now and eliminate code you are currently writing in JavaScript.
1. Container Queries Replace Your Resize Observers
You are probably using ResizeObserver or window resize listeners to make components responsive to their container. Stop.
Before (JavaScript):
function StatsWidget() {
const ref = useRef(null)
const [isWide, setIsWide] = useState(false)
useEffect(() => {
const observer = new ResizeObserver(([entry]) => {
setIsWide(entry.contentRect.width > 500)
})
observer.observe(ref.current)
return () => observer.disconnect()
}, [])
return (
<div ref={ref} className={isWide ? "stats-horizontal" : "stats-vertical"}>
{/* content */}
</div>
)
}
After (CSS only):
.widget-slot {
container-type: inline-size;
}
@container (min-width: 500px) {
.stats-widget { display: grid; grid-template-columns: repeat(3, 1fr); }
}
@container (max-width: 499px) {
.stats-widget { display: grid; grid-template-columns: 1fr; }
}
Zero JavaScript. Zero re-renders. Zero useEffect. The browser handles it natively, faster than any JavaScript solution. Works in all major browsers since late 2023.
2. The :has() Selector Replaces Your Conditional State
Before :has(), styling a parent based on its children required JavaScript. You would track state in React and toggle CSS classes manually.
Before (JavaScript):
function Form() {
const [hasError, setHasError] = useState(false)
return (
<form className={hasError ? "form-error" : ""}>
<input
onInvalid={() => setHasError(true)}
onChange={() => setHasError(false)}
/>
</form>
)
}
After (CSS only):
form:has(input:invalid) {
border-color: #ef4444;
background: #fef2f2;
}
.card:has(img) {
grid-template-rows: 200px 1fr;
}
.empty-state:has(~ .list-item) {
display: none;
}
The form gets a red border when any input inside it is invalid. No state. No event handlers. No re-renders. The browser evaluates this in the CSS engine which is orders of magnitude faster than a React state update.
3. CSS Nesting Eliminates Sass for 90% of Projects
If your only reason for using Sass is nesting, you can delete it from your project today.
Native CSS nesting (no preprocessor):
.card {
padding: 1.5rem;
border-radius: 0.75rem;
.title {
font-size: 1.25rem;
font-weight: 600;
}
.description {
color: #6b7280;
line-height: 1.6;
}
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
@media (width < 768px) {
padding: 1rem;
}
}
This is plain CSS. No build step. No PostCSS. No Sass compilation. Rename your .scss files to .css, remove the Sass dependency, and your nesting still works. Supported in all browsers since May 2023.
4. clamp() Replaces Your Responsive Typography Media Queries
If you are writing three media queries to handle font sizes at different breakpoints, you are overcomplicating it.
Before (media queries):
.title { font-size: 1.5rem; }
@media (min-width: 768px) { .title { font-size: 2rem; } }
@media (min-width: 1024px) { .title { font-size: 3rem; } }
After (one line):
.title {
font-size: clamp(1.5rem, 4vw, 3rem);
}
.container {
width: min(90%, 1200px);
margin-inline: auto;
padding: clamp(1rem, 3vw, 2rem);
}
The title scales smoothly from 1.5rem to 3rem based on viewport width. No breakpoints. No jumps. One line. Works everywhere.
5. Cascade Layers End Specificity Wars Forever
Third party CSS overriding your styles? Utility classes losing to component selectors? Cascade layers give you explicit control over which styles win.
@layer base, components, utilities;
@layer base {
h1 { font-size: 2rem; }
a { color: #3b82f6; }
}
@layer components {
.card { padding: 1.5rem; border-radius: 0.75rem; }
.nav-link { color: #1f2937; font-weight: 500; }
}
@layer utilities {
.hidden { display: none; }
.text-center { text-align: center; }
}
A simple .hidden in the utilities layer always beats a complex .card .content .wrapper .text selector in the components layer. Order of layers decides the winner, not specificity math. No more !important. Tailwind v4 uses this internally which is why it plays better with custom CSS than v3 did.
The Quick Migration Checklist
1. Search your codebase for ResizeObserver. Replace with container queries where possible.
2. Search for state variables that only exist to toggle CSS classes. Check if :has() can replace them.
3. If Sass is only used for nesting, remove it and use native CSS nesting.
4. Search for chains of media queries adjusting font-size or padding. Replace with clamp().
5. If you have specificity conflicts, add cascade layers to your global CSS.
Five changes. Less JavaScript. Less dependencies. Faster rendering. The browser's CSS engine does this work faster than React ever will.
More CSS, performance, and architecture patterns at jsgurujobs.com.
Top comments (0)