DEV Community

JSGuruJobs
JSGuruJobs

Posted on

5 Modern CSS Features That Replace Your JavaScript and Most React Developers Have No Idea They Exist

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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; }
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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; } }
Enter fullscreen mode Exit fullscreen mode

After (one line):

.title {
  font-size: clamp(1.5rem, 4vw, 3rem);
}

.container {
  width: min(90%, 1200px);
  margin-inline: auto;
  padding: clamp(1rem, 3vw, 2rem);
}
Enter fullscreen mode Exit fullscreen mode

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; }
}
Enter fullscreen mode Exit fullscreen mode

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)