Email Templates

Email components

The styled Email* components, and how to compose a welcome email, an alert, a receipt or a digest.

This page shows the styled components every Frigear email is built from, and how to compose an email of your own. It is for app developers who write templates. Set up the module first: Overview.

Components

Import the components from @frigear-nu/email-templates/components. The renderer registers only its own E* components globally, and you can mix both freely. Every email has the same shape: a grey page, a white card with the logo, the content and a footer.

ComponentPropsRenders
EmailLayoutpreview?, lang? (en); slots: default, footerThe whole document: page, card, EmailHeader and EmailFooter.
EmailHeaderlogoUrl?The logo, 150px wide, or the brand name when there is no logo.
EmailHeadingtone?: 'default' | 'danger'The h2 title. danger is for security alerts.
EmailTextvariant?: 'default' | 'muted' | 'small'A paragraph of body text, secondary text or fine print.
EmailButtonhref, color?: 'primary' | 'danger'A call-to-action button.
EmailLinkFallbackhref; a slot for the lead-in"Or copy this link to your browser: …"
EmailDividernoneA horizontal rule.
EmailFooterslot"This email was sent automatically for security reasons."
The components use inline styles and table layout, because email clients support neither CSS classes nor flexbox or grid.

Composing an email

Every template starts from EmailLayout, which draws the page, the card, the logo and the footer. Inside it, the usual order is <ESubject>, an EmailHeading, an EmailDivider, EmailText paragraphs, then an EmailButton and its EmailLinkFallback:

app/emails/welcome.vue
<script setup lang="ts">
import { EmailButton, EmailDivider, EmailHeading, EmailLayout, EmailLinkFallback, EmailText } from '@frigear-nu/email-templates/components'

defineProps<{ name: string, url: string }>()
</script>

<template>
  <EmailLayout :preview="`Welcome, ${name}!`">
    <ESubject>Welcome, {{ name }}</ESubject>
    <EmailHeading>Welcome, {{ name }}!</EmailHeading>
    <EmailDivider />
    <EmailText>Thanks for signing up. Click the button below to get started.</EmailText>
    <EmailButton :href="url">Get started</EmailButton>
    <EmailLinkFallback :href="url" />
  </EmailLayout>
</template>

Beyond that shape:

  • Tables and columns. Use the renderer's ERow and EColumn for details or line items.
  • Fine-tuning a block. A style you pass merges into the component's own, such as style="margin: 0".
  • Formatting. Templates render outside the Nuxt app, so useFormat() and other app composables are not available. Format numbers and dates with Intl, or pass them in formatted.
  • Your own footer. Fill the layout's footer slot, for example with an unsubscribe link.

A security alert, with a details table and a danger heading and button:

app/emails/new-sign-in.vue
<script setup lang="ts">
import { EmailButton, EmailDivider, EmailHeading, EmailLayout, EmailLinkFallback, EmailText } from '@frigear-nu/email-templates/components'

defineProps<{
  name: string
  device: string
  location: string
  signedInAt: string
  secureUrl: string
}>()
</script>

<template>
  <EmailLayout preview="New sign-in to your account">
    <ESubject>New sign-in to your account</ESubject>
    <EmailHeading tone="danger">
      New sign-in detected
    </EmailHeading>
    <EmailDivider />
    <EmailText>Hi {{ name }}, your account was just signed in to from a new device.</EmailText>

    <!-- Renderer components mix in freely: a two-column details table. -->
    <ERow
      v-for="[label, value] in [['Device', device], ['Location', location], ['Time', signedInAt]]"
      :key="label"
    >
      <EColumn style="width: 35%; padding: 4px 0">
        <EmailText
          variant="muted"
          style="margin: 0"
        >
          {{ label }}
        </EmailText>
      </EColumn>
      <EColumn style="padding: 4px 0">
        <EmailText style="margin: 0">
          {{ value }}
        </EmailText>
      </EColumn>
    </ERow>

    <EmailDivider />
    <EmailText>If this was you, no action is needed. If not, secure your account now.</EmailText>
    <EmailButton
      :href="secureUrl"
      color="danger"
    >
      Secure my account
    </EmailButton>
    <EmailLinkFallback :href="secureUrl" />
  </EmailLayout>
</template>

A receipt, with line items, a computed total and a footer of its own:

app/emails/order-receipt.vue
<script setup lang="ts">
import { computed } from 'vue'
import { EmailButton, EmailDivider, EmailFooter, EmailHeading, EmailLayout, EmailText } from '@frigear-nu/email-templates/components'

const props = defineProps<{
  name: string
  orderNumber: string
  items: { label: string, quantity: number, amount: number }[]
  currency: string
  orderUrl: string
}>()

// Templates render outside the Nuxt app, so format with Intl rather than app composables.
const money = (amount: number) => new Intl.NumberFormat('en', { style: 'currency', currency: props.currency }).format(amount)
const total = computed(() => props.items.reduce((sum, item) => sum + item.amount * item.quantity, 0))
</script>

<template>
  <EmailLayout :preview="`Receipt for order ${orderNumber}`">
    <ESubject>Your receipt for order {{ orderNumber }}</ESubject>
    <EmailHeading>Thanks for your order, {{ name }}!</EmailHeading>
    <EmailText variant="muted">
      Order {{ orderNumber }}
    </EmailText>
    <EmailDivider />

    <ERow
      v-for="item in items"
      :key="item.label"
    >
      <EColumn>
        <EmailText style="margin: 0 0 8px">
          {{ item.quantity }} × {{ item.label }}
        </EmailText>
      </EColumn>
      <EColumn align="right">
        <EmailText style="margin: 0 0 8px">
          {{ money(item.amount * item.quantity) }}
        </EmailText>
      </EColumn>
    </ERow>

    <EmailDivider />
    <ERow>
      <EColumn>
        <EmailText style="font-weight: 700; margin: 0 0 16px">
          Total
        </EmailText>
      </EColumn>
      <EColumn align="right">
        <EmailText style="font-weight: 700; margin: 0 0 16px">
          {{ money(total) }}
        </EmailText>
      </EColumn>
    </ERow>

    <EmailButton :href="orderUrl">
      View order
    </EmailButton>

    <template #footer>
      <EmailFooter>Questions about your order? Just reply to this email.</EmailFooter>
    </template>
  </EmailLayout>
</template>

A digest with no button: a list of links, an empty state, and an unsubscribe link in the footer:

app/emails/weekly-digest.vue
<script setup lang="ts">
import { EmailDivider, EmailFooter, EmailHeading, EmailLayout, EmailText } from '@frigear-nu/email-templates/components'

defineProps<{
  name: string
  events: { title: string, date: string, url: string }[]
  unsubscribeUrl: string
}>()
</script>

<template>
  <EmailLayout preview="Upcoming events this week">
    <ESubject>This week at Frigear</ESubject>
    <EmailHeading>Hi {{ name }}, here’s what’s coming up</EmailHeading>
    <EmailDivider />

    <template
      v-for="event in events"
      :key="event.url"
    >
      <EmailText style="margin: 0 0 4px; font-weight: 600">
        <ELink
          :href="event.url"
          style="color: inherit"
        >
          {{ event.title }}
        </ELink>
      </EmailText>
      <EmailText variant="muted">
        {{ event.date }}
      </EmailText>
    </template>

    <EmailText v-if="!events.length">
      Nothing scheduled this week. We’ll let you know when something comes up.
    </EmailText>

    <!-- Replace the default footer, e.g. with an unsubscribe link. -->
    <template #footer>
      <EmailFooter>
        You get this digest because you follow Frigear events.
        <a
          :href="unsubscribeUrl"
          style="color: inherit"
        >Unsubscribe</a>.
      </EmailFooter>
    </template>
  </EmailLayout>
</template>

Send them like any other template. The props are typed from each defineProps:

await sendEmailTemplate('order-receipt', {
  name: user.name,
  orderNumber: order.number,
  currency: 'DKK',
  items: order.lines.map(line => ({ label: line.label, quantity: line.quantity, amount: line.amount })),
  orderUrl: `${origin}/orders/${order.number}`,
}, { to: user.email })

Next steps

Copyright © 2026