Let's be honest — styled-components is broken. Well, perhaps not 'broken' in the sense that it doesn't work, but it's increasingly becoming a solution to a problem that native CSS has already solved. I remember when we absolutely needed JS to handle dynamic themes and complex layouts, but in 2026, the browser has finally caught up.
The Death of the Runtime Overhead
The biggest gripe I've always had with CSS-in-JS is the performance hit of parsing styles at runtime. Now, with CSS Native Nesting and Cascade Layers, we can organise our code just as cleanly without the JS tax.
/* Modern Native Nesting - No preprocessor needed */
.card {
background: var(--surface);
padding: 1.5rem;
& .title {
font-size: 1.25rem;
color: var(--text-primary);
&:hover {
color: var(--brand-colour);
}
}
@media (width > 600px) {
padding: 2rem;
}
}
We can also use Cascade Layers (@layer) to manage specificity, which was one of the primary reasons people reached for CSS-in-JS libraries in the first place.
/* Organising specificity with @layer */
@layer base, components, utilities;
@layer components {
.button {
background: blue;
padding: 0.5rem 1rem;
}
}
@layer utilities {
.m-0 {
margin: 0 !important;
}
}
Dynamic Styling with Typed attr()
One of the 'killer features' of styled-components was passing props to CSS. The new typed attr() function allows us to pull values directly from HTML attributes into our CSS with proper types.
/* Using typed attr() to read data attributes */
.dynamic-box {
background-color: attr(data-bg color, #ccc);
width: attr(data-size length, 100px);
margin: 1rem;
}
This means your React components can remain simple, passing data attributes that the browser interprets directly.
// React Component in 2026
const Box = ({ bg, size }) => (
<div
className="dynamic-box"
data-bg={bg}
data-size={`${size}px`}
/>
);
The Magic of :has() and Container Queries
We used to need complex JS logic to style a parent based on its children. Not anymore. The :has() selector (often called the 'family selector') is a total game changer for design systems.
/* Style the card only if it contains an image */
.card:has(img) {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 1rem;
}
/* Style label if the input is invalid */
.form-group:has(input:invalid) label {
color: var(--error-red);
}
Combine this with Container Queries, and you have components that are truly context-aware without a single line of ResizeObserver logic.
/* Container Queries for truly modular components */
.container-wrapper {
container-type: inline-size;
}
@container (width > 400px) {
.card-content {
display: flex;
flex-direction: row;
font-size: 1.2rem;
}
}
Staggered Animations: sibling-index()
I've lost count of how many times I've mapped over an array in React just to inject an index for an animation delay. The new sibling-index() function handles this natively.
/* Staggered entry animations without JS-injected delays */
.list-item {
opacity: 0;
animation: fadeIn 0.5s ease forwards;
/* Multiply index by delay - no props required! */
animation-delay: calc(sibling-index() * 0.1s);
}
@keyframes fadeIn {
to { opacity: 1; }
}
You can even use sibling-count() to adjust styles based on the total number of items in a list.
/* Adjust font size based on item count */
.menu-item {
font-size: calc(2rem / sibling-count());
}
Scroll-State Queries
This is the kind of thing that makes me love CSS. We can now query the state of a scroll container, such as whether an element is currently snapped.
/* Styling based on scroll snap state */
.carousel-item {
container-type: scroll-state;
transition: scale 0.3s;
}
@container scroll-state(snapped: x) {
.carousel-item {
scale: 1.1;
border: 2px solid var(--primary);
}
}
Browser Support & Fallbacks
While features like Subgrid (97% support) and Container Queries are ready for prime time, some of the newer functions like sibling-index() are still rolling out. Always use @supports for progressive enhancement.
/* Progressive enhancement for typed attr() */
.card {
background: #ccc; /* Fallback */
}
@supports (background: attr(data-bg color)) {
.card {
background: attr(data-bg color);
}
}
Wrapping Up
The era of heavy JS-based styling is coming to an end. By moving our logic back to the browser, we get better performance, smaller bundles, and a much cleaner separation of concerns. Here's the gist:
- Use :has() and Container Queries for layout logic instead of JS state.
- Leverage typed attr() and CSS variables to handle dynamic 'props'.
- Utilise sibling-index() for staggered animations to reduce DOM-heavy calculations.
I'd highly encourage you to try refactoring just one component from your favourite CSS-in-JS library to native CSS. You'll be surprised at how much cleaner it feels! If you want to go deeper and learn how to build real, production-ready CSS design systems step by step, check out my full course here: CSS Design Systems Course
Got questions or want to share how you're using this? Drop me a message on LinkedIn - I always enjoy chatting about this stuff!