QR Code Scan Login Form with Password Fallback for React and Tailwind CSS (shadcn/ui) pairs QR scanning with email sign-in. It is built to help people move from phone to desktop. Includes scan motion, confirmation, expiry, refresh, and a sliding password form; controlled status and callbacks; keyboard access, reduced motion, and light/dark themes. Use it for SaaS and app logins. The included demo simulates sign-in; the component makes no requests. Requires React and lucide-react, with no animation library.
Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds QR Code Scan Login Form with Password Fallback to your project and fits it to your theme.
Code
How QR Code Scan Login Form with Password Fallback is used: the demo's code. The component's own source comes with a free account.
Usage.tsx
"use client";
import * as React from "react";
import { Pause, Play, RotateCcw } from "lucide-react";
import { QrCodeScanLogin } from "@/components/ui/qr-code-scan-login";
import type { QrCodeLoginStatus } from "@/components/ui/qr-code-scan-login";
// Generated with qrcode 1.5.4. Payload: https://example.com/demo (no authentication).
// Regenerate with review/original/variants/qr-code-scan-login/generate-qr.cjs.
export const DEMO_QR_MODULES: boolean[][] = /* QR_MODULES_START */ [
[true,true,true,true,true,true,true,false,false,false,true,true,true,true,false,false,true,false,true,true,true,true,true,true,true],
[true,false,false,false,false,false,true,false,false,true,true,true,true,true,true,true,true,false,true,false,false,false,false,false,true],
[true,false,true,true,true,false,true,false,true,false,false,false,true,false,true,false,false,false,true,false,true,true,true,false,true],
[true,false,true,true,true,false,true,false,true,true,false,false,true,true,true,true,false,false,true,false,true,true,true,false,true],
[true,false,true,true,true,false,true,false,true,false,true,true,false,true,false,false,true,false,true,false,true,true,true,false,true],
[true,false,false,false,false,false,true,false,true,false,true,false,true,false,true,true,false,false,true,false,false,false,false,false,true],
[true,true,true,true,true,true,true,false,true,false,true,false,true,false,true,false,true,false,true,true,true,true,true,true,true],
[false,false,false,false,false,false,false,false,true,true,false,false,true,false,true,false,true,false,false,false,false,false,false,false,false],
[true,false,true,true,true,true,true,false,false,false,true,false,false,true,false,false,false,false,true,true,true,true,true,false,false],
[false,true,true,false,false,true,false,true,true,true,false,true,false,true,false,false,false,true,false,true,false,false,false,true,false],
[true,false,true,false,false,true,true,true,true,true,true,true,true,true,true,true,true,false,false,true,false,true,false,true,true],
[true,true,false,true,false,true,false,false,false,false,true,false,true,false,true,true,true,false,true,true,false,false,false,false,true],
[true,true,false,false,false,true,true,true,true,false,true,true,false,true,true,true,false,true,true,false,true,false,true,true,true],
[true,true,true,true,true,true,false,false,true,false,true,false,true,false,false,false,true,false,false,true,false,true,false,true,false],
[true,false,true,false,false,false,true,true,true,false,true,false,false,false,true,true,false,false,true,true,true,true,false,true,true],
[true,false,false,true,false,true,false,true,true,true,true,false,true,false,false,true,true,true,true,true,true,false,false,false,true],
[true,false,true,true,true,false,true,true,true,true,true,false,true,true,true,false,true,true,true,true,true,false,true,false,false],
[false,false,false,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,true,true,false,false,false],
[true,true,true,true,true,true,true,false,false,false,false,true,true,false,true,false,true,false,true,false,true,false,true,true,true],
[true,false,false,false,false,false,true,false,true,false,false,true,false,false,true,false,true,false,false,false,true,true,false,false,true],
[true,false,true,true,true,false,true,false,true,true,false,true,true,true,true,true,true,true,true,true,true,false,true,false,false],
[true,false,true,true,true,false,true,false,true,true,false,false,true,true,false,true,false,true,true,false,true,true,true,true,true],
[true,false,true,true,true,false,true,false,true,true,false,false,false,false,true,true,false,false,false,false,false,true,true,false,true],
[true,false,false,false,false,false,true,false,false,true,false,true,false,false,true,false,true,true,false,true,true,true,false,false,true],
[true,true,true,true,true,true,true,false,true,false,false,true,true,true,true,false,false,true,true,true,true,true,true,true,true]
] /* QR_MODULES_END */;
type DemoProps = { autoplay?: boolean };
export default function QrCodeScanLoginDemo({ autoplay = true }: DemoProps = {}) {
const [status, setStatus] = React.useState("waiting" as QrCodeLoginStatus);
const [passwordOpen, setPasswordOpen] = React.useState(false);
const [paused, setPaused] = React.useState(false);
const [reduced, setReduced] = React.useState(true);
const [ready, setReady] = React.useState(false);
const [cycle, setCycle] = React.useState(0);
const [message, setMessage] = React.useState("");
const playing = autoplay && ready && !reduced && !paused;
const stop = () => setPaused(true);
React.useLayoutEffect(() => {
const media = window.matchMedia("(prefers-reduced-motion: reduce)");
const update = () => setReduced(media.matches);
update();
setReady(true);
media.addEventListener("change", update);
return () => media.removeEventListener("change", update);
}, []);
React.useEffect(() => {
if (!playing) return;
const timers = [
window.setTimeout(() => setStatus("scanned"), 2600),
window.setTimeout(() => setStatus("confirmed"), 4400),
window.setTimeout(() => { setStatus("waiting"); setPasswordOpen(true); }, 6000),
window.setTimeout(() => { setStatus("waiting"); setPasswordOpen(false); setCycle((value) => value + 1); }, 7200),
];
return () => timers.forEach((timer) => window.clearTimeout(timer));
}, [playing, cycle]);
const replay = () => {
setStatus("waiting");
setPasswordOpen(false);
setMessage("");
setCycle((value) => value + 1);
setPaused(false);
};
return (
<main data-qr-demo data-playing={playing} data-autoplay={autoplay ? "on" : "off"} className="flex min-h-svh w-full flex-col items-center justify-center gap-4 bg-muted/40 px-5 py-5 text-foreground">
<div className="flex w-full max-w-[660px] items-center justify-between gap-3 px-1"><p className="text-xs font-medium tracking-tight">Your next session starts here.</p><span className="rounded-full border border-border px-2 py-1 text-[9px] font-medium uppercase tracking-[.16em] text-muted-foreground">Demo</span></div>
<div className="w-full max-w-[660px]" onPointerDownCapture={stop} onKeyDownCapture={stop}>
<QrCodeScanLogin
qrModules={DEMO_QR_MODULES}
appName="Wayline"
logo={<svg viewBox="0 0 24 24" fill="none" className="size-6"><path d="m4 7 4 11 4-8 4 8 4-11" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" /></svg>}
status={status}
onStatusChange={setStatus}
codeId={cycle}
deviceName="iPhone"
welcomeMessage="Welcome back, Maria"
autoplay={playing}
passwordOpen={passwordOpen}
onPasswordOpenChange={setPasswordOpen}
onUsePassword={stop}
onRefresh={() => { setStatus("waiting"); setCycle((value) => value + 1); }}
onPasswordSubmit={() => setMessage("Demo only · no sign-in request was sent")}
/>
</div>
<div className="flex w-full max-w-[660px] items-center justify-between gap-2 px-1 text-[10px] text-muted-foreground">
<span data-demo-note role="status" className="min-w-0">{message || "Sample workspace · No real sign-in"}</span>
<div className="flex shrink-0 items-center gap-3">
{autoplay && !reduced && <button type="button" data-pause aria-label={playing ? "Pause demo" : "Play demo"} onClick={() => playing ? stop() : replay()} className="flex items-center gap-1 rounded p-1 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">{playing ? <Pause className="size-3" aria-hidden="true" /> : <Play className="size-3" aria-hidden="true" />}{playing ? "Pause" : "Play"}</button>}
<button type="button" data-replay onClick={replay} className="flex items-center gap-1 rounded p-1 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"><RotateCcw className="size-3" aria-hidden="true" />Replay</button>
</div>
</div>
</main>
);
}
















