# How the Compound Component Pattern Works in Astryx Dialog Components

> Learn how Astryx uses the compound component pattern for Dialog components. Access shared internal state via React context for a stable API.

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

---

**Astryx implements the compound component pattern to let a parent `Dialog` component expose a stable public API while letting its children access shared internal state through a React context.**

The **compound component pattern** in the [facebook/astryx](https://github.com/facebook/astryx) repository enables complex UI components to remain flexible and accessible without prop drilling. By leveraging React Context, Astryx's `Dialog` component shares internal flags—such as the `isInline` rendering mode—with child components like `DialogHeader`, allowing them to automatically adjust behavior for focus management and accessibility. This architecture keeps the public API minimal while encapsulating complex logic within the component tree.

## How the Compound Component Pattern Works in Astryx

The implementation follows a three-step architecture that uses React's Context API to share state between parent and child components.

### Creating the DialogContext

In [`packages/core/src/Dialog/DialogContext.ts`](https://github.com/facebook/astryx/blob/main/packages/core/src/Dialog/DialogContext.ts), Astryx defines a context interface that holds the data children need. The `DialogContextValue` interface contains an `isInline` boolean that indicates whether the dialog renders as a plain `<div>` for documentation or as a native `<dialog>` element.

```tsx
// https://github.com/facebook/astryx/blob/main/packages/core/src/Dialog/DialogContext.ts
export interface DialogContextValue {
  /** Whether the dialog is rendered inline for docs/showcases. */
  isInline: boolean;
}

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

export function useDialogContext() { 
  return use(DialogContext); 
}

```

### Providing Context from the Parent Dialog

The `Dialog` component creates the context value and wraps its children with the provider. In [`packages/core/src/Dialog/Dialog.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/Dialog/Dialog.tsx), the parent memoizes the context value using `useMemo` and passes it to `DialogContext.Provider` inside the internal `innerContent` wrapper.

```tsx
// https://github.com/facebook/astryx/blob/main/packages/core/src/Dialog/Dialog.tsx
const dialogContextValue = useMemo(() => ({ isInline }), [isInline]);

const innerContent = (
  <div …>
    <DialogContext value={dialogContextValue}>{children}</DialogContext>
  </div>
);

```

### Consuming Context in Child Components

Child components like `DialogHeader` consume the context to adapt their behavior. In [`packages/core/src/Dialog/DialogHeader.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/Dialog/DialogHeader.tsx), the component calls `useDialogContext()` to determine whether to auto-focus the title for screen-reader accessibility.

```tsx
// https://github.com/facebook/astryx/blob/main/packages/core/src/Dialog/DialogHeader.tsx
const dialogContext = useDialogContext();
const shouldAutoFocus = dialogContext?.isInline !== true;

useEffect(() => {
  if (shouldAutoFocus && titleRef.current) {
    titleRef.current.focus();
  }
}, [shouldAutoFocus]);

```

By keeping shared state in a context, **any component that lives inside a `Dialog` can behave correctly without the parent passing down props manually**. This makes the API simple while giving children full control over behavior such as focus management, variant styling, and inline rendering.

## Practical Implementation Examples

### Basic Modal Dialog with DialogHeader

The compound component pattern enables clean composition where `DialogHeader` automatically accesses the parent's context without explicit props for inline mode.

```tsx
import {Dialog, DialogHeader} from '@astryxdesign/core';
import {Layout, LayoutContent, LayoutFooter} from '@astryxdesign/core';

function Example() {
  const [open, setOpen] = React.useState(false);

  return (
    <>
      <button onClick={() => setOpen(true)}>Open dialog</button>

      <Dialog isOpen={open} onOpenChange={setOpen}>
        <Layout
          header={
            <DialogHeader
              title="Confirm action"
              onOpenChange={setOpen}
            />
          }
          content={<LayoutContent>Are you sure?</LayoutContent>}
          footer={<LayoutFooter hasDivider>/* buttons */</LayoutFooter>}
        />
      </Dialog>
    </>
  );
}

```

Key implementation details:
- `DialogHeader` reads `isInline` from `DialogContext` without requiring a prop.
- The close button inside `DialogHeader` invokes `onOpenChange(false)` to dismiss the modal.

### Inline Rendering for Documentation

When rendering dialogs inline for Storybook or documentation, the `isInline` flag suppresses modal-specific behaviors like auto-focus and backdrop locking.

```tsx
import {Dialog, DialogHeader} from '@astryxdesign/core';

function InlineDemo() {
  return (
    <Dialog isOpen={true} isInline>
      <DialogHeader title="Inline dialog" />
      {/* Content goes here */}
    </Dialog>
  );
}

```

Because `isInline` is `true`, the component renders a plain `<div>` instead of a `<dialog>` element, and `DialogHeader` suppresses the auto-focus side-effect through the `shouldAutoFocus` guard.

### Extending Dialog with Custom Variants

The pattern supports module augmentation for custom variants without modifying the core library. The `Dialog` component reads the `variant` prop and passes it to `themeProps('dialog', {variant})` to apply the appropriate style sheet.

```ts
// my-theme.d.ts
declare module '@astryxdesign/core/Dialog' {
  interface DialogVariantMap {
    drawer: true;
  }
}

```

Usage with the new variant:

```tsx
<Dialog isOpen={open} onOpenChange={setOpen} variant="drawer">
  {/* … */}
</Dialog>

```

## Summary

- **React Context powers the pattern**: Astryx shares internal state between `Dialog` parents and children via `DialogContext`, eliminating prop drilling.
- **The `isInline` flag controls rendering**: Defined in [`DialogContext.ts`](https://github.com/facebook/astryx/blob/main/DialogContext.ts), this boolean determines whether the dialog renders as a native `<dialog>` or inline `<div>`, affecting child behavior.
- **Automatic focus management**: `DialogHeader` consumes context via `useDialogContext()` to conditionally auto-focus titles for accessibility when `isInline` is `false`.
- **Minimal public API**: Parents only manage `isOpen` and `onOpenChange` props, while internal behaviors (focus, styling, inline detection) remain encapsulated.
- **Documentation synchronization**: The repository maintains `.doc.mjs` files (such as `Dialog.doc.mjs` and `DialogHeader.doc.mjs`) alongside implementation files to keep documentation in sync with code changes.

## Frequently Asked Questions

### How does Astryx handle focus management in compound Dialog components?

Astryx manages focus through the `DialogContext`. When `DialogHeader` detects it is not in inline mode (`isInline !== true`), it automatically focuses the title element via a `useEffect` hook. This ensures screen readers announce the dialog content when the modal opens, while suppressing focus behavior in documentation previews where auto-focus would be disruptive.

### Can I create custom Dialog sub-components that access the context?

Yes. Any component rendered inside a `Dialog` can access the shared state by importing `useDialogContext` from `@astryxdesign/core`. This allows you to build custom headers, footers, or content areas that respond to the dialog's inline state or variant props without requiring the parent `Dialog` to expose additional props.

### What is the difference between inline and modal rendering in Astryx Dialog?

The `isInline` flag controls whether the Dialog renders as a native HTML `<dialog>` element or a plain `<div>`. Inline mode is used for documentation and Storybook showcases where modal behavior—such as backdrop locking and auto-focus—would interfere with the page. The context automatically propagates this flag to all children, allowing them to adjust their behavior accordingly.

### Where are the key files for the Dialog compound component pattern located?

The core files are located in `packages/core/src/Dialog/`:
- [`DialogContext.ts`](https://github.com/facebook/astryx/blob/main/DialogContext.ts) defines the context interface and `useDialogContext` hook.
- [`Dialog.tsx`](https://github.com/facebook/astryx/blob/main/Dialog.tsx) implements the provider and modal logic.
- [`DialogHeader.tsx`](https://github.com/facebook/astryx/blob/main/DialogHeader.tsx) demonstrates context consumption for accessibility features.
- `Dialog.doc.mjs` and `DialogHeader.doc.mjs` maintain documentation that stays synchronized with the implementation.