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:
DialogHeaderreadsisInlinefromDialogContextwithout requiring a prop.- The close button inside
DialogHeaderinvokesonOpenChange(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
Dialogparents and children viaDialogContext, eliminating prop drilling. - The
isInlineflag controls rendering: Defined inDialogContext.ts, this boolean determines whether the dialog renders as a native<dialog>or inline<div>, affecting child behavior. - Automatic focus management:
DialogHeaderconsumes context viauseDialogContext()to conditionally auto-focus titles for accessibility whenisInlineisfalse. - Minimal public API: Parents only manage
isOpenandonOpenChangeprops, while internal behaviors (focus, styling, inline detection) remain encapsulated. - Documentation synchronization: The repository maintains
.doc.mjsfiles (such asDialog.doc.mjsandDialogHeader.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.tsdefines the context interface anduseDialogContexthook.Dialog.tsximplements the provider and modal logic.DialogHeader.tsxdemonstrates context consumption for accessibility features.Dialog.doc.mjsandDialogHeader.doc.mjsmaintain 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →