# How to Customize the Astryx Rich Text Editor Component: A Complete Guide

> Learn how to customize the Astryx Rich Text Editor component using props xstyle nodes plugins and toolbar configuration with RichTextEditorToolbar props and the core icon registry for a tailored user experience.

- Repository: [Meta/astryx](https://github.com/facebook/astryx)
- Tags: how-to-guide
- Published: 2026-08-05

---

**The Astryx Rich Text Editor is customized through props like `xstyle`, `nodes`, and `plugins`, plus toolbar configuration via `RichTextEditorToolbar` props and the core icon registry.**

The **Astryx Rich Text Editor** from `@astryxdesign/lab` provides a composable, Lexical-based editing experience designed for extensibility without forking. This guide covers every customization layer—from visual styling to custom nodes and toolbar injections—based on the actual source implementation in the `facebook/astryx` repository.

---

## Editor Architecture and Extension Points

Understanding the component structure helps you customize effectively. The editor is split into focused modules in `packages/lab/src/RichTextEditor/`:

| Component | File Path | Responsibility |
|-----------|-----------|----------------|
| **Core Editor** | [`RichTextEditor.tsx`](https://github.com/facebook/astryx/blob/main/RichTextEditor.tsx) | `LexicalComposer` wrapper, default node registration, focus handling, value changes, markdown shortcuts |
| **Read-Only View** | [`RichTextView.tsx`](https://github.com/facebook/astryx/blob/main/RichTextView.tsx) | Serialized state renderer without editing affordances |
| **Toolbar** | [`RichTextEditorToolbar.tsx`](https://github.com/facebook/astryx/blob/main/RichTextEditorToolbar.tsx) | Composable formatting bar with configurable buttons |
| **Auto-Link Plugin** | [`RichTextEditorAutoLinkPlugin.tsx`](https://github.com/facebook/astryx/blob/main/RichTextEditorAutoLinkPlugin.tsx) | Optional URL detection |

The editor uses **uncontrolled state**: `defaultValue` is read once at mount, and you persist changes via `onChange` with `editorState.toJSON()`.

---

## Visual Customization: Size, Layout, and Status

Control the editor's appearance through dedicated props without touching internal styles.

### Size and Dimensions

- **`size`**: `'sm' | 'md' | 'lg'` — controls internal padding and wrapper spacing
- **`width`**: number (px) or CSS string — `width={300}` or `width="100%"`

### Custom Layout with `xstyle`

The `xstyle` prop accepts StyleX styles for layout overrides like margins or shadows:

```tsx
import {RichTextEditor} from '@astryxdesign/lab';
import * as stylex from '@stylexjs/stylex';

const customStyles = stylex.create({
  wrapper: {
    borderRadius: '8px',
    boxShadow: '0 0 0 1px var(--color-border)',
    backgroundColor: 'var(--color-bg-primary)',
  },
});

export function StyledEditor() {
  return (
    <RichTextEditor
      label="Styled"
      xstyle={customStyles.wrapper}
    />
  );
}

```

### Status and Validation

The `status` prop combines visual borders with optional messages:

```tsx
<RichTextEditor
  label="Feedback"
  status={{ type: 'error', message: 'Too many characters' }}
  maxLength={500}
/>

```

**`maxLength`** shows a live counter that automatically styles to error when exceeded.

---

## Behavioral Customization: Nodes, Plugins, and Shortcuts

### Registering Custom Lexical Nodes

Pass custom node classes via the **`nodes`** prop. **Critical**: use the same array in **`RichTextView`** for proper serialization:

```tsx
import {
  RichTextEditor,
  RichTextView,
} from '@astryxdesign/lab';
import {MyMentionNode} from './MyMentionNode';

const commonNodes = [MyMentionNode];

export function MentionEditor({initialJSON}) {
  return (
    <>
      <RichTextEditor
        label="Message"
        defaultValue={initialJSON}
        nodes={commonNodes}
        plugins={<MyMentionPlugin />}
      />
      <RichTextView value={initialJSON} nodes={commonNodes} />
    </>
  );
}

```

### Custom Plugins

Any Lexical plugin can be injected via the **`plugins`** prop. The toolbar itself is a plugin:

```tsx
import {
  RichTextEditor,
  RichTextEditorToolbar,
} from '@astryxdesign/lab';

export function ArticleEditor() {
  return (
    <RichTextEditor
      label="Article"
      plugins={<RichTextEditorToolbar />}
    />
  );
}

```

### Markdown Shortcuts

Control markdown typing behavior:

```tsx
// Disable entirely
<RichTextEditor hasMarkdownShortcuts={false} />

// Or supply custom transformers
<RichTextEditor transformers={myCustomTransformers} />

```

---

## Toolbar Customization

`RichTextEditorToolbar` in [`RichTextEditorToolbar.tsx`](https://github.com/facebook/astryx/blob/main/RichTextEditorToolbar.tsx) exposes granular control through props.

### Toolbar Props Reference

| Prop | Type | Purpose |
|------|------|---------|
| `headingLevels` | `Array<1 \| 2 \| 3 \| 4 \| 5 \| 6>` | Which heading buttons appear |
| `hasLink` | `boolean` | Enable/disable link button |
| `promptForUrl` | `(selectedText?: string) => string \| Promise<string>` | Replace `window.prompt` for URL input |
| `linkOpensInNewTab` | `boolean` | Control `target="_blank"` and `rel` attributes |
| `endContent` | `ReactNode` | Inject custom controls after divider |

### Injecting Custom Toolbar Controls

Add an AI button or any custom UI:

```tsx
import {
  RichTextEditor,
  RichTextEditorToolbar,
} from '@astryxdesign/lab';
import {Toolbar, Button} from '@astryxdesign/core';

function AIInsertButton() {
  return (
    <Button
      size="sm"
      onPress={() => alert('AI generated snippet!')}
    >
      AI
    </Button>
  );
}

export function AIEnhancedEditor() {
  return (
    <RichTextEditor
      label="AI-enabled"
      plugins={
        <RichTextEditorToolbar
          endContent={<AIInsertButton />}
        />
      }
    />
  );
}

```

### Disabling Link Button

```tsx
<RichTextEditor
  label="Plain"
  plugins={<RichTextEditorToolbar hasLink={false} />}
/>

```

### Custom Link Prompt Dialog

```tsx
import {Dialog} from '@astryxdesign/core';

function UrlPrompt(selectedText) {
  // Replace with your Dialog component
  return window.prompt('Enter link URL', selectedText);
}

export function CustomLinkEditor() {
  return (
    <RichTextEditor
      label="Links"
      plugins={
        <RichTextEditorToolbar promptForUrl={UrlPrompt} />
      }
    />
  );
}

```

---

## Icon Theming Without Forking

Register custom glyphs through the core icon registry. The toolbar uses `richtext:*` keys:

```tsx
import {registerIcons} from '@astryxdesign/core/Icon';
import {BoldIcon} from './MyIcons';

registerIcons({
  richtext: {bold: <BoldIcon />},
});

// All subsequent toolbars use the custom bold icon

```

See [`RichTextEditorToolbar.tsx`](https://github.com/facebook/astryx/blob/main/RichTextEditorToolbar.tsx) for the full icon key list (`richtext:bold`, `richtext:italic`, `richtext:link`, etc.).

---

## Accessibility Customization

Fine-tune screen reader behavior:

- **`tabEscapeHint`**: Override or suppress the "Escape then Tab" hint
- **`hasAutoFocus`**: Auto-focus on mount

```tsx
<RichTextEditor
  label="Notes"
  hasAutoFocus
  tabEscapeHint="Press Escape then Tab to leave editor"
/>

```

Label, description, and status announcements are wired automatically.

---

## Key Source Files

| File | Purpose |
|------|---------|
| [`packages/lab/src/RichTextEditor/RichTextEditor.tsx`](https://github.com/facebook/astryx/blob/main/packages/lab/src/RichTextEditor/RichTextEditor.tsx) | Core component, props, imperative API |
| [`packages/lab/src/RichTextEditor/RichTextEditorToolbar.tsx`](https://github.com/facebook/astryx/blob/main/packages/lab/src/RichTextEditor/RichTextEditorToolbar.tsx) | Toolbar implementation, icon registry integration |
| [`packages/lab/src/RichTextEditor/RichTextView.tsx`](https://github.com/facebook/astryx/blob/main/packages/lab/src/RichTextEditor/RichTextView.tsx) | Read-only state renderer |
| [`packages/lab/src/RichTextEditor/editorNodes.ts`](https://github.com/facebook/astryx/blob/main/packages/lab/src/RichTextEditor/editorNodes.ts) | Default node list (headings, lists, link, code) |
| [`packages/lab/src/RichTextEditor/editorTheme.ts`](https://github.com/facebook/astryx/blob/main/packages/lab/src/RichTextEditor/editorTheme.ts) | Shared StyleX theme tokens |
| `packages/lab/src/RichTextEditor/RichTextEditor.doc.mjs` | Public API documentation |

---

## Summary

- **Visual styling**: Use `size`, `width`, `xstyle`, and `status` props
- **Custom nodes**: Pass `nodes` array to both `RichTextEditor` and `RichTextView`
- **Toolbar control**: Configure via `RichTextEditorToolbar` props or inject `plugins`
- **Custom controls**: Use `endContent` prop for additional buttons
- **Icon theming**: Override via `registerIcons({ richtext: {...} })`
- **Accessibility**: Adjust `tabEscapeHint` and `hasAutoFocus` as needed

The Astryx Rich Text Editor's composable architecture lets you adapt it to product requirements while maintaining design system consistency and full accessibility support.

---

## Frequently Asked Questions

### How do I add custom formatting buttons to the Astryx Rich Text Editor?

Use the `endContent` prop on `RichTextEditorToolbar` to inject any React element after the standard buttons. This renders after an automatic divider, keeping your custom controls visually separated from built-in formatting options.

### Can I use custom Lexical nodes with the Astryx editor?

Yes. Pass your node classes to the `nodes` prop on both `RichTextEditor` and `RichTextView` to ensure proper serialization and round-tripping. The default nodes are defined in [`editorNodes.ts`](https://github.com/facebook/astryx/blob/main/editorNodes.ts) and merged with your custom array.

### How do I replace the default link prompt with my own dialog?

Provide a `promptForUrl` function to `RichTextEditorToolbar`. This receives the selected text and must return a URL string or Promise. You can integrate any modal system—the example above uses a placeholder `window.prompt` that you'd replace with your `Dialog` component.

### Where are the toolbar icons defined and how do I override them?

Icons are keyed under `richtext:*` in Astryx's core icon registry. Import `registerIcons` from `@astryxdesign/core/Icon` and pass an object mapping keys like `bold`, `italic`, or `link` to your custom components. Changes apply globally to all subsequent toolbar renders.