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!

Sources