# Astryx Component Composition Patterns and Exposed Internals: A Complete Guide

> Master Astryx component composition patterns and exposed internals. Discover how Astryx enables flexible UI building with typed props, theming, context, and utility hooks.

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

---

**Astryx components use a layered composition system built on typed props interfaces, StyleX theming, context-driven subcomponents, and exposed utility hooks that enable flexible UI construction while maintaining strict separation between public APIs and internal implementation details.**

The `facebook/astryx` design system provides sophisticated patterns for building reusable, theme-aware React components. Understanding these **Astryx component composition patterns and exposed internals** is essential for advanced customization, testing, and extending the library's capabilities.

## Core Composition Patterns in Astryx

### BaseProps Pattern with Explicit Type Exports

Every Astryx component extends a generic `BaseProps<T>` interface and exports a dedicated props type. This creates a consistent, discoverable API surface while preserving full TypeScript safety for standard HTML attributes.

In [`packages/core/src/Dialog/Dialog.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/Dialog/Dialog.tsx), the component declares:

```typescript
export interface DialogProps extends BaseProps<'dialog'> {
  isOpen: boolean;
  onOpenChange: (open: boolean) => void;
  variant?: 'standard' | 'alert' | 'confirmation';
  purpose?: 'info' | 'success' | 'warning' | 'error';
  width?: number;
  padding?: 0 | 1 | 2 | 3 | 4 | 5;
}

```

The `BaseProps<T>` pattern appears consistently across the codebase—[`ClickableCard.tsx`](https://github.com/facebook/astryx/blob/main/ClickableCard.tsx) exports `ClickableCardProps`, `Divider` exports `DividerProps`, and so on. Consumers gain autocomplete for both component-specific and native HTML attributes without type conflicts.

### Theming via themeProps and StyleX

Astryx generates static, highly performant CSS through **StyleX**. Components invoke `themeProps()` to inject design tokens, then apply styles with `stylex.create` and `stylex.props`.

From [`Dialog.tsx`](https://github.com/facebook/astryx/blob/main/Dialog.tsx):

```typescript
import * as stylex from '@stylexjs/stylex';

const styles = stylex.create({
  dialog: (tokens) => ({
    backgroundColor: tokens.backgroundColor,
    borderRadius: tokens.radiusVars.medium,
    boxShadow: tokens.elevationVars.high,
  }),
});

// Inside the component
const themeTokens = themeProps('dialog', {variant, purpose});

```

The `themeProps()` helper lives in [`packages/core/src/theme/themeProps.ts`](https://github.com/facebook/astryx/blob/main/packages/core/src/theme/themeProps.ts) and maps component names to token bundles defined in [`tokens.stylex.ts`](https://github.com/facebook/astryx/blob/main/tokens.stylex.ts). This decouples visual styling from component logic and enables brand-level theming without code changes.

### Utility Helpers: mergeProps, mergeRefs, devWarn

Shared utilities in `packages/core/src/utils/` enforce consistent behavior across all components:

- **`mergeProps()`** – Combines multiple props objects with specific precedence rules
- **`mergeRefs()`** – Flattens multiple ref callbacks or object refs into one
- **`devWarn()`** – Development-only warnings for improper API usage

Both [`Dialog.tsx`](https://github.com/facebook/astryx/blob/main/Dialog.tsx) and [`ClickableCard.tsx`](https://github.com/facebook/astryx/blob/main/ClickableCard.tsx) import these helpers:

```typescript
import {mergeProps, mergeRefs, devWarn} from '@astryxdesign/utils';

```

Centralizing these operations guarantees identical merging semantics everywhere and simplifies testing.

### Context-Driven Subcomponents

Complex components expose internal context objects that child parts consume. This eliminates props drilling and automatically wires accessibility features.

The **Dialog** pattern demonstrates this clearly:

```typescript
// DialogContext.ts
export interface DialogContextValue {
  titleId: string;
  descriptionId: string;
  onOpenChange: (open: boolean) => void;
}

export const DialogContext = createContext<DialogContextValue | null>(null);

```

[`DialogHeader.tsx`](https://github.com/facebook/astryx/blob/main/DialogHeader.tsx) consumes this context:

```typescript
export function DialogHeader({title, onOpenChange: onOpenChangeProp}: DialogHeaderProps) {
  const context = useContext(DialogContext);
  const titleId = context?.titleId;
  
  // titleId automatically applied for a11y
  return <h2 id={titleId}>{title}</h2>;
}

```

This "slot-like" composition lets consumers nest subcomponents without manually threading IDs or callbacks.

### Slot-Based Layout Components

Many Astryx components accept children structured as layout primitives. The **Layout** component in [`packages/core/src/Layout/Layout.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/Layout/Layout.tsx) formalizes this:

```typescript
export interface LayoutProps {
  header?: ReactNode;
  content?: ReactNode;
  footer?: ReactNode;
}

```

Combined with context-aware children, this creates a declarative composition model:

```tsx
<Dialog isOpen={open} onOpenChange={setOpen}>
  <Layout
    header={<DialogHeader title="Settings" />}
    content={<p>Your preferences</p>}
    footer={<Button label="Save" />}
  />
</Dialog>

```

## Exposed Internals for Advanced Use

Astryx deliberately exposes certain implementation details for power users, testing scenarios, and custom component development.

### Imperative Hooks and Internal APIs

Components needing specialized behavior export hooks that are **not** part of the main UI surface but available for import.

| Hook | Location | Purpose |
|------|----------|---------|
| `useImperativeDialog` | [`Dialog/useImperativeDialog.tsx`](https://github.com/facebook/astryx/blob/main/Dialog/useImperativeDialog.tsx) | Programmatic open/close control |
| `useScrollLock` | [`Dialog/useScrollLock.tsx`](https://github.com/facebook/astryx/blob/main/Dialog/useScrollLock.tsx) | Body scroll management |
| `useClickableContainer` | [`ClickableCard/useClickableContainer.tsx`](https://github.com/facebook/astryx/blob/main/ClickableCard/useClickableContainer.tsx) | Keyboard and click handling for card wrappers |
| `useScheduleContext` | [`Schedule/useScheduleContext.tsx`](https://github.com/facebook/astryx/blob/main/Schedule/useScheduleContext.tsx) | Timeline/scheduling data access |

These follow the naming convention `use{Component}{Capability}` and are co-located with their parent component.

### Component Documentation Files

Every component ships a `*.doc.mjs` file containing a typed `ComponentDoc` object. These power the CLI (`astryx component <Name> --dense`) and Storybook auto-docs.

Example from `packages/core/src/ClickableCard/ClickableCard.doc.mjs`:

```javascript
export default {
  component: 'ClickableCard',
  props: [
    {name: 'label', type: 'string', required: true, description: 'Accessible label for screen readers'},
    {name: 'href', type: 'string', description: 'Navigation target when card is clicked'},
    {name: 'variant', type: "'blue' | 'neutral' | 'subtle'", default: 'neutral'},
    {name: 'elevation', type: "'low' | 'medium' | 'high'", default: 'low'},
  ],
  theming: {
    targets: ['clickable-card'],
    tokens: ['colorVars', 'elevationVars', 'radiusVars'],
  },
};

```

### Variant Maps for Extension

Components can be augmented via exported `VariantMap` types without modifying core source. `DialogVariantMap` allows consumers to register additional variants through the theme system:

```typescript
// In your theme configuration
declare module '@astryxdesign/core/Dialog' {
  interface DialogVariantMap {
    'wizard': {stepCount: number};
  }
}

```

### StyleX Token Bundles

Design tokens in [`packages/core/src/theme/tokens.stylex.ts`](https://github.com/facebook/astryx/blob/main/packages/core/src/theme/tokens.stylex.ts) are public for custom theming:

```typescript
export const colorVars = stylex.defineVars({
  textPrimary: '#1c1b1f',
  textSecondary: '#49454f',
  surface: '#fff',
  // ...
});

export const radiusVars = stylex.defineVars({
  small: '4px',
  medium: '8px',
  large: '16px',
});

```

Components reference these directly, so custom themes can override values at the token level.

## Practical Implementation Examples

### Dialog Composition with Context-Aware Header

```tsx
import {useState} from 'react';
import {Dialog} from '@astryxdesign/core/Dialog';
import {DialogHeader} from '@astryxdesign/core/Dialog/DialogHeader';
import {Layout} from '@astryxdesign/core/Layout';
import {Button} from '@astryxdesign/core/Button';

export function ExampleDialog() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <Button label="Open dialog" onClick={() => setOpen(true)} />
      <Dialog
        isOpen={open}
        onOpenChange={setOpen}
        width={480}
        padding={4}
        variant="standard"
        purpose="info"
      >
        <Layout
          header={<DialogHeader title="User settings" onOpenChange={setOpen} />}
          content={<p>Configure your preferences here.</p>}
          footer={<Button label="Save" onClick={() => setOpen(false)} />}
        />
      </Dialog>
    </>
  );
}

```

The `DialogHeader` automatically receives `titleId` from `DialogContext` for `aria-labelledby` wiring—no manual ID management required.

### ClickableCard with Isolated Nested Interaction

```tsx
import {ClickableCard} from '@astryxdesign/core/ClickableCard';
import {Button} from '@astryxdesign/core/Button';
import {Text} from '@astryxdesign/core/Text';

export function CardWithNestedAction() {
  return (
    <ClickableCard
      label="Open product details"
      href="/products/123"
      variant="blue"
      elevation="low"
    >
      <Text type="heading" level={3}>Wireless Headphones</Text>
      <Text type="body">30 h battery, noise-cancelling.</Text>

      <Button
        label="Add to cart"
        size="small"
        onClick={(e) => {
          e.stopPropagation(); // Card navigation prevented
          console.log('Added to cart');
        }}
      />
    </ClickableCard>
  );
}

```

`useClickableContainer` (internal to [`ClickableCard.tsx`](https://github.com/facebook/astryx/blob/main/ClickableCard.tsx)) handles keyboard activation, focus management, and click event coordination between the card wrapper and nested interactives.

### Imperative Dialog Control

```tsx
import {useRef} from 'react';
import {Dialog, useImperativeDialog} from '@astryxdesign/core/Dialog';

export function ImperativeDialogDemo() {
  const dialogRef = useRef<HTMLDialogElement>(null);
  const {open, close} = useImperativeDialog(dialogRef);

  return (
    <>
      <button onClick={open}>Open imperatively</button>
      <Dialog ref={dialogRef} isOpen={false} onOpenChange={() => {}}>
        <button onClick={close}>Close</button>
      </Dialog>
    </>
  );
}

```

`useImperativeDialog` in [`packages/core/src/Dialog/useImperativeDialog.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/Dialog/useImperativeDialog.tsx) encapsulates the logic for managing the native `<dialog>` element's imperative methods while maintaining state synchronization with React.

## Key Source Files and Their Roles

| File Path | What It Demonstrates |
|-----------|----------------------|
| [`packages/core/src/Dialog/Dialog.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/Dialog/Dialog.tsx) | Full composition pattern: props interface, context provider, StyleX theming |
| [`packages/core/src/Dialog/DialogContext.ts`](https://github.com/facebook/astryx/blob/main/packages/core/src/Dialog/DialogContext.ts) | Context definition for subcomponent communication |
| [`packages/core/src/Dialog/DialogHeader.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/Dialog/DialogHeader.tsx) | Context consumption and accessibility wiring |
| [`packages/core/src/Dialog/useImperativeDialog.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/Dialog/useImperativeDialog.tsx) | Exposed internal hook for programmatic control |
| [`packages/core/src/ClickableCard/ClickableCard.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/ClickableCard/ClickableCard.tsx) | `useClickableContainer` integration with nested interactives |
| `packages/core/src/ClickableCard/ClickableCard.doc.mjs` | ComponentDoc metadata structure |
| [`packages/core/src/theme/themeProps.ts`](https://github.com/facebook/astryx/blob/main/packages/core/src/theme/themeProps.ts) | Theming helper used across all components |
| [`packages/core/src/theme/tokens.stylex.ts`](https://github.com/facebook/astryx/blob/main/packages/core/src/theme/tokens.stylex.ts) | Design token definitions |
| [`packages/core/src/utils/mergeProps.ts`](https://github.com/facebook/astryx/blob/main/packages/core/src/utils/mergeProps.ts) | Props merging with precedence rules |
| [`packages/core/src/utils/mergeRefs.ts`](https://github.com/facebook/astryx/blob/main/packages/core/src/utils/mergeRefs.ts) | Ref flattening utility |
| [`packages/core/src/Layout/Layout.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/Layout/Layout.tsx) | Slot-based container component |

## Summary

- **BaseProps + explicit exports** provide consistent, type-safe component APIs across Astryx
- **StyleX and themeProps** enable static, performant CSS with full design token integration
- **Context-driven subcomponents** eliminate props drilling and automate accessibility wiring
- **Utility helpers** (`mergeProps`, `mergeRefs`, `devWarn`) centralize shared logic in `packages/core/src/utils/`
- **Exposed internals** include imperative hooks (`useImperativeDialog`, `useClickableContainer`), context objects, and `ComponentDoc` metadata files
- **VariantMap types** and **token bundles** allow extension without core modifications

## Frequently Asked Questions

### What makes Astryx components theme-aware?

Components call `themeProps('<component-name>', {variant})` from [`packages/core/src/theme/themeProps.ts`](https://github.com/facebook/astryx/blob/main/packages/core/src/theme/themeProps.ts) to receive context-appropriate design tokens. All styling uses StyleX (`stylex.create`, `stylex.props`) with references to shared token bundles in [`tokens.stylex.ts`](https://github.com/facebook/astryx/blob/main/tokens.stylex.ts). This ensures static CSS generation and consistent visual language across variants.

### How do Astryx components handle nested interactive elements?

Components like `ClickableCard` use the internal `useClickableContainer` hook to coordinate event handling. The hook manages keyboard activation, focus boundaries, and click propagation. Nested elements can call `event.stopPropagation()` to prevent parent activation, as shown in [`ClickableCard.tsx`](https://github.com/facebook/astryx/blob/main/ClickableCard.tsx) at [`packages/core/src/ClickableCard/ClickableCard.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/ClickableCard/ClickableCard.tsx).

### Can I access Astryx component internals for custom behavior?

Yes. Astryx exposes `useImperativeDialog`, `useScrollLock`, `useClickableContainer`, context objects (`DialogContext`, `ThemeContext`), and `ComponentDoc` files. These are not part of the primary import surface but can be imported from component subdirectories for advanced use cases, testing, or building custom variants.

### What is the purpose of `*.doc.mjs` files in Astryx?

Each `ComponentDoc` object in these files describes props, theming targets, and usage patterns. The CLI command `astryx component <Name> --dense` consumes this metadata, and Storybook auto-docs render it for interactive documentation. Source: `packages/core/src/ClickableCard/ClickableCard.doc.mjs`.