I built a scanner to find JavaScript that modern CSS can replace

작성자

카테고리:

← 피드로
DEV Community · Theo · 2026-08-11 개발(SW)

I spend a lot of time writing about CSS features that replace small pieces of JavaScript. The examples are usually easy to understand in isolation. Finding the same opportunity inside a real website is much harder.

A dropdown may be buried in a minified bundle. A modal may be a fixed <div> with a home-made focus trap. A reveal animation may only exist after you scroll halfway down the page.

I wanted to paste in a URL and get a useful answer to one question:

Which parts of this page could use native HTML or modern CSS instead?

So I built CSS Radar.

A class name is not evidence

My first concern was false positives.

Finding .modal in a stylesheet proves very little. It might be an image, a legacy rule that no longer matches anything, or a non-modal panel with an unfortunate name. The same problem applies to classes like .dropdown, .accordion, and .reveal.

The scanner renders one public page in Chromium. It scrolls through the whole page, hovers controls, and clicks interactions that it can test safely. Form submissions, navigation, and downloads are blocked.

It also collects the relevant HTML, CSS, and JavaScript around each candidate. A finding is only published when the captured evidence matches a replacement the scanner knows how to verify.

That makes it conservative. I prefer a missed opportunity to a report full of guesses.

A custom modal can become <dialog>

Here is a familiar modal setup:

<div class="modal" role="dialog" aria-modal="true" hidden>
  <button class="modal__close">Close</button>
  <p>Modal content</p>
</div>

Enter fullscreen mode Exit fullscreen mode

trigger.addEventListener('click', () => {
  modal.hidden = false;
  trapFocus(modal);
});

Enter fullscreen mode Exit fullscreen mode

The code is not necessarily bad. It just rebuilds behavior the browser now provides.

<button command="show-modal" commandfor="settings-dialog">
  Open settings
</button>

<dialog id="settings-dialog">
  <p>Modal content</p>
  <button command="close" commandfor="settings-dialog">
    Close
  </button>
</dialog>

Enter fullscreen mode Exit fullscreen mode

<dialog> gives you modal semantics, the top layer, focus handling, Escape dismissal, and ::backdrop. The newer command and commandfor attributes can even connect the buttons without a click listener in supporting browsers.

This is the kind of finding CSS Radar can verify from several pieces of evidence: aria-modal="true", the fixed overlay, its hidden state, and the code that opens it.

A class-toggled menu can become a popover

This pattern is everywhere:

<div class="language-selector">
  <div class="selector-button">Language</div>
  <div class="language-menu">
    <a href="/en">English</a>
    <a href="/fr">French</a>
  </div>
</div>

Enter fullscreen mode Exit fullscreen mode

selector.addEventListener('click', () => {
  selector.classList.toggle('open');
});

Enter fullscreen mode Exit fullscreen mode

.language-menu {
  display: none;
  position: absolute;
}

.language-selector.open .language-menu {
  display: block;
}

Enter fullscreen mode Exit fullscreen mode

A modern version can use a real button and the Popover API:

<button popovertarget="language-menu">Language</button>

<nav id="language-menu" popover>
  <a href="/en">English</a>
  <a href="/fr">French</a>
</nav>

Enter fullscreen mode Exit fullscreen mode

Popover handles opening, Escape, light dismissal, and the top layer. CSS Anchor Positioning can attach the menu to its button when you need more control over placement.

The awkward part for a scanner is that real sites do not always use buttons. Sometimes the trigger is a <div> with a click listener. Clicking every <div> on a page would be reckless, so CSS Radar only tests a non-semantic trigger when the surrounding structure or behavior gives it a concrete reason.

That rule came from debugging a missed language selector. The scanner saw the open class and initially treated it like an accordion. Watching the menu become an out-of-flow panel made the correct replacement much clearer.

Some scroll reveals no longer need an observer

Another common pattern uses IntersectionObserver to add a class when an element enters the viewport:

const observer = new IntersectionObserver((entries) => {
  for (const entry of entries) {
    if (entry.isIntersecting) {
      entry.target.classList.add('is-visible');
    }
  }
});

document.querySelectorAll('.reveal').forEach((element) => {
  observer.observe(element);
});

Enter fullscreen mode Exit fullscreen mode

For a decorative entry animation, a view timeline can often do the job in CSS:

.reveal {
  animation: reveal linear both;
  animation-timeline: view();
  animation-range: entry 0% cover 30%;
}

@keyframes reveal {
  from {
    opacity: 0;
    translate: 0 2rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  .reveal {
    animation: none;
  }
}

Enter fullscreen mode Exit fullscreen mode

This recommendation only makes sense when the observer controls presentation. If the callback loads data, changes application state, or records a business event, CSS is not a replacement.

That difference is why the scanner looks for both the viewport observation and a matching visual mutation on the same element.

What the report contains

The report does not rewrite the website. I do not think an automated tool should silently replace interaction code after spotting one pattern.

Instead, each finding shows:

  • the element or selector that matched
  • the captured code that drives it
  • a possible native replacement
  • current browser support
  • what to verify before shipping the change

When I have a matching example from my book, I also link to the CodePen. That gives you something working to inspect before touching production code.

The scanner also catches smaller CSS opportunities such as paired physical spacing declarations that can become logical properties. Repeated occurrences are grouped into one finding because six cards that all say padding-top plus padding-bottom should become padding-block are not six different ideas.

What it cannot tell you

CSS Radar scans one public page. It cannot enter an account or reach a component hidden behind an application-specific workflow. A component may also appear on another route and never exist on the page you submit.

It is not a security, performance, or accessibility audit. Native HTML often removes custom code and gives the browser more responsibility, but that does not make every replacement automatically correct.

You still need to test keyboard behavior, older browsers, analytics hooks, and any state tied to the current implementation. The report is a review list, not a merge button.

If you have an old page with a suspicious amount of UI glue, you can run a free audit on CSS Radar. I am especially interested in the patterns it misses. Those failures have been more useful than the easy successes.

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다