How the @media (hover: hover) Guard Works for Hover Styles in Astryx
Astryx wraps every :hover style declaration inside an @media (hover: hover) media query to ensure hover effects only activate on devices that support true hovering, preventing confusing visual feedback on touchscreens.
The @media (hover: hover) guard is a defensive CSS pattern implemented throughout the facebook/astryx repository to conditionally render hover states exclusively on mouse-capable devices. By nesting this media feature within :hover pseudo-classes using StyleX, the design system prevents unwanted overlays and background changes on smartphones and tablets while maintaining rich desktop interactions.
What Is the @media (hover: hover) Guard?
The @media (hover: hover) CSS media feature evaluates to true only on devices with a primary pointing mechanism that supports hovering, such as a mouse or trackpad. On touch-only devices, this feature evaluates to false, causing browsers to ignore any rules nested inside the query. Astryx leverages this behavior to ensure that hover-specific visual effects—like background color changes or underlines—do not appear on mobile devices where users cannot hover, only tap.
How Astryx Implements the Guard
Astryx builds its styling system on StyleX, which allows JavaScript objects to define complex CSS rules. The guard is implemented by defining a reusable constant and nesting it inside :hover declarations:
// From packages/core/src/CommandPalette/CommandPaletteItem.tsx
const HOVER_HOVER = '@media (hover: hover)';
const styles = stylex.create({
itemHover: {
':hover': {
[HOVER_HOVER]: {
backgroundColor: colorVars['--color-overlay-hover'],
},
},
':active': {
backgroundColor: colorVars['--color-overlay-pressed'],
},
},
});
When StyleX compiles this object, it generates CSS that nests the media query inside the hover selector:
.item:hover {
@media (hover: hover) {
background-color: var(--color-overlay-hover);
}
}
.item:active {
background-color: var(--color-overlay-pressed);
}
Because the @media block is nested within :hover, browsers only apply the background color change when both conditions are met: the user is hovering and the device supports hover.
Where the Pattern Appears in the Source Code
The HOVER_HOVER guard appears consistently across Astryx core components:
- CommandPaletteItem:
packages/core/src/CommandPalette/CommandPaletteItem.tsx(lines 29-55) defines the constant and demonstrates the nested structure. - Button:
packages/core/src/Button/Button.tsx(lines 77-84) applies the guard to all button variants, ensuring hover overlays only appear on desktop. - TopNavItem:
packages/core/src/TopNav/TopNavItem.tsx(lines 65-70) uses the pattern for navigation item hover states. - Link:
packages/core/src/Link/Link.tsx(lines 62-68) wraps underline transitions in the guard to prevent persistent underlines on mobile taps.
Benefits of the Guard
Implementing the @media (hover: hover) guard provides several advantages:
- Accessibility: Touch users avoid confusing "hover" cues they cannot trigger, reducing cognitive load and interface ambiguity.
- Performance: Browsers skip the nested rules on devices that do not support hover, slightly reducing style calculation overhead.
- Design Consistency: Hover effects automatically disable on mobile without requiring device detection logic in individual components.
- Future-Proofing: Convertible laptops or stylus-enabled tablets that advertise hover capability correctly receive hover effects when in laptop mode.
How StyleX Compiles the Guard
Under the hood, StyleX processes the stylex.create call through three stages:
- Object Parsing: StyleX identifies the
:hoverkey and its nested object containing the[HOVER_HOVER]dynamic key. - Rule Generation: The compiler emits a CSS rule that nests the
@mediablock inside the:hoverpseudo-class selector. - Runtime Injection: The final CSS is injected into the document (or extracted during build), where browsers evaluate the media feature at viewport initialization.
This compilation step ensures the guard applies consistently across all components without runtime JavaScript overhead.
Implementing the Guard in Custom Components
To add hover-safe styles to your own Astryx components, import the constant and nest it within your :hover declarations:
import * as stylex from '@stylexjs/stylex';
import {colorVars, spacingVars} from '../theme/tokens.stylex';
const HOVER_HOVER = '@media (hover: hover)';
const styles = stylex.create({
card: {
padding: spacingVars['--spacing-3'],
borderRadius: '4px',
backgroundColor: 'transparent',
cursor: 'pointer',
},
cardHover: {
':hover': {
[HOVER_HOVER]: {
backgroundColor: colorVars['--color-overlay-hover'],
},
},
},
});
export function Card({children}: {children: React.ReactNode}) {
return (
<div {...stylex.props(styles.card, styles.cardHover)}>
{children}
</div>
);
}
This approach ensures the hover background appears on desktops but remains disabled on smartphones, where only the :active state (if defined) would trigger.
Summary
- Astryx uses the
@media (hover: hover)guard to conditionally render hover effects only on devices that support true hovering. - The pattern is implemented via StyleX by nesting the media query inside
:hoverpseudo-classes using theHOVER_HOVERconstant. - Key files implementing this guard include
CommandPaletteItem.tsx,Button.tsx,TopNavItem.tsx, andLink.tsx. - This approach improves accessibility, performance, and cross-device consistency without requiring component-level device detection.
Frequently Asked Questions
What happens if I omit the @media (hover: hover) guard in Astryx?
Without the guard, hover styles will apply on all devices, including smartphones and tablets. On touch devices, this can cause the hover state to "stick" after a tap or appear when scrolling, creating a confusing user experience where visual feedback persists incorrectly.
How does StyleX handle nested media queries inside pseudo-classes?
StyleX treats object keys starting with @media as media query rules and correctly nests them within the parent selector—in this case, :hover. During compilation, StyleX transforms the nested JavaScript object into valid CSS where the @media block resides inside the pseudo-class, ensuring proper specificity and cascade behavior.
Does the @media (hover: hover) guard affect keyboard navigation or focus states?
No, the guard only affects the :hover pseudo-class. Focus states (triggered by keyboard navigation) are separate and typically use :focus or :focus-visible selectors. Astryx components often define distinct focus styles outside the hover guard to ensure keyboard users receive appropriate visual feedback regardless of device capabilities.
Which Astryx components use this guard?
The pattern appears consistently across interactive components in packages/core/src/, including CommandPaletteItem, Button, TopNavItem, and Link. Any component that displays visual feedback on mouse hover—such as background color changes, underlines, or scale transforms—implements this guard to prevent those effects on touch-only devices.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →