DHxWhy

QR Code Scan Login Form with Password Fallback

Sign-in 82

About

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>
  );
}

Dependencies

More by DHxWhy

View all
  • Sun Moon Dark Mode Theme Toggle

    Sun Moon Dark Mode Theme Toggle

    186
  • Live Notification Feed

    Live Notification Feed

    110
  • Country Flags Globe

    Country Flags Globe

    110
  • Airplane Window Gallery

    Airplane Window Gallery

    96
  • Domino Scroll Image Gallery

    Domino Scroll Image Gallery

    93
  • Constellation Image Gallery

    Constellation Image Gallery

    80
  • Ferris Wheel Image Gallery

    Ferris Wheel Image Gallery

    73
  • What's New Changelog Timeline

    What's New Changelog Timeline

    69

More in Sign-in

View all
  • Modern & Stunning sign in

    Modern & Stunning sign in

    1.6K
  • SignInPage

    SignInPage

    631
  • Sign In Form

    Sign In Form

    201
  • Clean & Minimal sign in

    Clean & Minimal sign in

    617
  • Sign In

    Sign In

    97
  • Split Sign In Form

    Split Sign In Form

    16
  • Authentication Card

    Authentication Card

    47
  • Modern Animated Sign In

    Modern Animated Sign In

    1.1K