How the Compound Component Pattern Works in Astryx Dialog Components

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 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, 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.

// 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, the parent memoizes the context value using useMemo and passes it to DialogContext.Provider inside the internal innerContent wrapper.

// 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, the component calls useDialogContext() to determine whether to auto-focus the title for screen-reader accessibility.

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

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.

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.

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

Usage with the new variant:

<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, 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 defines the context interface and useDialogContext hook.
  • Dialog.tsx implements the provider and modal logic.
  • DialogHeader.tsx demonstrates context consumption for accessibility features.
  • Dialog.doc.mjs and DialogHeader.doc.mjs maintain documentation that stays synchronized with the implementation.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →