mirror of
https://github.com/resend/react-email.git
synced 2026-09-14 14:18:02 +08:00
437 lines
14 KiB
Plaintext
437 lines
14 KiB
Plaintext
---
|
|
title: "Inspector"
|
|
sidebarTitle: "Inspector"
|
|
description: "Add a contextual sidebar for document, node, and text editing."
|
|
icon: "sidebar"
|
|
---
|
|
|
|
The Inspector is a sidebar that provides a way to inspect and edit the email's properties. From the Inspector, you can edit document, node, and text properties such as colors, fonts, and padding.
|
|
|
|
View a minimal example of the Inspector at the [Standalone Editor Inspector](https://react.email/editor/examples/standalone-editor-inspector) demo page.
|
|
|
|
## Quick start
|
|
|
|
Add [EmailTheming](/editor/api-reference/theming-api) and render
|
|
`Inspector.Root` next to the editor content. The inspector switches
|
|
automatically between document, node, and text controls based on the current
|
|
selection.
|
|
|
|
```tsx
|
|
import { StarterKit } from '@react-email/editor/extensions';
|
|
import { EmailTheming } from '@react-email/editor/plugins';
|
|
import { Inspector } from '@react-email/editor/ui';
|
|
import { EditorContent, EditorContext, useEditor } from '@tiptap/react';
|
|
import '@react-email/editor/themes/default.css';
|
|
|
|
const extensions = [StarterKit, EmailTheming];
|
|
|
|
export function MyEditor() {
|
|
const editor = useEditor({
|
|
extensions,
|
|
content,
|
|
});
|
|
|
|
return (
|
|
<EditorContext.Provider value={{ editor }}>
|
|
<div className="flex min-h-0 overflow-hidden">
|
|
<div className="flex-1 min-w-0 p-4">
|
|
<EditorContent editor={editor} />
|
|
</div>
|
|
|
|
<Inspector.Root className="w-60 shrink-0 border-l p-4">
|
|
<Inspector.Breadcrumb />
|
|
<Inspector.Document />
|
|
<Inspector.Node />
|
|
<Inspector.Text />
|
|
</Inspector.Root>
|
|
</div>
|
|
</EditorContext.Provider>
|
|
);
|
|
}
|
|
```
|
|
|
|
Click the editor background to edit document-level styles. Click a node, such as a button, to edit its properties. Select text to switch to text controls.
|
|
|
|
<Tip>
|
|
You can [customize the inspector](#customizing-the-inspector) with render props. The default panels can also be styled using [CSS variables and `data-re-*` selectors](/editor/features/styling#inspector).
|
|
</Tip>
|
|
|
|
## Using with EmailEditor
|
|
|
|
If you use the standalone `EmailEditor` component, pass the inspector as a child instead of setting up `EditorProvider` manually.
|
|
|
|
```tsx
|
|
import { EmailEditor } from '@react-email/editor';
|
|
import { Inspector } from '@react-email/editor/ui';
|
|
|
|
export function MyEditor() {
|
|
return (
|
|
<div className="flex" style={{ height: '600px' }}>
|
|
<EmailEditor
|
|
content="<h1>Hello</h1><p>Click any element to inspect it.</p>"
|
|
className="flex-1 min-w-0 overflow-y-auto p-4"
|
|
>
|
|
<Inspector.Root className="w-60 shrink-0 border-l p-4 overflow-y-auto">
|
|
<Inspector.Breadcrumb />
|
|
<Inspector.Document />
|
|
<Inspector.Node />
|
|
<Inspector.Text />
|
|
</Inspector.Root>
|
|
</EmailEditor>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
`EmailEditor` includes `EmailTheming` in its default extensions, so the
|
|
[required extension](#required-extension) is already covered.
|
|
|
|
<Tip>
|
|
Children of `EmailEditor` render inside the Tiptap `EditorProvider` as DOM
|
|
siblings of the editor content area, so flex layouts work naturally.
|
|
</Tip>
|
|
|
|
## Required extension
|
|
|
|
`Inspector.Root` requires the [EmailTheming](/editor/api-reference/theming-api)
|
|
extension. Without it, the inspector cannot resolve theme defaults or apply
|
|
document-level style changes, and it will error.
|
|
|
|
```tsx
|
|
import { StarterKit } from '@react-email/editor/extensions';
|
|
import { EmailTheming } from '@react-email/editor/plugins';
|
|
|
|
const extensions = [StarterKit, EmailTheming];
|
|
```
|
|
|
|
## Zero-config defaults
|
|
|
|
When you render the three default panels without children, the inspector gives
|
|
you a ready-made sidebar with sensible controls for common editing tasks using
|
|
the standard HTML elements like input and select.
|
|
|
|
| Component | When it appears | Default behavior |
|
|
| -- | -- | -- |
|
|
| `Inspector.Document` | When the editor is not focused on a specific node or text selection | Controls global theme-backed styles like page background and container settings |
|
|
| `Inspector.Node` | When a node is focused or selected | Adapts to the current node type and shows sections like attributes, size, typography, padding, border, and background |
|
|
| `Inspector.Text` | When text is selected | Shows text formatting, alignment, typography, and link color controls |
|
|
|
|
This gives you a complete inspector with no custom UI code required.
|
|
|
|
## Breadcrumb navigation
|
|
|
|
Use `Inspector.Breadcrumb` to show the current path from the document root down
|
|
to the focused node, and let users jump back up the hierarchy.
|
|
|
|
```tsx
|
|
<Inspector.Root>
|
|
<Inspector.Breadcrumb>
|
|
{(segments) =>
|
|
segments.map((segment, index) => (
|
|
<button key={index} type="button" onClick={() => segment.focus()}>
|
|
{segment.node.nodeType}
|
|
</button>
|
|
))
|
|
}
|
|
</Inspector.Breadcrumb>
|
|
|
|
<Inspector.Document />
|
|
<Inspector.Node />
|
|
<Inspector.Text />
|
|
</Inspector.Root>
|
|
```
|
|
|
|
To use the default breadcrumb rendering:
|
|
|
|
```tsx
|
|
<Inspector.Root>
|
|
<Inspector.Breadcrumb/>
|
|
<Inspector.Document />
|
|
<Inspector.Node />
|
|
<Inspector.Text />
|
|
</Inspector.Root>
|
|
```
|
|
|
|
## Customizing the Inspector
|
|
|
|
The Inspector.Root itself keeps track of what is meant to be inspected. This can either be the document, a node, or a text selection.
|
|
|
|
The `<Inspector.Document>`, `<Inspector.Node>`, and `<Inspector.Text>` components
|
|
are convenience components that only render when their respective target is active.
|
|
|
|
### Inspector.Document
|
|
|
|
```tsx
|
|
<Inspector.Root>
|
|
<Inspector.Document>
|
|
{({ findStyleValue, setGlobalStyle, batchSetGlobalStyle }) => (
|
|
<input
|
|
type="color"
|
|
value={findStyleValue('body', 'backgroundColor')}
|
|
onChange={(e) =>
|
|
setGlobalStyle('body', 'backgroundColor', e.target.value)
|
|
}
|
|
/>
|
|
)}
|
|
</Inspector.Document>
|
|
</Inspector.Root>
|
|
```
|
|
|
|
<ResponseField name="findStyleValue" type="(classReference, property) => string | number">
|
|
Looks up the current value for a document-level style property.
|
|
</ResponseField>
|
|
|
|
<ResponseField name="setGlobalStyle" type="(classReference, property, value) => void">
|
|
Updates an existing style entry or adds a new one.
|
|
</ResponseField>
|
|
|
|
<ResponseField name="batchSetGlobalStyle" type="(changes) => void">
|
|
Applies multiple document-level style updates in one call.
|
|
</ResponseField>
|
|
|
|
Internally, [EmailTheming](/editor/api-reference/theming-api) keeps track of the document-level styles through a
|
|
large JSON array with entries for each selector and style property.
|
|
|
|
### Inspector.Node
|
|
|
|
```tsx
|
|
<Inspector.Root>
|
|
<Inspector.Node>
|
|
{({
|
|
nodeType,
|
|
getStyle,
|
|
setStyle,
|
|
getAttr,
|
|
setAttr,
|
|
themeDefaults,
|
|
presetColors
|
|
}) => (
|
|
<>
|
|
<div>{nodeType}</div>
|
|
<input
|
|
type="text"
|
|
value={String(getAttr('alt') ?? '')}
|
|
onChange={(e) => setAttr('alt', e.target.value)}
|
|
/>
|
|
<input
|
|
type="color"
|
|
value={String(getStyle('backgroundColor') ?? '')}
|
|
onChange={(e) => setStyle('backgroundColor', e.target.value)}
|
|
/>
|
|
</>
|
|
)}
|
|
</Inspector.Node>
|
|
</Inspector.Root>
|
|
```
|
|
|
|
<ResponseField name="nodeType" type="string">
|
|
The currently focused node type, such as `image`, `button`, or `section`.
|
|
</ResponseField>
|
|
|
|
<ResponseField name="getStyle" type="(prop) => string | number | undefined">
|
|
Reads a resolved style value from the active node.
|
|
</ResponseField>
|
|
|
|
<ResponseField name="setStyle" type="(prop, value) => void">
|
|
Updates a single inline style on the active node.
|
|
</ResponseField>
|
|
|
|
<ResponseField name="batchSetStyle" type="(changes) => void">
|
|
Applies multiple style updates in one call.
|
|
</ResponseField>
|
|
|
|
<ResponseField name="getAttr" type="(name) => unknown">
|
|
Reads a node attribute such as `alt`, `href`, or `width`.
|
|
</ResponseField>
|
|
|
|
<ResponseField name="setAttr" type="(name, value) => void">
|
|
Updates a node attribute.
|
|
</ResponseField>
|
|
|
|
<ResponseField name="themeDefaults" type="Record<string, string | number | undefined>">
|
|
The resolved theme defaults for the current node before inline overrides.
|
|
</ResponseField>
|
|
|
|
<ResponseField name="presetColors" type="string[]">
|
|
Colors collected from the current document that can be reused in custom controls.
|
|
</ResponseField>
|
|
|
|
<ResponseField name="nodePos" type="{ pos: number; inside: number }">
|
|
The ProseMirror position metadata for the focused node.
|
|
</ResponseField>
|
|
|
|
### Inspector.Text
|
|
|
|
```tsx
|
|
<Inspector.Root>
|
|
<Inspector.Text>
|
|
{({
|
|
marks,
|
|
toggleMark,
|
|
alignment,
|
|
setAlignment,
|
|
linkHref,
|
|
linkColor,
|
|
setLinkColor,
|
|
isLinkActive,
|
|
getStyle,
|
|
setStyle,
|
|
presetColors
|
|
}) => (
|
|
<>
|
|
<button type="button" onClick={() => toggleMark('bold')}>
|
|
{marks.bold ? 'Unbold' : 'Bold'}
|
|
</button>
|
|
<button type="button" onClick={() => setAlignment('center')}>
|
|
{alignment === 'center' ? 'Centered' : 'Center'}
|
|
</button>
|
|
</>
|
|
)}
|
|
</Inspector.Text>
|
|
</Inspector.Root>
|
|
```
|
|
|
|
<ResponseField name="marks" type="Record<string, boolean>">
|
|
A map of active text marks. These include `bold`, `italic`, `underline`, `strike`, and `code`.
|
|
</ResponseField>
|
|
|
|
<ResponseField name="toggleMark" type="(mark) => void">
|
|
Toggles a text mark on the current selection.
|
|
</ResponseField>
|
|
|
|
<ResponseField name="alignment" type="string">
|
|
The current alignment of the parent text block.
|
|
</ResponseField>
|
|
|
|
<ResponseField name="setAlignment" type="(value) => void">
|
|
Updates the alignment of the parent text block.
|
|
</ResponseField>
|
|
|
|
<ResponseField name="linkHref" type="string">
|
|
The current link URL when the selection is inside a link.
|
|
</ResponseField>
|
|
|
|
<ResponseField name="linkColor" type="string">
|
|
The resolved color for the active link.
|
|
</ResponseField>
|
|
|
|
<ResponseField name="setLinkColor" type="(color) => void">
|
|
Updates the color of the active link.
|
|
</ResponseField>
|
|
|
|
<ResponseField name="isLinkActive" type="boolean">
|
|
Whether the current text selection is inside a link.
|
|
</ResponseField>
|
|
|
|
<ResponseField name="getStyle" type="(prop) => string | number | undefined">
|
|
Reads a resolved style value from the parent text block.
|
|
</ResponseField>
|
|
|
|
<ResponseField name="setStyle" type="(prop, value) => void">
|
|
Updates a style on the parent text block.
|
|
</ResponseField>
|
|
|
|
<ResponseField name="presetColors" type="string[]">
|
|
Colors collected from the current document that can be reused in custom controls.
|
|
</ResponseField>
|
|
|
|
## Using portals without losing inspector focus
|
|
|
|
If your custom inspector uses portaled UI like Radix `Select`, `Popover`, or
|
|
dropdown content, moving focus into that portal can make the editor think it
|
|
blurred. That can switch the active inspector target or clear the current text
|
|
selection.
|
|
|
|
Wrap the portaled content with
|
|
[`EditorFocusScope`](/editor/api-reference/ui/editor-focus-scope) so focus
|
|
inside the portal is still treated as part of the editor UI.
|
|
|
|
```tsx
|
|
import { EditorFocusScope, Inspector } from '@react-email/editor/ui';
|
|
import * as Select from '@radix-ui/react-select';
|
|
|
|
<Inspector.Node>
|
|
{({ getAttr, setAttr }) => (
|
|
<Select.Root
|
|
value={String(getAttr('alignment') ?? 'left')}
|
|
onValueChange={(value) => setAttr('alignment', value)}
|
|
>
|
|
<Select.Trigger>
|
|
<Select.Value />
|
|
</Select.Trigger>
|
|
|
|
<Select.Portal>
|
|
<EditorFocusScope>
|
|
<Select.Content>
|
|
<Select.Viewport>
|
|
<Select.Item value="left">
|
|
<Select.ItemText>Left</Select.ItemText>
|
|
</Select.Item>
|
|
<Select.Item value="center">
|
|
<Select.ItemText>Center</Select.ItemText>
|
|
</Select.Item>
|
|
<Select.Item value="right">
|
|
<Select.ItemText>Right</Select.ItemText>
|
|
</Select.Item>
|
|
</Select.Viewport>
|
|
</Select.Content>
|
|
</EditorFocusScope>
|
|
</Select.Portal>
|
|
</Select.Root>
|
|
)}
|
|
</Inspector.Node>
|
|
```
|
|
|
|
`Inspector.Root` already uses
|
|
[`EditorFocusScope`](/editor/api-reference/ui/editor-focus-scope) by default to
|
|
handle the editor's focus idiomatically and reliably. Inside a custom inspector
|
|
you usually only need to add `EditorFocusScope` around the portaled content.
|
|
The same pattern works for other portaled Radix components, not just `Select`.
|
|
|
|
## Reusing built-in sections
|
|
|
|
You can also mix custom layouts with the built-in section components.
|
|
|
|
```tsx
|
|
<Inspector.Node>
|
|
{(context) => (
|
|
<>
|
|
<Inspector.Size {...context} />
|
|
<Inspector.Padding {...context} />
|
|
<Inspector.Border {...context} />
|
|
</>
|
|
)}
|
|
</Inspector.Node>
|
|
```
|
|
|
|
Available section components:
|
|
|
|
| Component | Description |
|
|
| -- | -- |
|
|
| `Inspector.Attributes` | Editable node attributes using field types inferred from the attribute schema |
|
|
| `Inspector.Background` | Background color control for nodes |
|
|
| `Inspector.Border` | Border width, style, color, and radius controls |
|
|
| `Inspector.Link` | Link URL and link color controls for active text links |
|
|
| `Inspector.Padding` | Four-sided padding editor |
|
|
| `Inspector.Size` | Width and height controls |
|
|
| `Inspector.Typography` | Color, size, line height, marks, and alignment controls |
|
|
|
|
## Examples
|
|
|
|
See the inspector in action with runnable examples:
|
|
|
|
<CardGroup cols={2}>
|
|
<Card title="Inspector — Defaults" icon="code" href="https://react.email/editor/examples/inspector-defaults">
|
|
Zero-config document, node, and text inspectors.
|
|
</Card>
|
|
<Card title="Inspector — Composed" icon="code" href="https://react.email/editor/examples/inspector-composed">
|
|
Showing how to use default sections and add a custom one.
|
|
</Card>
|
|
<Card title="Inspector — Fully Custom" icon="code" href="https://react.email/editor/examples/inspector-custom">
|
|
Fully custom inspector UI built from render props.
|
|
</Card>
|
|
<Card title="Standalone Editor — Inspector" icon="code" href="https://react.email/editor/examples/standalone-editor-inspector">
|
|
Inspector sidebar with the standalone EmailEditor component.
|
|
</Card>
|
|
</CardGroup>
|