Componentpickers1.71 KB gzipZero dependencies
Two Factor
2FA setup panel with animated QR scan line, recovery codes and copy action.
Preview
Live Preview
Two-Factor Authentication
Scan the QR code with your authenticator application.
Recovery Codes
4F7A-9K2MB3X8-QP1DM2VD-88ZLT9RC-55WNJ6HB-33YFW4KN-77TSP8QX-21GBR5ZM-66JVInstallation
Tokens guide →Add to your project via CLI (zero dependencies, code you own):
npx bigbullui add two-factorOr install the full npm package:
npm install bigbulluiUsage
Open in StackBlitzimport { TwoFactor } from "@/components/ui/two-factor";
<TwoFactor />Usage guidance
Use Two Factor when a ticket stub surface needs 2FA setup panel with animated QR scan line, recovery codes and copy action. Keep props minimal and prefer composition with sibling stubs.
Avoid Two Factor for long form flows or dense data grids where a dedicated layout block fits better.
Source code
two-factor.tsxZero dependencies (only React + utils). Copy and paste directly into your project:
"use client";
import * as React from "react";
import { cn } from "./lib/utils";
export interface TwoFactorProps extends React.HTMLAttributes<HTMLDivElement> {
/** Backup recovery codes; displays generated sample if omitted */
recoveryCodes?: string[];
onConfirm?: () => void;
}
/** Two-factor authentication setup: QR code + recovery backup codes. */
export function TwoFactor({ recoveryCodes, onConfirm, className, ...props }: TwoFactorProps) {
const codes = recoveryCodes ?? [
"4F7A-9K2M", "B3X8-QP1D", "M2VD-88ZL", "T9RC-55WN",
"J6HB-33YF", "W4KN-77TS", "P8QX-21GB", "R5ZM-66JV",
];
const [revealed, setRevealed] = React.useState(false);
const [copied, setCopied] = React.useState(false);
const copyAll = () => {
try { void navigator.clipboard?.writeText(codes.join("\n")); } catch { /* yoksay */ }
setCopied(true);
setTimeout(() => setCopied(false), 1800);
};
return (
<div className={cn("w-full max-w-sm space-y-5", className)} {...props}>
<style>{`
@keyframes twofaScan {
0% { top: 4%; opacity: 0.9; }
50% { top: 92%; opacity: 1; }
100% { top: 4%; opacity: 0.9; }
}
@keyframes twofaIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
`}</style>
<div className="text-center">
<h3 className="text-lg font-semibold tracking-tight">Two-Factor Authentication</h3>
<p className="mt-1 text-sm text-muted-foreground">Scan the QR code with your authenticator application.</p>
</div>
{/* QR simulator */}
<div className="relative mx-auto size-40 overflow-hidden rounded-lg border-2 border-dashed border-border bg-card p-3">
<div className="grid size-full grid-cols-8 gap-0.5" aria-hidden="true">
{Array.from({ length: 64 }).map((_, i) => {
const on = ((i * 7 + 13) % 17) % 3 !== 0;
return <div key={i} className={cn("rounded-[1px]", on ? "bg-foreground" : "bg-transparent")} style={{ animationDelay: `${i * 12}ms` }} />;
})}
</div>
<div className="absolute left-2 right-2 h-0.5 bg-destructive/80" style={{ animation: "twofaScan 2.4s ease-in-out infinite" }} aria-hidden="true" />
</div>
{/* Recovery codes */}
<div className="rounded-md border border-border bg-secondary/40 p-3 animate-[twofaIn_0.3s_ease-out_0.15s_both] motion-reduce:animate-none">
<div className="flex items-center justify-between">
<span className="font-mono text-[10px] uppercase tracking-[0.15em] text-muted-foreground">Recovery Codes</span>
<button
onClick={() => setRevealed((r) => !r)}
className="font-mono text-[9px] uppercase tracking-wider text-accent hover:underline focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring rounded-sm"
>
{revealed ? "HIDE" : "SHOW"}
</button>
</div>
<div className={cn("mt-2 grid grid-cols-2 gap-1 transition-all duration-300 motion-reduce:transition-none", revealed ? "opacity-100" : "opacity-0 select-none blur-sm")}>
{codes.map((c, i) => (
<code key={c} className="font-mono text-[11px] text-foreground/80" style={{ animationDelay: `${i * 30}ms` }}>{c}</code>
))}
</div>
<button
onClick={copyAll}
className="mt-2 w-full rounded-sm border border-dashed border-border py-1 font-mono text-[9px] uppercase tracking-wider text-muted-foreground transition-colors hover:border-foreground/40 hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring motion-reduce:transition-none"
>
{copied ? "✓ COPIED" : "COPY ALL"}
</button>
</div>
<button
onClick={onConfirm}
className={cn(
"w-full rounded-md bg-accent px-4 py-2.5 text-sm font-semibold uppercase tracking-widest text-accent-foreground",
"transition-all duration-150 hover:bg-accent/90 active:scale-[0.98]",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none",
"animate-[twofaIn_0.3s_ease-out_0.25s_both] motion-reduce:animate-none"
)}
>
Enable 2FA
</button>
</div>
);
}
Props
| Prop | Type | Description |
|---|---|---|
| recoveryCodes | string[] | Recovery codes; displays generated sample if omitted |
| onConfirm | () => void |