Astryx Component Composition Patterns and Exposed Internals: A Complete Guide
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, the component declares:
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 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:
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 and maps component names to token bundles defined in 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 rulesmergeRefs()– Flattens multiple ref callbacks or object refs into onedevWarn()– Development-only warnings for improper API usage
Both Dialog.tsx and ClickableCard.tsx import these helpers:
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:
// DialogContext.ts
export interface DialogContextValue {
titleId: string;
descriptionId: string;
onOpenChange: (open: boolean) => void;
}
export const DialogContext = createContext<DialogContextValue | null>(null);
DialogHeader.tsx consumes this context:
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 formalizes this:
export interface LayoutProps {
header?: ReactNode;
content?: ReactNode;
footer?: ReactNode;
}
Combined with context-aware children, this creates a declarative composition model:
<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 |
Programmatic open/close control |
useScrollLock |
Dialog/useScrollLock.tsx |
Body scroll management |
useClickableContainer |
ClickableCard/useClickableContainer.tsx |
Keyboard and click handling for card wrappers |
useScheduleContext |
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:
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:
// 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 are public for custom theming:
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
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
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) handles keyboard activation, focus management, and click event coordination between the card wrapper and nested interactives.
Imperative Dialog Control
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 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 |
Full composition pattern: props interface, context provider, StyleX theming |
packages/core/src/Dialog/DialogContext.ts |
Context definition for subcomponent communication |
packages/core/src/Dialog/DialogHeader.tsx |
Context consumption and accessibility wiring |
packages/core/src/Dialog/useImperativeDialog.tsx |
Exposed internal hook for programmatic control |
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 |
Theming helper used across all components |
packages/core/src/theme/tokens.stylex.ts |
Design token definitions |
packages/core/src/utils/mergeProps.ts |
Props merging with precedence rules |
packages/core/src/utils/mergeRefs.ts |
Ref flattening utility |
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 inpackages/core/src/utils/ - Exposed internals include imperative hooks (
useImperativeDialog,useClickableContainer), context objects, andComponentDocmetadata 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 to receive context-appropriate design tokens. All styling uses StyleX (stylex.create, stylex.props) with references to shared token bundles in 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 at 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.
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 →