Skip to content
Groundmail

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.

  1. 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.

  2. 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.

  3. An "ignore this" line

    Many resets nobody asked for are typos or someone guessing. Tell people their password won't change unless they click.

  4. 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.

  5. A link people can paste

    Some clients strip or block buttons. The full URL under the button means nobody is stuck.

  6. 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.`,
  )
}

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.

1Terminal
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
2Node.js
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',
    },
  },
})

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

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.