Migration from React Email
Translate JSX templates, props, and rendering calls to Nuxt Email's Vue SFCs and generated server API.
Nuxt Email ports proven email-output behavior, not React's runtime or component API. Move a template by preserving its semantic document, replacing React components with the E-prefixed Vue primitives, and exposing it through Nuxt's generated server registry.
The same email, two frameworks
React Email
import { Body, Button, Container, Head, Heading, Html, Preview, Text } from '@react-email/components'
export function OrderEmail({ orderNumber, recipientName }: { orderNumber: number; recipientName: string }) {
return (
<Html lang="en">
<Head><title>Order confirmation</title></Head>
<Preview>Your order is ready.</Preview>
<Body style={{ backgroundColor: '#f5f5f5', margin: 0 }}>
<Container style={{ maxWidth: '600px', padding: '24px' }}>
<Heading>Order {orderNumber} for {recipientName}</Heading>
<Text>We have received your order.</Text>
<Button href={`https://example.com/orders/${orderNumber}`} style={{ padding: '12px 20px' }}>
View order
</Button>
</Container>
</Body>
</Html>
)
}Nuxt Email
Place the Vue version at app/emails/order-confirmation.vue:
<script setup lang="ts">
import { defineEmail } from '@lupinum/nuxt-email/define-email'
const props = defineProps<{
orderNumber: number
recipientName: string
}>()
defineEmail({
subject: () => `Order ${props.orderNumber} confirmed`,
})
</script>
<template>
<EHtml lang="en">
<EHead><title>Order confirmation</title></EHead>
<EBody :style="{ backgroundColor: '#f5f5f5', margin: 0 }">
<EPreview>Your order is ready.</EPreview>
<EContainer :style="{ maxWidth: '600px', padding: '24px' }">
<EHeading>Order {{ orderNumber }} for {{ recipientName }}</EHeading>
<EText>We have received your order.</EText>
<EButton :href="`https://example.com/orders/${orderNumber}`" :style="{ padding: '12px 20px' }">
View order
</EButton>
</EContainer>
</EBody>
</EHtml>
</template>EPreview belongs inside EBody, and the document title goes explicitly in EHead — Vue SSR does not reproduce React 19's preview-title hoisting (an intentional divergence).
Rendering
React Email renders an element directly and requests HTML and plain text separately:
const html = await render(<OrderEmail {...props} />)
const text = await render(<OrderEmail {...props} />, { plainText: true })Nuxt Email discovers the SFC as order-confirmation, generates its prop type, and returns both representations from one canonical server renderer:
export default defineEventHandler(() => {
return renderEmail('order-confirmation', {
orderNumber: 7319,
recipientName: 'Ada',
})
})Do not import a component renderer into application code. The public path is the generated Nitro-only renderEmail auto-import.
Authoring translation
| React Email | Nuxt Email |
|---|---|
| JSX/TSX component | Vue SFC under app/emails/ |
Imported Html, Body, Text, Button | Auto-registered EHtml, EBody, EText, EButton |
| TypeScript function props | Standard Vue defineProps<>() |
children | Default Vue slot |
{condition && <Text />} | <EText v-if="condition"> |
{items.map(item => <Text key={item.id} />)} | <EText v-for="item in items" :key="item.id"> |
style={{ backgroundColor: '#fff' }} | :style="{ backgroundColor: '#fff' }" |
<Tailwind> wrapper | Auto-registered ETailwind |
| Renderer receives an element | Renderer receives a generated template name and typed props |
| Preview-specific React app | Exact sibling fixture plus development-only /__email |
Use Vue's native attribute names and CSS styles. React-shaped convenience props such as mx are intentionally absent — use ordinary CSS in style.
ETailwind uses Tailwind v4, but it does not automatically inherit your Nuxt stylesheet or browser CSS variables. Pass concrete theme/config values from your application-owned token source when migrating custom utilities such as text-primary.
Behavioral differences to review
- Compatibility is tracked per behavior, never as a global React Email compatibility claim.
- Component names and the authoring API are Vue-native and E-prefixed.
- React streaming, Suspense behavior, generated React markers, JSX execution, and the React preview stack are not ported.
- Some document, preview, and table-marker output intentionally differs where Vue authoring or email safety requires it.
Do not use this summary as a parity matrix — review the conformance report for the authoritative cases and divergences.
Sending remains application-owned
Nuxt Email stops at { html, text, subject? }. Keep recipients, sender identity, provider credentials, attachments, tags, scheduling, and delivery policy in your provider SDK. There is no provider-neutral adapter or public send endpoint, so migrating does not require giving up provider-specific features. You can optionally compute the subject line beside the template with defineEmail.