Detect JavaScript Support in CSS

Posted on April 20, 2024
Takes about 6 minutes to read

I had been aware of the scripting CSS media feature but I was still under the impression that cross-browser support was lacking. What a pleasant surprise to discover that it has been available in all modern browsers as of December 2023 according to With this feature, we can provide alternative CSS rules depending on whether or not JavaScript is available in the user's browser. It can also help reduce flashes of unstyled content or undesirable layout shifts.

Before we dive in: As exciting as this feature is, I've learned that there are a couple unfortunate gotchas. I've amended the article with an issues section below.


We can progressively enhance our styles:

@media (scripting: enabled) {
  .my-element {
    /* enhanced styles if JS is available */

Or we can gracefully fall back to some alternate styles:

@media (scripting: none) {
  .my-element {
    /* fallback styles when JS is not supported */

There's also an initial-only value, which is for scripting that is enabled during page load but not after. The Media Queries Level 5 W3C Working Draft includes a couple cases where it can be useful.

Examples are printed pages, or pre-rendering network proxies that render a page on a server and send a nearly-static version of the page to the user.

I don't personally imagine using initial-only much, if ever. Although, I'd be interested to find more specific examples of it in practice.

The time before the query

Before this feature, one approach for detecting JavaScript support was by setting a custom selector on the opening html tag—a common one seen in the wild is the no-js class name. If JavaScript is supported and enabled, it removes that selector just prior to rendering page content. When JavaScript is disabled, we can supply alternative styles that adapt to the experience.

<html class="no-js">
  <!-- page content -->
.no-js .my-element {
  /* styles when JS is disabled */

Is this real life?

Imagine a new web campaign is on the cusp of going live and it's time to connect with all the key stakeholders. Everything looks great, most of the team satisfied with the result, but then suddenly some hip marketer in the meeting emphatically requests a complex intro animation on the hero component when the page loads. They gesture wildly as they ask for the main headline to fade in, shrink away as if it were being pulled back on a sling shot, and then... at this point they make an explosion noise with their mouth. "Make it pop!" they decree a mere 24 hours before launch.

Woof. Better get started.

To handle the complexity of this work, we might reach for an animation library such as GSAP. But what does the user see when JavaScript is not available, not to mention if a user's prefers reduced motion setting is enabled? We'll need to consider an alternate version of the hero without all that swooping and scaling.

This media query unlocks the ability to provide CSS rules that are a better fit to the user's experience. In the CodePen demo below, if we disable JavaScript, we'll find that the animation is skipped and the static headline is displayed.

Open CodePen demo

Watch that flash

To really make the intro animation feel smooth on page load, the demo relies on the scripting media query to hide the headline with CSS. By doing so, we won't catch a flash of unstyled text before the GSAP animation is loaded. Also, we only want to hide the headline if JavaScript is available, otherwise it would be hidden for users when it's disabled.

In the following video, watch what happens when the headline is not hidden on page load. The text flashing is even more glaring when throttling on a slower network.

In the video, the headline is no longer hidden on page load to share that pesky flash of unstyled text. When emulating slower network speeds, the issue becomes even more egregious.

Combining queries

In the CSS tab of the demo, notice that the media queries are combined to check both scripting and reduced-motion conditions.

@media (scripting: enabled) and (prefers-reduced-motion: no-preference) {
  /* JS available and motion OK */

@media (scripting: none), (prefers-reduced-motion) {
  /* JS disabled or reduced motion enabled */

Each condition can surely have exclusive styles if the desired outcome calls for it, but it's nice that we can combine them where there's overlap in rulesets.


Updated on April 21st, 2024 - After publishing this post, some feedback surfaced explaining where this media feature unexpectedly fails.

  1. It does not behave as anticipated when a browser extension such as NoScript or uBlock Origin is used to disable page scripts. scripting: enabled still matches even though the extension has JavaScript turned off.
  2. If a script gets blocked or fails to load, a fallback would need to be handled via JavaScript. In the demo above, the fallback would need to tap into the demo's scripting: none media query ruleset so that the static version of the hero is displayed.

Tremendous thanks to Sara, Šime, and Vadim for sharing!

Helpful resources

Back to all blog posts