npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

postcss-pseudo-hover

v1.2.0

Published

PostCSS plugin that extracts and wraps rules containing `:any-hover` pseudo-classes in `@media (any-hover: hover) {}` media queries

Readme

postcss-pseudo-hover

npm GitHub License: MIT

PostCSS plugin that extracts and wraps rules containing :any-hover pseudo-classes in @media (any-hover: hover) {} media queries, then restores :any-hover back to :hover.

/* Before */
.foo:any-hover {
  text-decoration: underline;
}

/* After */
@media (any-hover: hover) {
  .foo:hover {
    text-decoration: underline;
  }
}

Why?

Mobile devices use touchscreens and generally do not have a mouse hover capability. To work around this, mobile browser vendors simulate hover behavior: when a user touches an element, the browser places an invisible cursor on it and applies :hover styles. Even after the user lifts their finger, the hover styles persist until they tap somewhere else on the screen.

This default behavior often looks awkward — a button may remain highlighted long after the user's finger has left the screen. In response, some UX designers have begun advocating to abandon :hover styles entirely in the name of "mobile-first" design. This is an overcorrection. Hover styles are genuinely useful on devices that support them (desktops with mice, laptops with trackpads, tablets with styluses). The problem isn't :hover — it's applying :hover indiscriminately regardless of input capability.

This plugin solves the problem at the CSS level: it wraps hover rules inside @media (any-hover: hover), so browsers will only apply them when the user's device actually supports hovering. On touch-only devices, these styles are never applied, and there is no lingering highlight. On pointer-capable devices, hover styles work as intended. No JavaScript detection, no sniffing user agents — just standard CSS media queries.

Installation

Add postcss-pseudo-hover to your project:

# npm
npm install --save-dev postcss-pseudo-hover

# yarn
yarn add --dev postcss-pseudo-hover

# pnpm
pnpm add --save-dev postcss-pseudo-hover

Usage

Add postcss-pseudo-hover to your PostCSS plugins:

// postcss.config.js
import postcssPseudoHover from "postcss-pseudo-hover";

export default {
  plugins: [postcssPseudoHover()],
};

Then write :any-hover wherever you would normally write :hover:

.button:any-hover {
  background-color: #0056b3;
}

.link:any-hover {
  text-decoration: underline;
}

After processing, the output becomes:

@media (any-hover: hover) {
  .button:hover {
    background-color: #0056b3;
  }
  .link:hover {
    text-decoration: underline;
  }
}

On touch-only devices, browsers ignore the entire block. On pointer-capable devices, hover styles apply normally.

Use it with Stylelint

Stylelint users: If you use Stylelint, you may need to add any-hover (or your custom pseudo-class) to the selector-pseudo-class-no-unknown.ignorePseudoClasses configuration to avoid warnings about unknown pseudo-classes.

Example:

import type { Config } from "stylelint";

export default {
  rules: {
    "selector-pseudo-class-no-unknown": [true, {
      // Put you own selector pseudo class here.
      "ignorePseudoClasses": ["any-hover"],
    }],
  }
} satisfies Config;

Options

selector

Specifies the pseudo-class the plugin should look for and process. The leading colon is optional (:any-hover and any-hover are equivalent).

| Type | Default | |------|---------| | string | ":any-hover" |

Built-in choices:

  • :any-hover (default) — Recommended. Only rules explicitly using :any-hover are processed. Using :hover elsewhere leaves those rules untouched, so you have full control on a per-rule basis. This gives you an "escape hatch" for rare cases where you want :hover to apply unconditionally.
  • :hover — Processes all :hover pseudo-classes globally. Convenient, but there is no way to opt out a specific rule.
  • Custom — Any valid pseudo-class name (e.g. :my-hover, :hover-device). It must be at least one uppercase and lowercase letter, number, underscore, hyphen, and cannot start with a number.

Why not make :hover the default? Because processing every :hover removes your ability to write a rule that applies :hover regardless of device. With :any-hover, you decide exactly which rules go through the media query and which don't.

// Process all :hover selectors
postcssPseudoHover({ selector: ":hover" });

// Use a custom pseudo-class
postcssPseudoHover({ selector: ":my-fancy-hover" });

mediaQuery

Specifies the media query feature that wraps the processed rules. Accepts either a single feature string or an array of feature strings (joined with and).

| Type | Default | |------|---------| | string \| string[] | "any-hover: hover" |

Built-in choices (for individual features):

| Value | Meaning | |-------|---------| | any-hover: hover | The device has at least one input mechanism capable of hovering | | hover: hover | The primary input mechanism is capable of hovering | | any-hover | Same as any-hover: hover, but less future-proof if the spec adds new values | | hover | Same as hover: hover, but less future-proof if the spec adds new values | | any-pointer: fine | The device has at least one input mechanism capable of precise pointing | | pointer: fine | The primary input mechanism is capable of precise pointing | | Custom | Any string else (e.g. a hypothetical future media feature) |

Array usage: When you pass an array, the features are combined with and. For example, ["any-hover: hover", "any-pointer: fine"] produces @media (any-hover: hover) and (any-pointer: fine). This is useful when you want to require multiple conditions to be met simultaneously.

Why not make hover: hover the default? hover: hover only checks the primary input mechanism. Consider a laptop with both a touchscreen and a trackpad — the primary mechanism might be the touchscreen (hover: none), even though the trackpad can hover just fine. any-hover: hover checks all available input mechanisms, which is more accurate for this use case.

// Check only the primary input device
postcssPseudoHover({ mediaQuery: "hover: hover" });

// Use a future-proof or custom media feature
postcssPseudoHover({ mediaQuery: "-very-cool-hover: excellent" });

// Combine multiple features with AND
postcssPseudoHover({ mediaQuery: ["hover: hover", "pointer: fine"] });

escapeMediaQueries

Specifies additional media queries that, if the :any-hover selector is already nested inside, will prevent the plugin from creating a new media query wrapper (avoiding redundant or recursive nesting).

| Type | Default | |------|---------| | string[] | [] |

The following values are always included automatically — you do not need to add them:

  • any-hover: hover
  • hover: hover
  • any-hover
  • hover
  • any-pointer: fine
  • pointer: fine
  • The current value(s) of the mediaQuery option (each array element individually)
// Also skip wrapping when already inside @media (max-width: 768px)
postcssPseudoHover({ escapeMediaQueries: ["max-width: 768px"] });

This is useful when you have your own media query structure and don't want double-wrapping in certain contexts.

withFocusVisible

When enabled, the plugin creates an additional copy of each processed rule outside the media query, with :any-hover replaced by :focus-visible instead of :hover. This allows :hover and :focus-visible to share the same styles, providing keyboard users with a consistent visual experience.

| Type | Default | |------|---------| | boolean | false |

/* Input */
.foo:any-hover {
  text-decoration: underline;
}

/* Output with withFocusVisible: true */
@media (any-hover: hover) {
  .foo:hover {
    text-decoration: underline;
  }
}
.foo:focus-visible {
  text-decoration: underline;
}

Rules that are already inside an escape-listed @media query (see escapeMediaQueries) will not generate a :focus-visible copy — only :any-hover:hover restoration is performed.

postcssPseudoHover({ withFocusVisible: true });

How It Works

  1. The plugin walks each CSS rule and checks if the selector contains the configured pseudo-class (e.g. :any-hover).
  2. If the rule is already nested inside an escape-listed @media query, it simply restores :any-hover:hover without wrapping (and without creating a :focus-visible copy, regardless of withFocusVisible).
  3. Otherwise, it splits selectors into "hover" and "non-hover" groups:
    • Hover selectors are moved into a new @media query (with :any-hover restored to :hover).
    • Non-hover selectors stay in place (the original rule is replaced if only non-hover selectors remain).
  4. If withFocusVisible is enabled, an additional copy of the hover rule is placed outside the media query with :any-hover replaced by :focus-visible.
  5. Nested :any-hover pseudo-classes (e.g. inside :is(), :not(), :has()) are properly handled.

Comparison with Similar Plugins

postcss-hover

This plugin deletes all :hover pseudo-classes entirely, designed for workflows that build separate stylesheets for desktop (with :hover) and mobile (without :hover). This approach:

  • Does not support responsive design — a single page cannot adapt to both touch and pointer input.
  • Increases maintenance burden by requiring separate builds.
  • Is archived (no longer maintained).

postcss-hover-prefix

This plugin removes :hover from selectors and prepends a configurable prefix class as an ancestor selector (e.g. .foo:hover.supports-hover .foo). This is essentially the same strategy as postcss-hover-media-feature's fallback mode, using class-based detection rather than media queries. Like postcss-hover, it does not use media queries and requires JavaScript to toggle the prefix class.

postcss-hover-media-feature

This project's code was partly based on this plugin.

It wraps :hover rules in @media (hover: hover) and does not allow customizing the pseudo-class or media query. It also provides fallback options: when enabled, it will use class-based detection rather than media queries. Refer to the Fallback for legacy browsers section for detailed information.

You can configure postcss-pseudo-hover to behave identically to postcss-hover-media-feature by setting:

postcssPseudoHover({ selector: ":hover", mediaQuery: "hover: hover" });

postcss-plugin-hover

This plugin introduces a custom @hover at-rule. A block inside @hover { ... } behaves like using &:any-hover { ... } with this plugin — the at-rule is converted to a :hover selector wrapped inside @media (hover: hover) and (pointer: fine), and an identical copy is placed outside the media query with :hover replaced by :focus-visible. It offers no customization options.

You can achieve equivalent behavior with postcss-pseudo-hover by setting:

postcssPseudoHover({ mediaQuery: ["hover: hover", "pointer: fine"], withFocusVisible: true });

postcss-any-hover

This plugin wraps :hover rules in @media (any-hover: hover). Unlike postcss-hover-media-feature, it uses any-hover instead of hover, but it still operates on every :hover pseudo-class without allowing customization of the selector or media query. It also provides an alsoApplyToFocusVisible option: when enabled, in addition to moving :hover rules into the media query, it duplicates those rules outside the media query and replaces :hover with :focus-visible, so that keyboard users see the same styles.

You can achieve equivalent behavior with postcss-pseudo-hover by setting:

postcssPseudoHover({ selector: ":hover", mediaQuery: "any-hover: hover", withFocusVisible: alsoApplyToFocusVisible });

Plugin differences

| Feature | postcss-pseudo-hover | postcss-hover-media-feature | postcss-plugin-hover | postcss-any-hover | |---|---|---|---|---| | Default pseudo-class | :any-hover(opt-in per rule) | :hover(all-or-nothing) | @hover(opt-in with custom at-rule) | :hover(all-or-nothing) | | Default media query | any-hover: hover(all inputs) | hover: hover(primary only) | hover: hover(primary only) | any-hover: hover(all inputs) | | Custom pseudo-class | ☑️ | ❎ | N/A | ❎ | | Custom media query | ☑️ | ❎ | ❎ | ❎ | | :focus-visible companion | ☑️ (via with​Focus​Visible) | ❎ | ⚠️ (all-or-nothing) | ☑️ (via also​Apply​To​Focus​Visible) | | Fallback for old browsers | ❎ (see below) | ☑️ | ❎ | ❎ |

Unsupported Features (by Design)

Fallback for legacy browsers

postcss-hover-media-feature and postcss-hover-prefix includes a fallback mechanism for browsers that don't support (hover: hover) or (any-hover: hover) media queries: it uses JavaScript to detect touch support, adds a class like supports-hover or supports-touch to the <html> element, and then rewrites the selectors like below.

.foo:hover {

}
@media (hover: hover) {
  .foo:hover {

  }
}
.supports-hover .foo:hover {

}
html:not(.supports-touch) .foo:hover {

}

These media query features have been supported in all major browsers since December 2018 (see MDN). That is over several years ago. I consider a JavaScript-based fallback for such well-established features unnecessary.

:not() special handling

postcss-hover-media-feature skips processing :hover when it appears inside :not() (e.g. :not(:hover) is left outside the media query). This plugin does not implement that behavior. Since :any-hover is an independent pseudo-class, you control it directly:

  • :not(:any-hover) → gets wrapped in the media query (becomes :not(:hover))
  • :not(:hover) → left untouched

Full control, no magic.

Contradictory nesting

If you write a rule like:

@media (any-hover: none) {
  .foo:any-hover {
    color: red;
  }
}

It will compile to:

@media (any-hover: none) {
  @media (any-hover: hover) {
    .foo:hover {
      color: red;
    }
  }
}

This nested rule can never match (a device cannot simultaneously have any-hover: none and any-hover: hover). Rather than attempting to detect this contradiction, the plugin leaves it as-is. The correct approach is simply to use :hover directly when you intentionally want hover inside an any-hover: none context:

@media (any-hover: none) {
  .foo:hover {
    color: red;
  }
}

License

MIT