DHxWhy

Workspace Picker Login Form

Sign-in 7

About

Workspace picker login form for React and Tailwind CSS (shadcn/ui) that turns one email into a colorful list of the workspaces a person belongs to, Slack-style. It is built to get people into the right workspace with one email and one tap, instead of remembering a URL. Type an email, see a looking-up skeleton, then a staggered list of workspaces with gradient initial tiles, addresses, member counts, last-used hints, and sign-in method badges (SSO, passkey, or password). Pick one and it rises to the top with a FLIP transition while the matching continue button appears; a lone workspace is chosen automatically, and "Not you?" returns to the email step. Supports controlled email, status, workspaces, and selection, lookup and continue callbacks, keyboard navigation with arrow keys, listbox semantics with live announcements, reduced motion, and light and dark themes. Use it for multi-tenant SaaS sign-in, team switchers, and enterprise login pages. The included demo scripts a sample flow; 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 Workspace Picker Login Form to your project and fits it to your theme.

Code

How Workspace Picker Login Form 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 { WorkspacePickerLogin } from "@/components/ui/workspace-picker-login";
import type { Workspace, WorkspaceStatus } from "@/components/ui/workspace-picker-login";

const EMAIL = "maria@acme.com";
export const DEMO_WORKSPACES: Workspace[] = [
  { id: "acme", name: "Acme Design", address: "acme.workspace.com", members: 48, method: "sso", tone: 1 },
  { id: "northwind", name: "Northwind Labs", address: "northwind.workspace.com", members: 12, lastUsed: "2d ago", method: "passkey", tone: 2 },
  { id: "personal", name: "Maria's Notes", address: "maria.workspace.com", members: 1, method: "password", tone: 4 },
];

type DemoProps = { autoplay?: boolean; bare?: boolean };
type DemoState = { status: WorkspaceStatus; email: string; workspaces: Workspace[]; selected: string | null; welcome?: string };
const initial = (): DemoState => ({ status: "idle", email: "", workspaces: [], selected: null });

export default function WorkspacePickerDemo({ autoplay = true, bare = false }: DemoProps) {
  const [state, setState] = React.useState(initial);
  const [motion, setMotion] = React.useState({ ready: false, reduced: true });
  const [run, setRun] = React.useState({ id: 0, auto: true });
  const [paused, setPaused] = React.useState(false);

  React.useEffect(() => {
    const media = window.matchMedia("(prefers-reduced-motion: reduce)");
    const update = () => setMotion({ ready: true, reduced: media.matches });
    update();
    media.addEventListener("change", update);
    return () => media.removeEventListener("change", update);
  }, []);

  React.useEffect(() => {
    if (!motion.ready || !run.auto || !autoplay || motion.reduced || paused) return;
    const timers: number[] = [];
    const later = (ms: number, callback: () => void) => timers.push(window.setTimeout(callback, ms));
    setState(initial());
    for (let index = 1; index <= EMAIL.length; index += 1) later(400 + (index / EMAIL.length) * 1000, () => setState((previous) => ({ ...previous, email: EMAIL.slice(0, index) })));
    later(1700, () => setState((previous) => ({ ...previous, status: "looking" })));
    later(2400, () => setState((previous) => ({ ...previous, status: "ready", workspaces: DEMO_WORKSPACES })));
    later(4000, () => setState((previous) => ({ ...previous, selected: "northwind" })));
    later(5400, () => setState((previous) => ({ ...previous, status: "continuing" })));
    later(6100, () => setState((previous) => ({ ...previous, status: "done", welcome: "Welcome back, Maria" })));
    later(7600, () => setRun((previous) => ({ ...previous, id: previous.id + 1 })));
    return () => timers.forEach((timer) => window.clearTimeout(timer));
  }, [motion.ready, motion.reduced, autoplay, paused, run]);

  const takeOver = () => { setPaused(true); setRun((previous) => ({ ...previous, auto: false })); };

  return (
    <main className="flex min-h-svh w-full flex-col items-center justify-center gap-3 bg-background px-4 py-4 text-foreground">
      {!bare && <div className="flex w-full max-w-[400px] items-center justify-between px-1 text-[9px] font-medium uppercase tracking-[0.18em] text-muted-foreground"><span>One email · every workspace</span><span>Sample data</span></div>}
      <WorkspacePickerLogin
        appName="Workspace"
        email={state.email}
        onEmailChange={(email) => { takeOver(); setState((previous) => ({ ...previous, email })); }}
        status={state.status}
        workspaces={state.workspaces}
        selected={state.selected}
        onSelect={(selected) => { takeOver(); setState((previous) => ({ ...previous, selected })); }}
        onLookup={(email) => {
          takeOver();
          setState((previous) => ({ ...previous, email, status: "looking" }));
          window.setTimeout(() => setState((previous) => ({ ...previous, status: "ready", workspaces: email === EMAIL ? DEMO_WORKSPACES : [{ id: "personal", name: `${email.split("@")[0]}'s space`, members: 1, method: "password", tone: 3 }] })), 700);
        }}
        onContinue={(workspace) => {
          takeOver();
          setState((previous) => ({ ...previous, status: "continuing" }));
          window.setTimeout(() => setState((previous) => ({ ...previous, status: "done", welcome: `Welcome back to ${workspace.name}` })), 700);
        }}
        onChangeEmail={() => { takeOver(); setState(initial()); }}
        onCreateWorkspace={() => takeOver()}
        welcome={state.welcome}
        footer={<span>Sign in once. Switch workspaces any time.</span>}
      />
      {!bare && (
        <div className="flex items-center gap-2 text-[10px] text-muted-foreground">
          <span>{run.auto && !paused ? "Interactive preview" : "Try your own email"}</span>
          <span aria-hidden="true">·</span>
          <button type="button" data-replay onClick={() => { setPaused(false); setRun((previous) => ({ id: previous.id + 1, auto: true })); }} className="rounded px-1 underline decoration-border underline-offset-4 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">Replay</button>
        </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
  • QR Code Scan Login Form with Password Fallback

    QR Code Scan Login Form with Password Fallback

    82
  • Constellation Image Gallery

    Constellation Image Gallery

    80
  • Ferris Wheel Image Gallery

    Ferris Wheel Image Gallery

    73

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