Skip to main content

Migration from React Email

Translate JSX templates, props, and rendering calls to Nuxt Email's Vue single-file components and generated server API.

Upgrade from 0.1.1

1.0.0-beta.1 makes four intentional hard cutovers:

  • Add a non-empty lang to every EHtml.
  • Style ELink color and text decoration explicitly when the design needs them; the component is now visually neutral.
  • Replace reads of EmailRenderError.componentName with templateName. No deprecated alias is provided.
  • Give defineEmail at least one synchronous subject or text value. Authored text now replaces derived plain text.

For preview fixtures, replace the longer Vue utility-type expression with EmailComponentProps<typeof YourEmail>. Fixtures remain deterministic objects, not factories or scenarios.

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 Vue single-file component (.vue file) as order-confirmation. It generates its prop type and returns both representations from one 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 .vue file 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, credentials, attachments, tags, scheduling, and delivery policy in your provider SDK. Nuxt Email has no provider-neutral adapter or public send endpoint. You can keep provider-specific features and optionally compute the subject beside the template with defineEmail.