Source Code

<div class="container py-5 d-flex justify-content-center">
  <div class="card bs2fb-card">
    <div class="card-body p-4">
      <h5 class="fw-bold mb-1">Your backup codes</h5>
      <p class="small text-muted mb-3">Each code can only be used once. Click a code to mark it used, the way redeeming one during sign-in would.</p>

      <div class="row row-cols-2 g-2 mb-3" id="bs2fbGrid"></div>

      <p class="small fw-semibold mb-3"><span id="bs2fbRemaining">10</span> of 10 codes remaining</p>

      <div class="d-flex flex-wrap gap-2 mb-3">
        <button type="button" class="btn btn-sm btn-outline-secondary" id="bs2fbCopy">Copy all</button>
        <button type="button" class="btn btn-sm btn-outline-secondary" id="bs2fbDownload">Download .txt</button>
        <button type="button" class="btn btn-sm btn-outline-danger" id="bs2fbRegenBtn">Regenerate codes</button>
      </div>
      <p class="small mb-0" id="bs2fbFeedback">&nbsp;</p>
    </div>
  </div>
</div>

<div class="modal fade" id="bs2fbModal" tabindex="-1" aria-hidden="true">
  <div class="modal-dialog modal-dialog-centered">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title fw-bold">Regenerate backup codes?</h5>
        <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
      </div>
      <div class="modal-body">
        <p class="mb-0">Your 10 current codes will stop working immediately, including any you haven't used yet.</p>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
        <button type="button" class="btn btn-danger fw-bold" id="bs2fbRegenConfirm">Regenerate</button>
      </div>
    </div>
  </div>
</div>

Bootstrap Two-Factor Backup Codes Display — Free HTML CSS JS Snippet

Bootstrap Two-Factor Backup Codes Display · Forms · Plain HTML, CSS & JS · Live preview

What's included

Features

Each code tracks its own used state instead of relying on a separate removed-codes list
An ambiguity-free character set (no 0/O or 1/I) for codes people transcribe by hand
Clicking an already-used code is a real no-op, not just a visual style with no logic behind it
A destructive regenerate action gated behind a genuine confirmation modal
Copy-all and download actions read from the same codes array the grid renders from
One generateCodes() function defines the code format for both first load and regeneration

About this UI Snippet

Bootstrap Two-Factor Backup Codes Display — HTML, CSS & JavaScript

Screenshot of the Bootstrap Two-Factor Backup Codes Display snippet rendered live

Backup codes are single-use by definition, so the display has to make a redeemed code permanently distinguishable from an unredeemed one — this snippet models each code as an object with its own used boolean rather than just a flat array of strings, specifically so renderGrid() has somewhere to read that state from every time it repaints. Clicking a code that's already used is a no-op guarded directly in the click handler, matching how a real backup code can't be spent twice.

generateCodes() builds each code from a deliberately reduced character set — no 0, O, 1, or I — because backup codes are the kind of thing a person often reads off a screen and types into a different device by hand, and ambiguous characters turn that into a guessing game. The same function is reused for the initial 10 codes and for a full regeneration, so there's exactly one place that defines what a valid code looks like.

Regenerating is destructive — it invalidates every code, used or not — so it's gated behind a real Bootstrap modal rather than firing on a single click, the same pattern this collection uses for bootstrap-type-confirm-delete-modal. The download button builds a real Blob/URL.createObjectURL text file rather than faking the interaction, though like any snippet that triggers a file download, a sandboxed preview iframe without allow-downloads may block the click silently — it works normally on a real, non-sandboxed page.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Hand this snippet to an AI coding assistant like Claude and ask it to add a "Print" action using window.print() with a print-specific stylesheet that hides the buttons, or to add a masked default view where codes stay hidden behind a "Reveal codes" button until clicked, for an extra step before sensitive codes are shown on screen.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a Bootstrap 5.3 two-factor authentication backup codes display, using the real Bootstrap CDN framework (bootstrap.min.css and bootstrap.bundle.min.js), not custom CSS made to resemble it.

Requirements:
- Generate 10 codes in an easily-transcribed format (e.g. XXXX-XXXX), using a character set that excludes visually ambiguous characters like 0/O and 1/I.
- Display them in a grid where each code is individually clickable.
- Clicking an unused code marks it permanently used (struck through, no longer clickable) — track each code's used state individually rather than in a separate list.
- Show a live count of how many codes remain unused.
- Add a "Copy all" button that copies every code as plain text, and a "Download .txt" button that downloads them as a real text file using a Blob and an object URL.
- Add a "Regenerate codes" button that opens a Bootstrap confirmation modal warning that the current codes (including unused ones) will stop working, and only replaces the full set with a new batch after the user confirms.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Load the snippetTen freshly generated codes appear in a grid, all unused.
  2. 2
    Click any codeIt gets struck through and grayed out, and the remaining count drops by one.
  3. 3
    Click that same code againNothing happens — a used code can't be clicked back to unused, matching real single-use behavior.
  4. 4
    Click "Copy all"Every code (used or not) is copied to the clipboard as plain text, one per line.
  5. 5
    Click "Regenerate codes"A confirmation modal warns that the current codes will stop working before anything changes.
  6. 6
    Confirm regenerationA brand-new set of 10 unused codes replaces the old one, and the remaining count resets to 10.

Real-world uses

Common Use Cases

Two-factor authentication setup flows
Show immediately after a user enables 2FA, pairing with bootstrap-otp-login-form or bootstrap-2fa-verification-form for the sign-in side.
Account security settings pages
A "View backup codes" action inside a settings panel, alongside session and device management.
Learning single-use-token UI patterns
The used/unused-per-item pattern here generalizes to invite codes, one-time discount codes, and redeemable vouchers.

Got questions?

Frequently Asked Questions

This demo models what happens server-side when a backup code is redeemed during a real sign-in — the point is showing the code becoming permanently unusable, not building a per-code clipboard action (Copy all already covers the copy use case).

It builds a real Blob and triggers a real download link, which works normally in an actual page. A sandboxed iframe without the allow-downloads permission (some embedded previews) can silently block a script-triggered download click, which is a browser sandbox restriction, not a bug in the code.

Backup codes are frequently read off one screen and typed into another by hand — those characters are easy to visually confuse in many fonts, so leaving them out removes an entire category of transcription errors.

Yes. Move the codes array into component state (useState, ref, or a signal) and replace the direct grid.innerHTML rebuild with a mapped list render; the generateCodes and used-toggle logic itself needs no changes.

Yes — that matches how real backup-code systems work, since keeping some old codes valid after a regeneration would defeat the purpose of a "these are compromised, replace them" action, which is why the confirmation modal calls that out explicitly.