How the Astryx Icon System and globalIconRegistry Enable Theme-Aware Icon Management

The Astryx Icon system resolves semantic icon names to SVG React components through a globalIconRegistry that supports theme-specific overrides, library extensions, and universal rendering across server and client environments.

The facebook/astryx repository implements a sophisticated icon management solution that decouples semantic icon names from their visual implementations. At the heart of this system lies the globalIconRegistry, a module-level registry that merges default icons, theme overrides, and dynamically registered entries to provide consistent icon resolution. This architecture enables teams to maintain a single icon API while allowing per-theme customization and third-party library extensions.

Core Architecture of the Astryx Icon System

The Icon Component

The Icon component in packages/core/src/Icon/Icon.tsx serves as the primary interface for consuming icons. It accepts either a semantic string name (such as "close") or a direct React node, ensuring flexibility for both registry-based and ad-hoc icon usage. When receiving a string, the component invokes getIcon() to resolve the name against the current theme's registry, then wraps the result in a StyleX-styled <span> to apply size and color utilities consistently.

// packages/core/src/Icon/Icon.tsx
export function Icon({icon, size = 'md', color = 'primary', ...rest}: IconProps) {
  const theme = useThemeName();
  const resolved = typeof icon === 'string' ? getIcon(icon, theme) : icon;
  // ...render logic with StyleX classes...
}

Type Safety with IconName and ExtendedIconName

Astryx maintains strict type safety through the IconName and ExtendedIconName types. IconName defines the union of built-in semantic names (e.g., close, chevronDown), while ExtendedIconName widens to any string, allowing libraries to register custom keys such as "richtext:bold" without modifying core type definitions or triggering TypeScript errors.

How globalIconRegistry Manages Icon Resolution

Registry Implementation and API

Located in packages/core/src/Icon/globalIconRegistry.tsx, the registry maintains a module-level map named globalRegistry that stores overrides supplied by registerIcons(). The implementation exposes four key utilities: registerIcons for adding entries, getIcon for resolution, getIconRegistry for inspection, and resetIcons for testing isolation.

// packages/core/src/Icon/globalIconRegistry.tsx
export function registerIcons(icons: Partial<Record<ExtendedIconName, ReactNode>>): void {
  warnOnce(...);
  globalRegistry = { ...globalRegistry, ...icons };
}

export function getIcon(name: ExtendedIconName, source?: IconRegistrySource): ReactNode {
  const themeIcons = getThemeIconOverrides(source);
  return themeIcons?.[name as IconName] ?? globalRegistry[name] ?? defaultIcons[name as IconName];
}

Theme-Scoped Icon Registries

Each Astryx theme (e.g., stone, y2k, neutral) exports an IconRegistry object that maps the same semantic names to Lucide-react components. These registries reside in theme-specific files like packages/themes/stone/src/icons.tsx. The getThemeIconOverrides() function loads the appropriate registry based on the current theme name retrieved via useThemeName().

// packages/themes/stone/src/icons.tsx
export const stoneIconRegistry: IconRegistry = {
  close: <X />,
  chevronDown: <ChevronDown />,
  // ...other icons...
};

Resolution Priority and Lookup Order

The getIcon() function implements a strict three-tier cascade. When resolving an icon name, it checks theme overrides first, then falls back to the global registry entries, and finally to the default icons defined in globalIconRegistry.tsx. This priority ensures that themes can override both built-in defaults and library-registered icons without code changes in consuming components.

Runtime Resolution Process

The icon resolution flow operates as follows:

  1. A component renders <Icon icon="close" size="sm" color="primary" />
  2. The Icon component reads the current theme via useThemeName() (e.g., returning "stone")
  3. getIcon("close", "stone") executes the lookup cascade against stoneIconRegistry, then globalRegistry, then defaultIcons
  4. The returned React node (an SVG component) renders inside a styled <span> that provides the requested size and color through StyleX classes

This process works identically in React Server Components (RSCs) and client-side environments because the registry exists at the module level and resolves synchronously.

Extending the Icon System

Library Registration

Third-party libraries can extend the system by calling registerIcons() at initialization, typically in a root layout or provider component. This approach allows packages to contribute icons under namespaced keys like "mylib:custom" without conflicting with core definitions.

Component-Level Overrides

When a component requires a one-off custom icon that should not be globally registered, pass the SVG component directly to the icon prop. This component mode bypasses the registry entirely, skipping the getIcon() lookup while still benefiting from the Icon component's size and color styling utilities.

Summary

  • The globalIconRegistry in packages/core/src/Icon/globalIconRegistry.tsx provides a centralized, module-level store that persists across server and client renders.
  • Resolution follows a strict priority: theme overrides take precedence, followed by globally registered icons (via registerIcons), then default icons.
  • The Icon component in packages/core/src/Icon/Icon.tsx transparently handles both semantic name resolution and direct React node rendering.
  • Type safety is maintained through IconName and ExtendedIconName, enabling extension without modifying core definitions.
  • Each theme exports its own IconRegistry (e.g., packages/themes/stone/src/icons.tsx) to override visual implementations while preserving semantic names.

Frequently Asked Questions

How do I register custom icons in Astryx?

Import registerIcons from packages/core/src/Icon/globalIconRegistry.tsx and call it with an object mapping your custom ExtendedIconName keys to React nodes. Execute this registration at your application's entry point or root layout. The icons become available globally and persist for the lifetime of the application module.

What is the lookup priority for icons in globalIconRegistry?

The resolution order is: first theme-specific overrides (defined via defineTheme({icons})), then globally registered icons (added via registerIcons()), and finally the built-in default icons. This cascade allows themes to override both core defaults and third-party library registrations.

How does Astryx handle server-side rendering with icons?

The registry operates at the module level, making icon mappings immediately available in React Server Components without requiring client-side hydration. The getIcon() function executes synchronously during server rendering, returning the React node directly to the SSR stream.

Can I use icons without registering them in globalIconRegistry?

Yes. The Icon component accepts React nodes directly via the icon prop, bypassing the registry lookup entirely. This component mode is useful for one-off custom icons or when you need to avoid polluting the global namespace while still leveraging the component's size and color styling utilities.

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 →