# How to Use `stylex.when.ancestor()` with Hover State Markers in Astryx

> Learn how to use stylex.when.ancestor() with hover state markers in Astryx. Define parent-child state relationships and create dynamic hover effects for your UI.

- Repository: [Meta/astryx](https://github.com/facebook/astryx)
- Tags: how-to-guide
- Published: 2026-08-05

---

**`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:

1. **Define a marker** scoped to the component that owns the ancestor element
2. **Attach the marker** to the ancestor via `stylex.props()`
3. **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`](https://github.com/facebook/astryx/blob/main/packages/core/src/TabList/TabMenu.tsx) and [`packages/core/src/hooks/containerReveal.pool.stylex.ts`](https://github.com/facebook/astryx/blob/main/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`](https://github.com/facebook/astryx/blob/main/CLAUDE.md), markers must be scoped to the component that owns the ancestor element.

```ts
// 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`](https://github.com/facebook/astryx/blob/main/containerReveal.pool.stylex.ts) file demonstrates this with six separate markers (`m0` through `m5`) for a pooled animation system:

```ts
// 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.

```tsx
// 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.

```tsx
// 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`](https://github.com/facebook/astryx/blob/main/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.

```ts
[stylex.when.ancestor(':hover', tabScope)]: {
  '@media (hover: hover)': {
    opacity: 1,
  },
}

```

The [`containerReveal.pool.stylex.ts`](https://github.com/facebook/astryx/blob/main/containerReveal.pool.stylex.ts) implementation demonstrates this pattern extensively:

```ts
// 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`](https://github.com/facebook/astryx/blob/main/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`](https://github.com/facebook/astryx/blob/main/containerReveal.pool.stylex.ts) |
| `:focus-within` | Focus contained within ancestor | [`containerReveal.pool.stylex.ts`](https://github.com/facebook/astryx/blob/main/containerReveal.pool.stylex.ts) |
| `:active` | Active/pressed state | [`CAPABILITIES.md`](https://github.com/facebook/astryx/blob/main/CAPABILITIES.md) |
| `:disabled` | Disabled state | [`CAPABILITIES.md`](https://github.com/facebook/astryx/blob/main/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:

```ts
// === 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:

```ts
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 `:pseudo` selectors 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 via `stylex.props()`
- **Media query guards** (`@media (hover: hover)`) prevent hover effects on non-hover-capable devices
- **Pseudo-selector only**: `:hover`, `:focus-within`, `:active`, and `:disabled` are supported; attribute selectors are not
- **Source references**: [`containerReveal.pool.stylex.ts`](https://github.com/facebook/astryx/blob/main/containerReveal.pool.stylex.ts) and [`TabMenu.tsx`](https://github.com/facebook/astryx/blob/main/TabMenu.tsx) demonstrate production patterns in the `facebook/astryx` repository

## Frequently Asked Questions

### Can I use `stylex.when.ancestor()` with class selectors like `.active`?

No. According to [`internal/stylex-capabilities/CAPABILITIES.md`](https://github.com/facebook/astryx/blob/main/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.