# Compound Component Patterns in Astryx: Building SelectableCard and MultiSelector

> Discover Astryx's five compound component patterns for building SelectableCard and MultiSelector. Learn state sharing, scoped styling, state selectors, data-driven APIs, and theme overrides.

- Repository: [Meta/astryx](https://github.com/facebook/astryx)
- Tags: deep-dive
- Published: 2026-08-04

---

**Astryx provides five core patterns for building compound components: Context Provider + Hook for state sharing, StyleX marker definitions for scoped styling, the `stylex.when` API for declarative state selectors, a dual Data-Driven/Fallback API, and `defineTheme` overrides for theme customization.**

Astryx, Meta's React component library, delivers a systematic approach to **compound component patterns** that powers cohesive, state-aware UI primitives. This guide examines how `SelectableCard` and `MultiSelector` implement these patterns using context-based state management, StyleX markers, and flexible composition APIs.

---

## Context Provider + Hook Pattern

Every compound component in Astryx ships with a dedicated context file that eliminates prop drilling. The [`DropdownMenuContext.tsx`](https://github.com/facebook/astryx/blob/main/DropdownMenuContext.tsx) file demonstrates the reusable pattern:

```tsx
// From packages/core/src/DropdownMenu/DropdownMenuContext.tsx
const DropdownMenuContext = React.createContext<{
  isOpen: boolean;
  selectedItem: string | null;
  // ... additional state
} | null>(null);

export function useDropdownMenu() {
  const context = useContext(DropdownMenuContext);
  if (!context) {
    throw new Error('useDropdownMenu must be used within DropdownMenu');
  }
  return context;
}

```

Child components consume this hook to read and write shared state. For `SelectableCard`, the internal context propagates selection state to inner parts like the checkmark icon without exposing implementation details through the public API.

---

## StyleX Marker Definitions for Scoped Styling

Astryx uses `stylex.defineMarker()` in component `*.stylex.ts` files to generate scoped CSS markers that map directly to component states.

```typescript
// Pattern used in SelectableCard.stylex.ts
import * as stylex from '@stylexjs/stylex';

export const markers = {
  selected: stylex.defineMarker('selected'),
  disabled: stylex.defineMarker('disabled'),
  hovered: stylex.defineMarker('hovered'),
};

```

These markers create a **clear styling contract** between the component and consuming themes. The `marker:selected` identifier becomes a targetable token that theme authors can override without touching component internals.

---

## The `stylex.when` API for Declarative State Selectors

The `stylex.when` API provides compound-state selectors without CSS nesting. `SelectableCard` leverages this for focus-visible handling:

```typescript
import * as stylex from '@stylexjs/stylex';
import {markers} from './SelectableCard.stylex';

const styles = stylex.create({
  root: {
    borderWidth: 1,
    borderStyle: 'solid',
    borderColor: 'var(--color-border)',
  },
  selected: {
    // Applied when parent has marker:selected
    borderColor: 'var(--color-primary)',
    backgroundColor: 'var(--color-primary-subtle)',
  },
  focusVisible: stylex.when.ancestor(':focus-visible', markers.selected, {
    outline: '2px solid var(--color-focus-ring)',
    outlineOffset: 2,
  }),
});

```

**`stylex.when`** supports three relationship types:
- **`stylex.when.ancestor()`** — parent element states
- **`stylex.when.descendant()`** — child element states  
- **`stylex.when.sibling()`** — adjacent element states

This declarative approach eliminates brittle CSS selectors and ensures style encapsulation.

---

## Data-Driven API with Compound Fallback

`MultiSelector` demonstrates Astryx's **dual-mode API** that supports both rapid data-driven usage and custom JSX composition:

### Data-Driven Mode

```tsx
import {MultiSelector} from '@astryx/core';

export function TagPicker() {
  const [selected, setSelected] = useState<string[]>([]);

  const options = [
    {value: 'react', label: 'React'},
    {value: 'vue', label: 'Vue'},
    {value: 'svelte', label: 'Svelte'},
  ];

  return (
    <MultiSelector
      options={options}
      value={selected}
      onChange={setSelected}
    />
  );
}

```

### Compound Mode (Custom Rendering)

```tsx
export function CustomTagPicker() {
  const [selected, setSelected] = useState<string[]>([]);

  return (
    <MultiSelector value={selected} onChange={setSelected}>
      <MultiSelector.Item value="react">
        <Icon name="react" /> React
      </MultiSelector.Item>
      <MultiSelector.Item value="vue">
        <Badge>Popular</Badge> Vue
      </MultiSelector.Item>
      <MultiSelector.Item value="svelte">
        Svelte <Icon name="external" />
      </MultiSelector.Item>
    </MultiSelector>
  );
}

```

Both modes share the same internal context. The `options` prop is simply a convenience that auto-generates `MultiSelector.Item` children with standard rendering.

---

## `defineTheme` Overrides for Custom Theming

Theme authors target compound component states through the `defineTheme` API without writing additional CSS:

```typescript
import {defineTheme} from '@astryx/core';

const customTheme = defineTheme({
  components: {
    'selectable-card': {
      'marker:selected': {
        backgroundColor: 'var(--brand-surface)',
        borderColor: 'var(--brand-border)',
      },
      'marker:disabled': {
        opacity: 0.5,
        cursor: 'not-allowed',
      },
    },
    'multi-selector': {
      'marker:open': {
        boxShadow: 'var(--shadow-elevated)',
      },
    },
  },
});

```

This pattern creates a **single source of truth** where component states are addressable tokens in the theme system.

---

## Complete SelectableCard Example

```tsx
import {SelectableCard} from '@astryx/core';
import {useState} from 'react';

export function PlanSelector() {
  const [selected, setSelected] = useState(false);

  return (
    <SelectableCard
      label="Pro Plan"
      isSelected={selected}
      onChange={() => setSelected(!selected)}
    >
      <h3>Unlimited Projects</h3>
      <ul>
        <li>Priority support</li>
        <li>Custom integrations</li>
        <li>SSO authentication</li>
      </ul>
      <p>$29/month</p>
    </SelectableCard>
  );
}

```

**Key implementation details from the source:**
- [`SelectableCard.tsx`](https://github.com/facebook/astryx/blob/main/SelectableCard.tsx) handles the context provider setup
- `isSelected` and `onChange` form the controlled component API
- Internal checkmark visibility toggles via the shared context
- Focus and hover states use `stylex.when.ancestor()` from [`SelectableCard.stylex.ts`](https://github.com/facebook/astryx/blob/main/SelectableCard.stylex.ts)

---

## Key Source Files

| Component | File Path | Purpose |
|-----------|-----------|---------|
| SelectableCard | [`packages/core/src/SelectableCard/SelectableCard.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/SelectableCard/SelectableCard.tsx) | Core implementation with context provider |
| SelectableCard | [`packages/core/src/SelectableCard/SelectableCard.stylex.ts`](https://github.com/facebook/astryx/blob/main/packages/core/src/SelectableCard/SelectableCard.stylex.ts) | Marker definitions and `stylex.when` usage |
| MultiSelector | [`packages/core/src/MultiSelector/MultiSelector.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/MultiSelector/MultiSelector.tsx) | Dual-mode API implementation |
| Context Pattern | [`packages/core/src/DropdownMenu/DropdownMenuContext.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/DropdownMenu/DropdownMenuContext.tsx) | Reusable context + hook template |
| Theming | [`packages/core/src/theme/defineTheme.test.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/theme/defineTheme.test.tsx) | Theme override examples |

---

## Summary

- **Context Provider + Hook**: Shares state across compound children without prop drilling, implemented in files like [`DropdownMenuContext.tsx`](https://github.com/facebook/astryx/blob/main/DropdownMenuContext.tsx)
- **StyleX Markers**: `defineMarker()` creates scoped, themeable state tokens in `*.stylex.ts` files
- **`stylex.when` API**: Declarative selectors for ancestor, descendant, and sibling relationships without CSS nesting
- **Dual API Design**: `MultiSelector` supports both `options` prop and explicit `<MultiSelector.Item>` children
- **`defineTheme` Integration**: Component states become overrideable theme tokens via `marker:selected`, `marker:disabled`, etc.

These patterns combine to give Astryx components predictable state management, encapsulated styling, and flexible composition boundaries.

---

## Frequently Asked Questions

### How does Astryx avoid prop drilling in compound components?

Astryx uses a **Context Provider + Hook pattern** where each compound component exports a context file (e.g., [`DropdownMenuContext.tsx`](https://github.com/facebook/astryx/blob/main/DropdownMenuContext.tsx)) that provides a `useDropdownMenu()` hook. Child components call this hook to access shared state directly, eliminating the need to thread props through intermediate layers.

### What is a StyleX marker and why does Astryx use them?

A **StyleX marker** is a scoped CSS identifier created via `stylex.defineMarker()` that represents a component state. Astryx uses markers to create a styling contract between components and themes—states like `marker:selected` become addressable tokens that theme authors can override through `defineTheme()` without modifying component source code.

### Can I use MultiSelector without the options prop?

Yes. `MultiSelector` supports **compound mode** where you render explicit `<MultiSelector.Item>` children instead of passing an `options` array. This enables custom markup, additional UI elements per item, or conditional rendering while maintaining the same internal state management and selection behavior.

### How do I theme the selected state of SelectableCard?

Use `defineTheme` with the component name and marker state as keys: `defineTheme({components: {'selectable-card': {'marker:selected': {backgroundColor: '...'}}}})`. This targets the internal marker without requiring CSS selectors or component prop overrides.