Free email template
Password reset email template
A single-use link to choose a new password, with an expiry and a clear "ignore this" line. Free and MIT-licensed, in React Email, MJML, HTML and plain text, with light and dark mode built in.
- MIT license
- Works with any email provider
- Light and dark mode
What every password reset email needs.
This template does all six. If you write your own, check it against this list.
A link that works once
The token in the link should be single-use and tied to this request. Anyone who finds the email later gets nothing from it.
A short expiry, stated plainly
Say "expires in 60 minutes" in the email. People act sooner, and an old link sitting in an inbox stops being a way in.
An "ignore this" line
Many resets nobody asked for are typos or someone guessing. Tell people their password won't change unless they click.
Never a password
Not the old one, not a new one. Send a link to choose one. A password in an inbox is a password in every backup of it.
A link people can paste
Some clients strip or block buttons. The full URL under the button means nobody is stuck.
A plain-text part
Every template ships a .txt version. Screen readers, watches and some spam filters rely on it.
The code. Copy the format your stack uses.
Change the brand name, logo and colors once in theme.ts and every template follows.
import * as React from 'react'
import { Button, Divider, Layout, LinkFallback, P, Small, Title } from '../../components'
import type { TemplateMeta } from '../../components/meta'
import { plainText } from '../../components/text'
import { theme } from '../../theme'
export type PasswordResetProps = {
name: string
resetUrl: string
expiresIn: string
}
export const meta: TemplateMeta = {
name: 'Password reset',
description: 'A single-use link to choose a new password, with an expiry and a clear "ignore this" line.',
group: 'Authentication',
subject: `Reset your ${theme.brand.name} password`,
}
export default function PasswordReset({ name, resetUrl, expiresIn }: PasswordResetProps) {
return (
<Layout
preview={`Use this link to choose a new password. It expires in ${expiresIn}.`}
footerNote="You're receiving this because a password reset was requested for your account."
>
<Title>Reset your password</Title>
<P>Hi {name},</P>
<P>Someone asked to reset the password for your account. Choose a new one with the button below.</P>
<Button href={resetUrl}>Choose a new password</Button>
<Small>
This link expires in {expiresIn} and can only be used once. If you didn't ask for a reset, you can
ignore this email. Your password won't change.
</Small>
<Divider />
<LinkFallback href={resetUrl} />
</Layout>
)
}
PasswordReset.PreviewProps = {
name: 'Ada',
resetUrl: 'https://example.com/reset-password?token=8c1f2e',
expiresIn: '60 minutes',
} satisfies PasswordResetProps
export function text({ name, resetUrl, expiresIn }: PasswordResetProps) {
return plainText(
'Reset your password',
`Hi ${name},`,
'Someone asked to reset the password for your account. Choose a new one here:',
resetUrl,
`This link expires in ${expiresIn} and can only be used once. If you didn't ask for a reset, you can ignore this email. Your password won't change.`,
)
}
<mjml lang="en" dir="ltr">
<mj-head>
<mj-title>Reset your password</mj-title>
<mj-preview>Use this link to choose a new password. It expires in {{expires_in}}.</mj-preview>
<!-- theme:start -->
<!-- Generated from theme.ts by `npm run build`. Edit theme.ts, not this block. -->
<mj-raw>
<meta name="color-scheme" content="light dark" />
<meta name="supported-color-schemes" content="light dark" />
</mj-raw>
<mj-attributes>
<mj-all font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif" />
<mj-body background-color="#f4f4f5" width="560px" />
<mj-wrapper padding="40px 16px" />
<mj-section padding="0" />
<mj-column padding="0" />
<mj-text padding="0 0 16px" font-size="15px" line-height="24px" color="#18181b" css-class="em-text" />
<mj-button align="left" background-color="#2f5bd3" color="#ffffff" font-size="15px" font-weight="600" line-height="20px" border-radius="8px" inner-padding="12px 22px" padding="8px 0 24px" css-class="em-button" />
<mj-divider border-width="1px" border-color="#e4e4e7" padding="12px 0 28px" css-class="em-rule" />
<mj-table padding="0 0 24px" font-size="14px" line-height="20px" color="#18181b" />
<mj-class name="card" background-color="#ffffff" border="1px solid #e4e4e7" border-radius="12px" padding="40px" css-class="em-card" />
<mj-class name="h1" font-size="24px" line-height="32px" font-weight="650" letter-spacing="-0.3px" css-class="em-text em-h1" />
<mj-class name="small" font-size="13px" line-height="20px" color="#52525b" padding="0 0 12px" css-class="em-muted" />
<mj-class name="danger" background-color="#b42318" css-class="em-button-danger" />
<mj-class name="footer" font-size="12px" line-height="18px" color="#52525b" align="center" padding="0 0 6px" css-class="em-muted" />
</mj-attributes>
<mj-style inline="inline">
.em-label { padding: 10px 0; color: #52525b; width: 45%; vertical-align: top; }
.em-value { padding: 10px 0; color: #18181b; text-align: right; vertical-align: top; }
.em-row-border { border-top: 1px solid #e4e4e7; }
.em-total { padding: 12px 0 10px; color: #18181b; font-weight: 650; border-top: 2px solid #18181b; vertical-align: top; }
.em-total-value { padding: 12px 0 10px; color: #18181b; font-weight: 650; border-top: 2px solid #18181b; text-align: right; vertical-align: top; }
.em-codebox { margin: 8px 0 8px; padding: 18px 12px; background-color: #f4f4f5; border: 1px solid #e4e4e7; border-radius: 8px; text-align: center; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace; font-size: 32px; line-height: 40px; font-weight: 700; letter-spacing: 8px; color: #18181b; }
.em-callout { padding: 14px 16px; background-color: #f4f4f5; border: 1px solid #e4e4e7; border-radius: 8px; font-size: 14px; line-height: 22px; color: #18181b; }
.em-track { height: 8px; font-size: 1px; line-height: 8px; border-radius: 999px; background-color: #f4f4f5; }
.em-fill { height: 8px; font-size: 1px; line-height: 8px; border-radius: 999px; background-color: #2f5bd3; }
.em-link { color: #2f5bd3; text-decoration: underline; }
.em-step { color: #52525b; }
</mj-style>
<mj-style>
body { -webkit-font-smoothing: antialiased; }
:root { color-scheme: light dark; supported-color-schemes: light dark; }
.em-logo-dark { display: none; }
@media (prefers-color-scheme: dark) {
body, .em-page { background-color: #0e0e10 !important; }
.em-card, .em-card > table, .em-card > table > tbody > tr > td { background-color: #18181b !important; border-color: #2e2e33 !important; }
.em-text, .em-text > div { color: #f4f4f5 !important; }
.em-muted, .em-muted > div, .em-muted a { color: #a1a1aa !important; }
.em-link, .em-text a { color: #7a9bff !important; }
.em-wordmark, .em-wordmark > div, .em-wordmark a { color: #f4f4f5 !important; }
.em-subtle { background-color: #232327 !important; border-color: #2e2e33 !important; }
.em-border { border-color: #2e2e33 !important; }
.em-strong-border { border-color: #f4f4f5 !important; }
.em-rule, .em-rule p { border-top-color: #2e2e33 !important; }
.em-track { background-color: #232327 !important; }
.em-fill { background-color: #7a9bff !important; }
a.em-button, .em-button a, .em-button td[bgcolor] { background-color: #7a9bff !important; color: #0b0d14 !important; border-color: #7a9bff !important; }
a.em-button-danger, .em-button-danger a, .em-button-danger td[bgcolor] { background-color: #f97066 !important; color: #0b0d14 !important; border-color: #f97066 !important; }
.em-logo-light { display: none !important; }
.em-logo-dark { display: block !important; }
}
[data-ogsc] .em-card, [data-ogsc] .em-card > table, [data-ogsc] .em-card > table > tbody > tr > td { border-color: #2e2e33 !important; }
[data-ogsc] .em-text, [data-ogsc] .em-text > div { color: #f4f4f5 !important; }
[data-ogsc] .em-muted, [data-ogsc] .em-muted > div, [data-ogsc] .em-muted a { color: #a1a1aa !important; }
[data-ogsc] .em-link, [data-ogsc] .em-text a { color: #7a9bff !important; }
[data-ogsc] .em-wordmark, [data-ogsc] .em-wordmark > div, [data-ogsc] .em-wordmark a { color: #f4f4f5 !important; }
[data-ogsc] .em-subtle { border-color: #2e2e33 !important; }
[data-ogsc] .em-border { border-color: #2e2e33 !important; }
[data-ogsc] .em-strong-border { border-color: #f4f4f5 !important; }
[data-ogsc] .em-rule, [data-ogsc] .em-rule p { border-top-color: #2e2e33 !important; }
[data-ogsc] a.em-button, [data-ogsc] .em-button a, [data-ogsc] .em-button td[bgcolor] { color: #0b0d14 !important; border-color: #7a9bff !important; }
[data-ogsc] a.em-button-danger, [data-ogsc] .em-button-danger a, [data-ogsc] .em-button-danger td[bgcolor] { color: #0b0d14 !important; border-color: #f97066 !important; }
[data-ogsb] body, [data-ogsb] .em-page { background-color: #0e0e10 !important; }
[data-ogsb] .em-card, [data-ogsb] .em-card > table, [data-ogsb] .em-card > table > tbody > tr > td { background-color: #18181b !important; border-color: #2e2e33 !important; }
[data-ogsb] .em-subtle { background-color: #232327 !important; border-color: #2e2e33 !important; }
[data-ogsb] .em-border { border-color: #2e2e33 !important; }
[data-ogsb] .em-strong-border { border-color: #f4f4f5 !important; }
[data-ogsb] .em-rule, [data-ogsb] .em-rule p { border-top-color: #2e2e33 !important; }
[data-ogsb] .em-track { background-color: #232327 !important; }
[data-ogsb] .em-fill { background-color: #7a9bff !important; }
[data-ogsb] a.em-button, [data-ogsb] .em-button a, [data-ogsb] .em-button td[bgcolor] { background-color: #7a9bff !important; border-color: #7a9bff !important; }
[data-ogsb] a.em-button-danger, [data-ogsb] .em-button-danger a, [data-ogsb] .em-button-danger td[bgcolor] { background-color: #f97066 !important; border-color: #f97066 !important; }
@media only screen and (max-width: 600px) {
.em-card > table > tbody > tr > td { padding: 32px 24px !important; }
.em-h1, .em-h1 > div { font-size: 22px !important; line-height: 30px !important; }
}
</mj-style>
<!-- theme:end -->
</mj-head>
<mj-body>
<!-- header:start -->
<!-- Generated from theme.ts by `npm run build`. Edit theme.ts, not this block. -->
<mj-wrapper css-class="em-page">
<mj-section padding="0 8px 24px">
<mj-column>
<mj-text padding="0" font-size="18px" line-height="28px" font-weight="700" letter-spacing="-0.2px" css-class="em-wordmark"><a href="https://example.com" style="color:#18181b;text-decoration:none;">Acme</a></mj-text>
</mj-column>
</mj-section>
<!-- header:end -->
<mj-section mj-class="card">
<mj-column>
<mj-text mj-class="h1">Reset your password</mj-text>
<mj-text>Hi {{name}},</mj-text>
<mj-text>Someone asked to reset the password for your account. Choose a new one with the button below.</mj-text>
<mj-button href="{{reset_url}}">Choose a new password</mj-button>
<mj-text mj-class="small">This link expires in {{expires_in}} and can only be used once. If you didn't ask for a reset, you can ignore this email. Your password won't change.</mj-text>
<mj-divider />
<mj-text mj-class="small" padding="0">If the button doesn't work, paste this link into your browser:<br /><a class="em-link" href="{{reset_url}}" style="word-break:break-all;">{{reset_url}}</a></mj-text>
</mj-column>
</mj-section>
<mj-section padding="24px 8px 0">
<mj-column>
<mj-text mj-class="footer">You're receiving this because a password reset was requested for your account.</mj-text>
</mj-column>
</mj-section>
<!-- footer:start -->
<!-- Generated from theme.ts by `npm run build`. Edit theme.ts, not this block. -->
<mj-section padding="0 8px">
<mj-column>
<mj-text mj-class="footer">Acme Inc. · 100 Market Street · San Francisco, CA 94105</mj-text>
</mj-column>
</mj-section>
</mj-wrapper>
<!-- footer:end -->
</mj-body>
</mjml>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html dir="ltr" lang="en">
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<meta name="x-apple-disable-message-reformatting" />
<meta name="color-scheme" content="light dark" />
<meta name="supported-color-schemes" content="light dark" />
<title>
Use this link to choose a new password. It expires in {{expires_in}}.
</title>
<style>
:root { color-scheme: light dark; supported-color-schemes: light dark; }
.em-logo-dark { display: none; }
@media (prefers-color-scheme: dark) {
.em-page, .em-page > table > tbody > tr > td { background-color: #0e0e10 !important; }
.em-card { background-color: #18181b !important; border-color: #2e2e33 !important; }
.em-text, .em-text > div { color: #f4f4f5 !important; }
.em-muted, .em-muted > div, .em-muted a { color: #a1a1aa !important; }
.em-link, .em-text a { color: #7a9bff !important; }
.em-wordmark, .em-wordmark > div, .em-wordmark a { color: #f4f4f5 !important; }
.em-subtle { background-color: #232327 !important; border-color: #2e2e33 !important; }
.em-border { border-color: #2e2e33 !important; }
.em-strong-border { border-color: #f4f4f5 !important; }
.em-rule, .em-rule p { border-top-color: #2e2e33 !important; }
.em-track { background-color: #232327 !important; }
.em-fill { background-color: #7a9bff !important; }
a.em-button, .em-button a, .em-button td[bgcolor] { background-color: #7a9bff !important; color: #0b0d14 !important; border-color: #7a9bff !important; }
a.em-button-danger, .em-button-danger a, .em-button-danger td[bgcolor] { background-color: #f97066 !important; color: #0b0d14 !important; border-color: #f97066 !important; }
.em-logo-light { display: none !important; }
.em-logo-dark { display: block !important; }
}
[data-ogsc] .em-card { border-color: #2e2e33 !important; }
[data-ogsc] .em-text, [data-ogsc] .em-text > div { color: #f4f4f5 !important; }
[data-ogsc] .em-muted, [data-ogsc] .em-muted > div, [data-ogsc] .em-muted a { color: #a1a1aa !important; }
[data-ogsc] .em-link, [data-ogsc] .em-text a { color: #7a9bff !important; }
[data-ogsc] .em-wordmark, [data-ogsc] .em-wordmark > div, [data-ogsc] .em-wordmark a { color: #f4f4f5 !important; }
[data-ogsc] .em-subtle { border-color: #2e2e33 !important; }
[data-ogsc] .em-border { border-color: #2e2e33 !important; }
[data-ogsc] .em-strong-border { border-color: #f4f4f5 !important; }
[data-ogsc] .em-rule, [data-ogsc] .em-rule p { border-top-color: #2e2e33 !important; }
[data-ogsc] a.em-button, [data-ogsc] .em-button a, [data-ogsc] .em-button td[bgcolor] { color: #0b0d14 !important; border-color: #7a9bff !important; }
[data-ogsc] a.em-button-danger, [data-ogsc] .em-button-danger a, [data-ogsc] .em-button-danger td[bgcolor] { color: #0b0d14 !important; border-color: #f97066 !important; }
[data-ogsb] .em-page, [data-ogsb] .em-page > table > tbody > tr > td { background-color: #0e0e10 !important; }
[data-ogsb] .em-card { background-color: #18181b !important; border-color: #2e2e33 !important; }
[data-ogsb] .em-subtle { background-color: #232327 !important; border-color: #2e2e33 !important; }
[data-ogsb] .em-border { border-color: #2e2e33 !important; }
[data-ogsb] .em-strong-border { border-color: #f4f4f5 !important; }
[data-ogsb] .em-rule, [data-ogsb] .em-rule p { border-top-color: #2e2e33 !important; }
[data-ogsb] .em-track { background-color: #232327 !important; }
[data-ogsb] .em-fill { background-color: #7a9bff !important; }
[data-ogsb] a.em-button, [data-ogsb] .em-button a, [data-ogsb] .em-button td[bgcolor] { background-color: #7a9bff !important; border-color: #7a9bff !important; }
[data-ogsb] a.em-button-danger, [data-ogsb] .em-button-danger a, [data-ogsb] .em-button-danger td[bgcolor] { background-color: #f97066 !important; border-color: #f97066 !important; }
@media only screen and (max-width: 600px) {
.em-card-body > tbody > tr > td { padding: 32px 24px !important; }
.em-h1, .em-h1 > div { font-size: 22px !important; line-height: 30px !important; }
}
</style>
</head>
<body
class="em-page"
dir="ltr"
lang="en"
style="background-color:#f4f4f5;margin:0;padding:0">
<div
style="display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0"
data-skip-in-text="true">
Use this link to choose a new password. It expires in {{expires_in}}.
<div>
</div>
</div>
<table
border="0"
width="100%"
cellpadding="0"
cellspacing="0"
role="presentation"
align="center">
<tbody>
<tr>
<td
dir="ltr"
lang="en"
style="margin:0;padding:0;background-color:#f4f4f5;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;-webkit-font-smoothing:antialiased">
<table
align="center"
width="100%"
class="em-page"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="max-width:560px;width:100%;margin:0 auto">
<tbody>
<tr style="width:100%">
<td style="padding:40px 16px">
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation">
<tbody>
<tr>
<td style="padding:0 8px 24px">
<a
href="https://example.com"
style="color:#067df7;text-decoration-line:none;text-decoration:none"
target="_blank"
><p
class="em-wordmark"
style="font-size:18px;line-height:28px;margin:0;font-weight:700;letter-spacing:-0.2px;color:#18181b;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">
Acme
</p></a
>
</td>
</tr>
</tbody>
</table>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
class="em-card"
style="background-color:#ffffff;border:1px solid #e4e4e7;border-radius:12px">
<tbody>
<tr>
<td>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
class="em-card-body">
<tbody>
<tr>
<td style="padding:40px 40px">
<h1
class="em-text em-h1"
style="margin:0 0 16px;font-size:24px;line-height:32px;font-weight:650;letter-spacing:-0.3px;color:#18181b">
Reset your password
</h1>
<p
class="em-text"
style="font-size:15px;line-height:24px;margin:0 0 16px;color:#18181b;margin-top:0;margin-right:0;margin-bottom:16px;margin-left:0">
Hi {{name}},
</p>
<p
class="em-text"
style="font-size:15px;line-height:24px;margin:0 0 16px;color:#18181b;margin-top:0;margin-right:0;margin-bottom:16px;margin-left:0">
Someone asked to reset the password for
your account. Choose a new one with the
button below.
</p>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="margin:8px 0 24px">
<tbody>
<tr>
<td>
<a
class="em-button"
href="{{reset_url}}"
style="line-height:20px;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px;background-color:#2f5bd3;color:#ffffff;font-size:15px;font-weight:600;border-radius:8px;padding:12px 22px;padding-top:12px;padding-right:22px;padding-bottom:12px;padding-left:22px"
target="_blank"
><span
><!--[if mso]><i style="mso-font-width:366.66666666666663%;mso-text-raise:18px" hidden>   </i><![endif]--></span
><span
style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:9px"
>Choose a new password</span
><span
><!--[if mso]><i style="mso-font-width:366.66666666666663%" hidden>   ​</i><![endif]--></span
></a
>
</td>
</tr>
</tbody>
</table>
<p
class="em-muted"
style="font-size:13px;line-height:20px;margin:0 0 12px;color:#52525b;margin-top:0;margin-right:0;margin-bottom:12px;margin-left:0">
This link expires in {{expires_in}} and
can only be used once. If you didn't
ask for a reset, you can ignore this
email. Your password won't change.
</p>
<hr
class="em-rule"
style="width:100%;border:none;border-color:transparent;border-top:1px solid #e4e4e7;margin:28px 0" />
<p
class="em-muted"
style="font-size:13px;line-height:20px;margin:0 0 0;color:#52525b;word-break:break-all;margin-top:0;margin-right:0;margin-bottom:0;margin-left:0">
If the button doesn't work, paste
this link into your browser:<br /><a
class="em-link"
href="{{reset_url}}"
style="color:#2f5bd3;text-decoration-line:none;text-decoration:underline"
target="_blank"
>{{reset_url}}</a
>
</p>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="text-align:center">
<tbody>
<tr>
<td style="padding:24px 8px 0">
<p
class="em-muted"
style="font-size:12px;line-height:18px;margin:0 0 6px;color:#52525b;margin-top:0;margin-right:0;margin-bottom:6px;margin-left:0">
You're receiving this because a password
reset was requested for your account.
</p>
<p
class="em-muted"
style="font-size:12px;line-height:18px;margin:0 0 6px;color:#52525b;margin-top:0;margin-right:0;margin-bottom:6px;margin-left:0">
Acme Inc. · 100 Market Street · San Francisco, CA
94105
</p>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</body>
</html>
Reset your password
Hi {{name}},
Someone asked to reset the password for your account. Choose a new one here:
{{reset_url}}
This link expires in {{expires_in}} and can only be used once. If you didn't ask for a reset, you can ignore this email. Your password won't change.
--
Acme
Acme Inc. · 100 Market Street · San Francisco, CA 94105
# Create the template in your account once
npx -y --package=groundmail-cli -- groundmail templates push templates/password-reset/password-reset.html \
--alias password-reset \
--name "Password reset" \
--subject "Reset your Acme password" \
--text templates/password-reset/password-reset.txt \
--publish
// Then send it by alias from your app
import { Groundmail } from 'groundmail'
const groundmail = new Groundmail(process.env.GROUNDMAIL_API_KEY!)
await groundmail.emails.send({
from: 'Acme <noreply@yourdomain.com>',
to: 'ada@example.com',
template: {
id: 'password-reset',
variables: {
name: 'Ada',
reset_url: 'https://example.com/reset-password?token=8c1f2e',
expires_in: '60 minutes',
},
},
})password-reset.tsx, 54 lines, shown in full (scroll inside the panel). Typed props. Copy it with theme.ts and components/ from the repo. View on GitHub
Send it with Groundmail
Create the template in your account once, then send it by alias from your app.
npx -y --package=groundmail-cli -- groundmail templates push templates/password-reset/password-reset.html \
--alias password-reset \
--name "Password reset" \
--subject "Reset your Acme password" \
--text templates/password-reset/password-reset.txt \
--publishimport { Groundmail } from 'groundmail'
const groundmail = new Groundmail(process.env.GROUNDMAIL_API_KEY!)
await groundmail.emails.send({
from: 'Acme <noreply@yourdomain.com>',
to: 'ada@example.com',
template: {
id: 'password-reset',
variables: {
name: 'Ada',
reset_url: 'https://example.com/reset-password?token=8c1f2e',
expires_in: '60 minutes',
},
},
})The CLI command and the Node.js send from the template's README, which also has Python, PHP and cURL. View on GitHub
Subject lines
Name your product and say what the email does. People look for it seconds after clicking "Forgot password", so plain beats clever.
- Reset your Acme passwordDefaultNames the product and the action. Easy to find in a search of the inbox.
- Your Acme password reset linkGoodSame job, worded around the link they asked for.
- Acme: choose a new passwordGoodLeads with the brand, which helps when people scan by sender.
- Urgent: action required on your accountAvoidUrgency and vagueness are what phishing looks like, to people and to spam filters.
A reset email that lands in spam is a locked-out user
The template is only half of it. The email has to arrive in seconds and reach the inbox. That comes down to the domain you send from and the servers that send it.
Why password reset emails go to spam →- Send from your own domain with SPF, DKIM and DMARC set up.
- Keep it on a transactional stream, apart from newsletters and marketing.
- Watch bounces: a reset sent to an address that bounced last week will bounce again.
- Groundmail sends this template by alias from your own domain, on our own mail servers. Send it with Groundmail
More authentication templates
All email templates →Questions
Is it free to use?
Yes. The templates are MIT-licensed: use them in commercial products and change them however you like.
Do I need Groundmail to use it?
No. The HTML and text files use plain placeholders that work with most providers' template engines and with Handlebars or Mustache. Sending through Groundmail just saves a step.
Does it work in dark mode and Outlook?
Yes. The full HTML file carries dark-mode styles for Apple Mail and Outlook.com, and the extra markup desktop Outlook needs to draw the button.
How do I change the colors and logo?
Edit theme.ts once (name, logo, address and colors for light and dark) and rebuild. Every template in the set picks it up.
How long should a reset link last?
Long enough to switch to the inbox and back, short enough that an old email is useless: the template's example is 60 minutes. The expiry is a plain {{expires_in}} variable, so pass whatever your backend enforces and the email says the same.