UI Builder supports multiple page types via the pageTypeRenderers and pageTypeCodeGenerators props. This extension point lets you add support for email templates, PDFs, React Native, or any other rendering target — without modifying UI Builder's internals.
UI Builder stays framework-agnostic. All framework-specific logic lives in your consumer code.
The react-email registry includes definitions for all commonly used @react-email/components:
| Component | Notes |
|---|---|
Html | Root element — new email pages default to this |
Head | Email <head> — must be inside Html |
Preview | Inbox preview text — must be inside Html |
Body | Email body — inside Html or Tailwind |
Tailwind | Applies Tailwind CSS to email — wraps Body |
Container | Centered max-width table container |
Section | Semantic content section |
Row | Table row layout |
Column | Table cell — must be inside Row |
Text | Paragraph / text content |
Button | CTA link-button with VML Outlook support |
Link | Anchor link |
Img | Email-safe image |
Hr | Horizontal divider |
React email supports a subset of Tailwind via the <Tailwind> wrapper component. Wrap your <Body> with <Tailwind> and use DEFAULT_EMAIL_TAILWIND_CONFIG so pixelBasedPreset is applied by default.
The react-email registry also uses email-specific className field overrides, so class suggestions avoid shadcn-only/theme-variable classes that do not map cleanly to email clients.
Supported: Most layout utilities (flex, grid, p-*, m-*, text-*, bg-*, etc.)
Not supported: prose (requires @tailwindcss/typography), space-*, hover: pseudo-class, most media queries in older clients.
See caniemail.com for client support details.
pageTypeRenderers?: PageTypeRenderersA map of page type key → PageTypeRenderer config. When provided, UI Builder shows a page type selector in the "Create new page" popover and dispatches canvas rendering to the matching renderer for pages of that type.
ts1interface PageTypeRenderer { 2 /** Display label shown in the page type selector (e.g. "Email", "PDF") */ 3 label?: string; 4 /** Root component type for new pages of this type. Defaults to "div". */ 5 defaultRootLayerType?: string; 6 /** Initial props for the root component of a new page. */ 7 defaultRootLayerProps?: Record<string, unknown>; 8 /** 9 * When true, EditorPanel skips AutoFrame + ResizableWrapper entirely. 10 * Reserved for page types that CANNOT render inside an iframe (PDF, react-native-web). 11 * Do NOT set for email — email renders fine inside AutoFrame. 12 */ 13 skipAutoFrame?: boolean; 14 /** 15 * Filters the global componentRegistry shown in the add-component popover. 16 * Return a subset of the registry to restrict available components. 17 */ 18 filterRegistry?: (registry: ComponentRegistry) => ComponentRegistry; 19 /** Renders the editor canvas for this page type. */ 20 renderEditorCanvas: (props: PageTypeRendererProps) => React.ReactNode; 21}
pageTypeCodeGenerators?: PageTypeCodeGeneratorsA map of page type key → PageTypeCodeGenerator. When present for the active page, replaces the default "React" code tab in the code panel.
ts1interface PageTypeCodeGenerator { 2 /** Generates the code/output string for the page. */ 3 generateCode: (page: ComponentLayer, registry: ComponentRegistry) => string; 4 /** Tab label, e.g. "HTML Email", "PDF", "React Native". Defaults to "React". */ 5 label?: string; 6}
The react-email registry ships everything needed to wire up UI Builder as a full email editor — component definitions, canvas substitutes, code generator, and page renderer config. UI Builder itself has zero knowledge of @react-email/*.
bash1npx shadcn@latest add https://raw.githubusercontent.com/olliethedev/ui-builder/main/registry/react-email-components-registry.json
This installs:
lib/ui-builder/registry/react-email-component-definitions.ts — ComponentRegistry for all 14 react-email componentslib/ui-builder/email/email-builder-utils.tsx — ready-to-use emailPageRenderer, emailCodeGenerator, DEFAULT_EMAIL_TAILWIND_CONFIG, and canvas substitutes@react-email/components, @react-email/render, web-streams-polyfillImport and pass the pre-built config objects to UIBuilder:
tsx1import UIBuilder from '@/components/ui/ui-builder'; 2import { reactEmailComponentDefinitions } from '@/lib/ui-builder/registry/react-email-component-definitions'; 3import { 4 emailPageRenderer, 5 emailCodeGenerator, 6 DEFAULT_EMAIL_TAILWIND_CONFIG, 7} from '@/lib/ui-builder/email/email-builder-utils'; 8 9export function EmailBuilder() { 10 return ( 11 <UIBuilder 12 componentRegistry={reactEmailComponentDefinitions} 13 pageTypeRenderers={{ email: emailPageRenderer }} 14 pageTypeCodeGenerators={{ email: emailCodeGenerator }} 15 tailwindThemePanel={false} 16 initialLayers={[ 17 { 18 id: 'email-1', 19 type: 'Html', 20 name: 'Email', 21 pageType: 'email', 22 props: { lang: 'en' }, 23 children: [ 24 { id: 'email-head-1', type: 'Head', name: 'Head', props: {}, children: [] }, 25 { id: 'email-preview-1', type: 'Preview', name: 'Preview', props: {}, children: 'Preview text' }, 26 { 27 id: 'email-tailwind-1', 28 type: 'Tailwind', 29 name: 'Tailwind', 30 props: { config: DEFAULT_EMAIL_TAILWIND_CONFIG }, 31 children: [ 32 { 33 id: 'email-body-1', 34 type: 'Body', 35 name: 'Body', 36 props: { className: 'bg-white font-sans' }, 37 children: [], 38 }, 39 ], 40 }, 41 ], 42 }, 43 ]} 44 allowPagesCreation 45 /> 46 ); 47}
Add this import at the top of your email route's entry file for Safari/iOS compatibility with @react-email/render:
ts1import 'web-streams-polyfill/polyfill';
The email-builder-utils.tsx file also exports the lower-level building blocks if you need to customize behavior — for example, to modify how structural HTML elements are substituted on the canvas:
tsx1import { useMemo } from 'react'; 2import LayerRenderer from '@/components/ui/ui-builder/layer-renderer'; 3import { 4 NullComponent, 5 CanvasHtml, 6 CanvasBody, 7 generateEmailCode, 8} from '@/lib/ui-builder/email/email-builder-utils'; 9import type { 10 PageTypeRenderer, 11 PageTypeRendererProps, 12 ComponentRegistry, 13} from '@/components/ui/ui-builder/types'; 14 15// Custom canvas renderer — add your own substitutes or logic 16function MyEmailCanvasRenderer({ page, componentRegistry, editorConfig }: PageTypeRendererProps) { 17 const canvasRegistry: ComponentRegistry = useMemo(() => { 18 const base = { ...componentRegistry }; 19 if (base.Html) base.Html = { ...base.Html, component: CanvasHtml }; 20 if (base.Head) base.Head = { ...base.Head, component: NullComponent }; 21 if (base.Preview) base.Preview = { ...base.Preview, component: NullComponent }; 22 if (base.Body) base.Body = { ...base.Body, component: CanvasBody }; 23 return base; 24 }, [componentRegistry]); 25 26 return ( 27 <LayerRenderer 28 className="contents" 29 page={page} 30 componentRegistry={canvasRegistry} 31 editorConfig={editorConfig} 32 /> 33 ); 34} 35 36const myEmailPageRenderer: PageTypeRenderer = { 37 label: 'Email', 38 defaultRootLayerType: 'Html', 39 defaultRootLayerProps: { lang: 'en' }, 40 renderEditorCanvas: (props) => <MyEmailCanvasRenderer {...props} />, 41};
The PageTypeRenderer / PageTypeCodeGenerator pattern is designed to support any rendering target:
tsx1const pdfPageRenderer: PageTypeRenderer = { 2 label: 'PDF', 3 defaultRootLayerType: 'Document', 4 skipAutoFrame: true, // PDF viewer cannot render inside an iframe 5 filterRegistry: (registry) => pdfOnlyRegistry, 6 renderEditorCanvas: (props) => ( 7 <PDFViewer style={{ width: '100%', height: '100%' }}> 8 <LayerRenderer {...props} /> 9 </PDFViewer> 10 ), 11}; 12 13const pdfCodeGenerator: PageTypeCodeGenerator = { 14 label: 'PDF', 15 generateCode: (page, registry) => { 16 // Generate React PDF JSX 17 return `// PDF template for ${page.name}`; 18 }, 19};
tsx1const nativePageRenderer: PageTypeRenderer = { 2 label: 'Native', 3 filterRegistry: (registry) => nativeComponentRegistry, 4 renderEditorCanvas: (props) => <LayerRenderer {...props} />, 5};