How to Use `stylex.when.ancestor()` with Hover State Markers in Astryx
stylex.when.ancestor() is Astryx's low-level API for expressing parent-to-child state relationships, allowing child elements to respond when an ancestor is hovered by combining a pseudo-selector with a scoped marker defined via stylex.defineMarker().
Astryx, Meta's experimental styling library, provides a unique pattern for ancestor-dependent styling that differs from traditional CSS selectors. Instead of descendant combinators, you use markers and conditional expressions to create type-safe, scoped relationships between components. This article explains the complete pattern for hover states based on the actual facebook/astryx source code.
The Core Pattern: Markers + Conditional Expressions
Ancestor-based hover styling in Astryx requires three coordinated pieces:
- Define a marker scoped to the component that owns the ancestor element
- Attach the marker to the ancestor via
stylex.props() - Style the child using
stylex.when.ancestor(':hover', marker)with an optional media query guard
This pattern appears throughout the Astryx codebase, particularly in packages/core/src/TabList/TabMenu.tsx and packages/core/src/hooks/containerReveal.pool.stylex.ts.
Step 1: Defining a Scoped Marker
Markers are created with stylex.defineMarker() and should be exported from a dedicated markers file. Per the design rules in CLAUDE.md, markers must be scoped to the component that owns the ancestor element.
// packages/core/src/TabList/tab.markers.stylex.ts
import {stylex} from '@astryxdesign/stylex';
export const tabScope = stylex.defineMarker();
Multiple markers can be defined for complex components. The containerReveal.pool.stylex.ts file demonstrates this with six separate markers (m0 through m5) for a pooled animation system:
// packages/core/src/hooks/containerReveal.pool.stylex.ts
export const m0 = stylex.defineMarker();
export const m1 = stylex.defineMarker();
// ... m2 through m5
Step 2: Attaching the Marker to the Ancestor
The ancestor component must include the marker in its stylex.props() call. This creates the linkage that child components can detect.
// Example: Tab component with marker attached
import {tabScope} from './tab.markers.stylex';
function Tab({children}: Props) {
return (
<button {...stylex.props(tabScope)}>
{children}
</button>
);
}
Without this step, stylex.when.ancestor() has no marker to match against—the child styles will never activate.
Step 3: Styling the Child with Hover Detection
Child components use the computed property syntax with stylex.when.ancestor() to apply styles when the marked ancestor enters a hover state.
// packages/core/src/TabList/TabMenu.tsx
import {tabScope} from './tab.markers.stylex';
export const tabMenuStyle = stylex.create({
root: {
opacity: 0, // default hidden state
[stylex.when.ancestor(':hover', tabScope)]: {
opacity: 1,
},
},
});
The first parameter accepts pseudo-selectors only—.class, [attr], or other complex selectors are not supported per internal/stylex-capabilities/CAPABILITIES.md.
Adding the Hover Media Query Guard
For production-ready hover effects, Astryx supports wrapping styles in @media (hover: hover). This prevents hover states from activating on touch devices where :hover behavior can be erratic.
[stylex.when.ancestor(':hover', tabScope)]: {
'@media (hover: hover)': {
opacity: 1,
},
}
The containerReveal.pool.stylex.ts implementation demonstrates this pattern extensively:
// packages/core/src/hooks/containerReveal.pool.stylex.ts
export const containerRevealStyle = stylex.create({
reveal0: {
[stylex.when.ancestor(':hover', m0)]: {
'@media (hover: hover)': '0s, 0s',
},
[stylex.when.ancestor(':focus-within', m0)]: '0s, 0s',
},
});
Note that the media query guard is automatically emitted when the style value contains an @media (hover: hover) key—no additional configuration is required.
Supported Pseudo-Selectors for Ancestor Conditions
According to internal/stylex-capabilities/CAPABILITIES.md, the following pseudo-selectors work with stylex.when.ancestor():
| Selector | Use Case | Example Source |
|---|---|---|
:hover |
Mouse hover states | containerReveal.pool.stylex.ts |
:focus-within |
Focus contained within ancestor | containerReveal.pool.stylex.ts |
:active |
Active/pressed state | CAPABILITIES.md |
:disabled |
Disabled state | CAPABILITIES.md |
Arbitrary attribute selectors like [data-state="open"] are not supported. Only selectors beginning with : are valid.
Complete Working Example
Here's the full implementation pattern from the Astryx source files:
// === tab.markers.stylex.ts ===
import {stylex} from '@astryxdesign/stylex';
export const tabScope = stylex.defineMarker();
// === Tab.tsx (ancestor component) ===
import {tabScope} from './tab.markers.stylex';
function Tab({children}: Props) {
return <button {...stylex.props(tabScope)}>{children}</button>;
}
// === TabMenu.stylex.ts (child styles) ===
import {tabScope} from './tab.markers.stylex';
export const styles = stylex.create({
menu: {
opacity: 0,
transform: 'translateY(-8px)',
transition: 'opacity 150ms, transform 150ms',
[stylex.when.ancestor(':hover', tabScope)]: {
'@media (hover: hover)': {
opacity: 1,
transform: 'translateY(0)',
},
},
},
});
Multiple Markers and Complex UIs
For components managing multiple ancestor possibilities—like tooltip systems or shared animation pools—define multiple markers and reference them individually:
export const poolStyles = stylex.create({
item0: {
[stylex.when.ancestor(':hover', m0)]: {
'@media (hover: hover)': { /* styles */ },
},
[stylex.when.ancestor(':focus-within', m0)]: { /* styles */ },
},
item1: {
[stylex.when.ancestor(':hover', m1)]: {
'@media (hover: hover)': { /* styles */ },
},
},
// ... item2 through item5
});
Each marker operates independently, allowing precise control over which ancestor triggers which child styles.
Key Limitations to Know
- Selector restriction: Only
:pseudoselectors allowed—no classes, IDs, or attribute selectors - Marker scoping: Markers must be defined in the ancestor's component scope, not the child's
- Runtime dependency: Both ancestor and child must be rendered for the relationship to establish; there's no CSS-only fallback
These constraints enable Astryx's compile-time optimizations and type safety guarantees.
Summary
stylex.when.ancestor(':hover', marker)creates parent-to-child hover relationships without CSS combinators- Markers are defined with
stylex.defineMarker()and attached viastylex.props() - Media query guards (
@media (hover: hover)) prevent hover effects on non-hover-capable devices - Pseudo-selector only:
:hover,:focus-within,:active, and:disabledare supported; attribute selectors are not - Source references:
containerReveal.pool.stylex.tsandTabMenu.tsxdemonstrate production patterns in thefacebook/astryxrepository
Frequently Asked Questions
Can I use stylex.when.ancestor() with class selectors like .active?
No. According to internal/stylex-capabilities/CAPABILITIES.md, only pseudo-selectors beginning with : are supported. You cannot use .class, #id, or [data-attribute] selectors. For component state, use :active or define a separate marker for each state variation.
Why is @media (hover: hover) recommended for hover states?
Touch devices expose inconsistent :hover behavior—elements may remain "hovered" after tap until another element receives focus. The media query guard ensures hover styles only apply on devices with precise pointing devices (mouse, trackpad, stylus). Astryx automatically emits this guard when you include the @media (hover: hover) key in your style object.
How do markers differ from CSS custom properties or data attributes?
Markers are compile-time identifiers, not runtime values. They allow Astryx to generate optimized CSS without selector string escaping issues or global namespace pollution. Unlike data-* attributes, markers are type-checked and scoped to specific component boundaries enforced by the framework.
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 →