Backup Codes

Makes users save their recovery codes and type one back before continuing, so they aren't locked out later by codes they never kept.

StopsLocked out with unsaved backup codes

Installation

pnpm dlx shadcn@latest add https://bollard-ui.vercel.app/r/backup-codes.json

The files land in your project. They're yours to edit.

Usage

backup-codes-usage.tsx
import { BackupCodes } from "@/components/ui/backup-codes"

<BackupCodes codes={codes} account="Acme · tushar@example.com" fileName="acme-recovery-codes.txt" warnOnLeave={false} onContinue={() => setDone(true)} />

What it catches

  • Moving on without saving the codes by download, print or copy
  • A saved copy that can't be read back, by asking for one code
  • Paste into the check field (refused unless allowPaste)
  • Leaving the page before finishing (beforeunload warning, on by default)

Props

BackupCodes props
PropTypeDefault
codes* (required)

The one-time recovery codes, shown once.

string[]—
account* (required)

Who the codes belong to, written at the top of the file, e.g. "Acme · tushar@example.com".

string—
onContinue* (required)

Called after every required save and the spot check. May return a promise.

() => void | Promise<void>—
require

Which saves unlock the check: any one of them (default), or every method. One real copy is enough to recover; "all" also forces a printer most people don't have.

"all" | "any""any"
checkIndex

Zero-based code the user must type back. Defaults to a stable pick from the set.

number—
allowPaste

Let the user paste into the spot check (off by default, so it can't be passed from the clipboard). Turn it on when your users rely on paste, e.g. for motor or cognitive access needs (WCAG 2.2 SC 3.3.8).

booleanfalse
warnOnLeave

Warn before the tab closes until the user continues.

booleantrue
fileName

Download file name.

string"recovery-codes.txt"
title

Templates: `{n}` is the 1-based code number, `{count}` the number of codes, `{left}` the saves still to do.

string"Save your recovery codes"
description

Text under the title.

string"If you lose your phone, each code gets you in once. You …
fileNote

Note written into the downloaded file.

string"Recovery codes. Each code works once. Keep them somewher…
downloadLabel

Download button text.

string"Download"
printLabel

Print button text.

string"Print"
copyLabel

Copy button text.

string"Copy"
copyFailedText

Shown when copying to the clipboard fails.

string"Copy was blocked. Select the codes and press Ctrl+C."
saveFirstText

Shown while more save methods are needed ({left} is filled in).

string"Still to do before you continue: {left}."
saveAnyText

Shown before any save method was used.

string"Download, print or copy the codes to continue."
unlockedText

Announced once the check unlocks, so screen reader users know the next step appeared.

string"Saved. One last step: type code #{n} below."
checkLabel

Label of the type-back field ({n} is the code's number).

string"Type code #{n} from where you saved it"
checkHint

Hint under the type-back field.

string"This proves your copy is readable. The code stays valid."
pasteBlockedText

Shown when a paste into the check field is refused.

string"Type it rather than paste, so we know your saved copy wo…
mismatchText

Shown when the typed code does not match.

string"That isn't code #{n}. Check your saved copy."
matchText

Shown when the typed code matches.

string"Code #{n} matches."
continueLabel

Continue button text.

string"Continue"

* required. Also accepts the native props of <section> (className, aria-*, data-*…), passed through to it.

Accessibility

  • Codes are an ordered list labelled by the title; save buttons use aria-pressed
  • The still-locked Continue uses aria-disabled with aria-describedby to what is left to do
  • The check field has a <label>, aria-invalid on a wrong length and an aria-live hint
  • Progress messages are aria-live="polite"

Only what the source does. Test with your own screen reader and content before shipping.

Limitations

This is a UX guard rail. Enforce on the server: Store only hashes of the codes, make each one single-use, and rate-limit recovery attempts on the server.

  • It knows a download, print or copy was started, not that the file was kept
  • Typing one code back proves one code was readable, not that all were saved

What it stops

Locked out with unsaved backup codes

Also known as

backup codes · recovery codes · 2fa setup · mfa · totp · download codes · authenticator setup

Dependencies

Pairs well with