Sending emails in SvelteKit w/ TailwindCSS

Sending styled emails for free in our favorite framework

Email on laptop
Sending emails is an important skill
Sending emails is crucial to know about if you ever need to send transactional email in your app. This could be for account confirmation, activity updates or billing emails. Doing it in SvelteKit is easier than you might think, and free!

To achieve good-looking emails you would traditionally have to resort to inline styling and tables, since email clients use an ancient subset of HTML/CSS features. I’m going to show you how to instead do this with our beloved tool Tailwind CSS!

Sending emails with Nodemailer (Gmail, Zoho Mail etc) #

If you’re just starting out and don’t have your own domain yet, there are multiple free providers that allow you to register email addresses without spending a penny. The benefit from this is also that they are reputable and emails that you send will rarely end up in the spam folder.

To allow third-party clients to send emails without needing a web interface, providers like Gmail and Zoho expose an SMTP server. We can leverage this to send emails within our SvelteKit application.

Gmail specifics

Two-step authentication
Set up 2-step authentication for Google

In order to send mails with Gmail, you first need to turn on 2-step verification in your Google account. After this, you should create an App Password which will let third-party apps (like ours) gain access to your account. Make sure to keep this password stuffed away securely, preferably in an environment variable.

Zoho mail specifics Zoho doesn’t require 2-factor verification for transmission, but if you do have it enabled, you might need to configure an App-specific password before you can send emails.

Sending emails in your app

One of the most adopted libraries for sending emails with SMTP is nodemailer and can be easily used with any Node.JS application (like SvelteKit).

Installing it is as easy as follows: npm install nodemailer

Here is an example SvelteKit server action that runs whenever the user registers an account in our application:

Registration page
Simple registration flow in SvelteKit

import type { Actions } from './$types';

export const actions = {
	register: async (event) => {
		const data = await request.formData();
		const email = data.get('email');
		const password = data.get('password');
		
		// Register the user... 
	}
} satisfies Actions;

This is where we can plug in nodemailer to send our first confirmation email!

import type { Actions } from './$types';
import nodemailer from 'nodemailer';

const transporter = nodemailer.createTransport({
	service: "gmail",
	host: "smtp.gmail.com",
	auth: {
		user: "[email protected]", 
		pass: "app_password_here"
	},
	secure: true,
	port: 465
});

export const actions = {
	register: async (event) => {
		const data = await request.formData();
		const email = data.get('email');
		const password = data.get('password');
		
		// Actually register the user...
		
		await transporter.sendMail({
		  from: "[email protected]", 
		  to: email, 
		  subject: "Confirm your email", 
		  html: "<p>Please confirm your email!</p>" 
		}); 
	}
} satisfies Actions;

Notice that we’re specifying the SMTP server smtp.gmail.com and the Gmail account credentials that we wish to send from. This would be substituted with smtp.zoho.eu or another SMTP server domain for an email provider of your choice.

If you’re using TypeScript like me, you will also need to install the @types/nodemailer NPM package to get proper typing support.

Using your own domain with Resend #

If you’re perhaps a Google-fearing individual or wish to look more professional, you can use your own domain name (such as [email protected]) to send emails with.

This would traditionally be quite the undertaking since you would need to setup your own email server, with software like Postfix. Then you would have to deal with delivery issues from mailbox providers treating your mails as spam, sad face! An easier alternative nowadays is to rely on an email service that does all the heavy lifting underneath the hood.

Alternatives include:

  • SendGrid
  • Amazon SES
  • MailGun
  • Maileroo
  • Resend

The common theme amongst these is that they expose a HTTP API and often a bunch of other email nice-to-haves.

In this blog post we will take a look at the last one, called Resend. They market themselves as “Email for developers” which seems terribly promising! Their pricing model is also appealing to a hobbyist like me, with 100 emails/day and 3000 emails/month for free.

Getting started is as easy as signing up for an account and creating your first API key. After this you can head to the Domains tab to setup your custom domain. You will need to configure two kinds of DNS records called SPF and DKIM, which are used to verify the legitimacy of your emails. They have a straightforward guide that will help you get this right.

Talking to Resend
We could use fetch to interface with their HTTP API but they also conveniently provide their own JS library that can be installed as follows:
npm install resend

After installation, we can configure our user registration flow in SvelteKit to send emails through Resend.

import type { Actions } from './$types';
import { Resend } from 'resend';

const resend = new Resend('re_xxxx...xxxxxx');

export const actions = {
	register: async (event) => {
		const data = await request.formData();
		const email = data.get('email');
		const password = data.get('password');
		
		// Actually register the user...
		
		await resend.emails.send({
		  from: '[email protected]',
		  to: email,
		  subject: 'Confirm your email',
		  html: '<p>Please confirm your email!</p>',
		});		
	}
} satisfies Actions;

Click the register button and voilá, you’re now sending emails with a custom domain!

Styling emails with Tailwind CSS #

Plaintext email
A boring plaintext email
Unless you want your emails to look like they come straight out of a 90’s website, we have some styling work to do. Like I mentioned in the introduction, this would normally entail lots of inline styling and weird CSS hacks. Can we somehow stay in our safe reusable TailwindCSS-styled Svelte harbor? As a matter of fact, we can!

There is a neat library called better-svelte-email that lets us compose emails just as if we were making another component! This favors the reusability of components for multiple different messages. The code will be transformed to email-compatible HTML for us underneath the hood.

Install it with npm:
npm install better-svelte-email

Create a new email component, preferably placed somewhere like src/lib/emails/confirm.svelte:

<script>
	import {
		Html,
		Head,
		Body,
		Preview,
		Container,
		Section,
		Text,
		Button,
		Row
	} from 'better-svelte-email';

	let { email = '[email protected]' } = $props();
</script>

<Html>
	<Head />
	<Body class="bg-zinc-100">
		<Preview preview="Confirm your email" />
		<Container class="m-8 mx-auto max-w-lg rounded-2xl bg-white p-8">
			<Section class="mx-auto text-center">
				<Text class="text-2xl font-bold text-zinc-900">Welcome {email}!</Text>
				<Text class="mt-3 text-zinc-600">
					Please confirm your email to get started.
				</Text>
				<Row class="mt-6">
					<Button
						href="https://myapp.com/confirm/token_here"
						pX={24}
						pY={14}
						class="mr-2 rounded-lg bg-orange-600 text-white"
					>
						Confirm your email
					</Button>
				</Row>
			</Section>
		</Container>
	</Body>
</Html>

Notice the use of some of the built-in components like Row, Button. More can be found in the documentation. We also pass in a name component that we can update dynamically based on who we are addressing the email to.

In order to send this email we will need to transform it into HTML. This can be done with the Renderer API:

import Renderer from 'better-svelte-email/render';
import ConfirmationEmail from '$lib/emails/confirm.svelte';

const emailRenderer = new Renderer();
const html = await renderer.render(ConfirmationEmail, { props: { email }});

Plugging this into our SvelteKit server action and sending the email with Resend:

import type { Actions } from './$types';
import Renderer from 'better-svelte-email/render';
import ConfirmationEmail from '$lib/emails/confirm.svelte';
import { Resend } from 'resend';

const resend = new Resend('re_xxxx...xxxxxx');
const emailRenderer = new Renderer();

export const actions = {
	register: async (event) => {
		const data = await request.formData();
		const email = data.get('email');
		const password = data.get('password');
		
		// Actually register the user...
		
		const html = await renderer.render(ConfirmationEmail, { props: { email }})
		
		await resend.emails.send({
		  from: '[email protected]',
		  to: email,
		  subject: 'Confirm your email',
		  html: html,
		});		
	}
} satisfies Actions;

Clicking the register button once more…

Styled email
Email styled with TailwindCSS
Won’t you look at that! That’s terribly more inspiring than some boring old text.

Now it’s time to wrap up this blog post. Hopefully you’ve noticed that it’s quite straightforward to send good-looking emails in SvelteKit, even for free!