Skip to main content

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

tsx
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:

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:

tsx
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:

server/api/order-confirmation.get.ts
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 EmailNuxt Email
JSX/TSX componentVue SFC under app/emails/
Imported Html, Body, Text, ButtonAuto-registered EHtml, EBody, EText, EButton
TypeScript function propsStandard Vue defineProps<>()
childrenDefault 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> wrapperAuto-registered ETailwind
Renderer receives an elementRenderer receives a generated template name and typed props
Preview-specific React appExact 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.