Docs / Email / Email templates
Email templates
Templates live in emails/ as React Email components, one file per message. Each is a plain function returning JSX, so it renders and previews without a running app, and takes its dynamic bits as props:
export function WelcomeEmail({ name, url }: { name: string; url: string }) {
return (
<EmailLayout preview="Welcome to Keel">
<Text style={emailHeading}>Welcome, {name}</Text>
<Text style={emailText}>Your email's verified and your account is ready.</Text>
<SectionGap>
<Link href={url} style={emailButton}>Go to dashboard</Link>
</SectionGap>
</EmailLayout>
);
}
The templates that ship: welcome, verify-email, reset-password, magic-link, team-invite, payment-receipt, payment-failed, subscription-canceled, and contact-message.
Shared chrome
Every template wraps its body in EmailLayout (emails/components/email-layout.tsx), which owns the "Keel" wordmark, the card frame, and the footer, so those live in one place instead of nine copies. The file also exports the shared inline styles (emailHeading, emailText, emailButton) and a SectionGap spacer, use them rather than inventing per-template styles, that is what keeps the set looking like one product.
Templates are intentionally light-only, with a system font
Colors are Keel's light-mode tokens hardcoded as hex, not the app's CSS variables, email clients don't support var(), and dark-mode email rendering is unreliable enough across clients that going light-only everywhere is the safe choice. The font is a system stack, not Hanken Grotesk, because most clients (Outlook desktop especially) ignore web fonts, so shipping one buys almost nothing.
Previewing locally
Each template sets PreviewProps with sample data, which React Email's preview server reads to render a realistic version without wiring up real data:
WelcomeEmail.PreviewProps = { name: "Jane", url: "https://example.com/app" };
Run the preview server on its own port:
bun run email:dev
It opens a browser gallery of every template at localhost:3001, live-reloading as you edit. This is the fast loop for design work, you never need to trigger a real send to see a change.
Adding a template
Create emails/your-email.tsx, wrap the body in EmailLayout, reuse the shared style exports, and give it a PreviewProps. Then call it from the event that should send it via sendEmail, passing the component to react.