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 rules
  • mergeRefs() – Flattens multiple ref callbacks or object refs into one
  • devWarn() – 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 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 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:

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 →