# Sign In Minimal

> Editorial single-column sign-in with an optional magic-link mode.

A shadcn/ui block from the [@devsloka](https://ui.devsloka.in/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/devsloka/sign-in-minimal).

- Page, with a live preview: https://shadcn.exchange/r/devsloka/sign-in-minimal
- Screenshot: https://shadcn.exchange/shot/devsloka/sign-in-minimal/light.png
- Registry item: https://ui.devsloka.in/r/sign-in-minimal.json
- Listed: 2026-10-28, source last changed 2026-10-28

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/devsloka/sign-in-minimal.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`
- Registry items: `button`, `input`, `label`

## Files

### `src/registry/blocks/auth/sign-in-minimal.tsx`

```tsx
"use client";

import {
  useEffect,
  useId,
  useRef,
  useState,
  type ChangeEvent,
  type ComponentProps,
  type CSSProperties,
  type FormEvent,
} from "react";
import { Eye, EyeOff, Loader2 } from "lucide-react";

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";

export type SignInMinimalMethod = "password" | "magic-link";

export type SignInMinimalValues =
  | { method: "password"; email: string; password: string }
  | { method: "magic-link"; email: string };

export type SignInMinimalProvider = { id: string; label: string };

export type SignInMinimalProps = {
  title?: string;
  /** Mono context line under the headline, e.g. the workspace address. */
  context?: string;
  /** Short lines in the left margin column. */
  notes?: string[];
  providers?: SignInMinimalProvider[];
  onProviderClick?: (id: string) => void;
  onSubmit?: (values: SignInMinimalValues) => void | Promise<void>;
  /** Show the "Email me a sign-in link" toggle. */
  allowMagicLink?: boolean;
  defaultMethod?: SignInMinimalMethod;
  forgotPasswordHref?: string;
  signUpHref?: string;
  termsHref?: string;
  privacyHref?: string;
  /** Service status line in the footer row. Pass an empty string to hide it. */
  statusLabel?: string;
  /** Wordmark square, status marker and primary button. Defaults to Devsloka blue. */
  accent?: string;
  /** Text colour on `accent`. Defaults to white. */
  accentForeground?: string;
} & Omit<ComponentProps<"section">, "onSubmit">;

const DEFAULTS = {
  title: "Welcome back.",
  context: "Workspace: field-notes.larch.app",
  notes: [
    "Larch is a shared notebook for small research teams.",
    "Interviews, field notes and tags in one place.",
  ],
  providers: [
    { id: "google", label: "Continue with Google" },
    { id: "sso", label: "Continue with SSO" },
  ],
  statusLabel: "All systems normal",
  accent: "#0A6EFF",
  accentForeground: "#FFFFFF",
};

type Field = "email" | "password";
type Errors = Partial<Record<Field, string>>;
type Status = "idle" | "submitting" | "success" | "error";

const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

function validate(values: Record<Field, string>, method: SignInMinimalMethod): Errors {
  const errors: Errors = {};
  const email = values.email.trim();
  if (!email) errors.email = "Enter the email you use for Larch.";
  else if (!EMAIL_PATTERN.test(email))
    errors.email = "That email looks incomplete. Check for a typo.";
  if (method === "password" && !values.password) errors.password = "Enter your password.";
  return errors;
}

const defaultSubmit = () =>
  new Promise<void>((resolve) => setTimeout(resolve, 900));

const linkClass =
  "rounded-sm underline decoration-1 underline-offset-4 outline-none hover:decoration-2 focus-visible:ring-2 focus-visible:ring-ring";

export function SignInMinimal({
  title = DEFAULTS.title,
  context = DEFAULTS.context,
  notes = DEFAULTS.notes,
  providers = DEFAULTS.providers,
  onProviderClick,
  onSubmit = defaultSubmit,
  allowMagicLink = true,
  defaultMethod = "password",
  forgotPasswordHref = "#",
  signUpHref = "#",
  termsHref = "#",
  privacyHref = "#",
  statusLabel = DEFAULTS.statusLabel,
  accent = DEFAULTS.accent,
  accentForeground = DEFAULTS.accentForeground,
  className,
  style,
  ...props
}: SignInMinimalProps) {
  const id = useId();
  const inboxHeadingRef = useRef<HTMLHeadingElement>(null);
  const [method, setMethod] = useState<SignInMinimalMethod>(
    allowMagicLink ? defaultMethod : "password"
  );
  const [values, setValues] = useState<Record<Field, string>>({ email: "", password: "" });
  const [showPassword, setShowPassword] = useState(false);
  const [errors, setErrors] = useState<Errors>({});
  const [attempted, setAttempted] = useState(false);
  const [status, setStatus] = useState<Status>("idle");
  const [message, setMessage] = useState("");

  const fieldId = (name: Field) => `${id}-${name}`;
  const errorId = (name: Field) => `${id}-${name}-error`;
  const submitting = status === "submitting";
  const linkSent = method === "magic-link" && status === "success";

  useEffect(() => {
    if (linkSent) inboxHeadingRef.current?.focus();
  }, [linkSent]);

  const switchMethod = () => {
    setMethod((current) => (current === "password" ? "magic-link" : "password"));
    setErrors({});
    setStatus("idle");
    setMessage("");
  };

  const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    setAttempted(true);
    const nextErrors = validate(values, method);
    setErrors(nextErrors);
    const firstInvalid = (Object.keys(nextErrors) as Field[])[0];
    if (firstInvalid) {
      document.getElementById(fieldId(firstInvalid))?.focus();
      return;
    }

    const email = values.email.trim();
    setStatus("submitting");
    setMessage("");
    try {
      await onSubmit(
        method === "password"
          ? { method, email, password: values.password }
          : { method, email }
      );
      setStatus("success");
      setMessage(
        method === "password"
          ? "Signed in. Opening your workspace…"
          : `Check your inbox. We sent a sign-in link to ${email}.`
      );
    } catch (error) {
      setStatus("error");
      setMessage(
        error instanceof Error && error.message
          ? error.message
          : method === "password"
            ? "That email and password don't match an account here. Check both and try again."
            : "We couldn't send the link just now. Wait a minute and try again."
      );
    }
  };

  const fieldProps = (name: Field) => ({
    id: fieldId(name),
    name,
    value: values[name],
    "aria-invalid": errors[name] ? true : undefined,
    "aria-describedby": errors[name] ? errorId(name) : undefined,
    onChange: (event: ChangeEvent<HTMLInputElement>) =>
      setValues((current) => ({ ...current, [name]: event.target.value })),
    onBlur: () => {
      if (attempted)
        setErrors((current) => ({ ...current, [name]: validate(values, method)[name] }));
    },
  });

  const errorText = (name: Field) =>
    errors[name] ? (
      <p id={errorId(name)} className="text-xs text-destructive">
        {errors[name]}
      </p>
    ) : null;

  return (
    <section
      data-slot="sign-in-minimal"
      aria-labelledby={`${id}-title`}
      style={
        {
          "--block-accent": accent,
          "--block-accent-fg": accentForeground,
          ...style,
        } as CSSProperties
      }
      className={cn("w-full bg-background text-foreground", className)}
      {...props}
    >
      <div className="mx-auto max-w-6xl px-6 py-16 md:py-24">
        <div className="grid gap-12 md:grid-cols-12 md:gap-8">
          <div className="md:col-span-4">
            <span className="inline-flex items-center gap-2 text-lg font-semibold tracking-tight">
              <span aria-hidden="true" className="size-2.5 bg-[var(--block-accent)]" />
              Larch
            </span>
            {notes.length > 0 ? (
              <ul className="mt-6 hidden max-w-[16rem] space-y-2 text-sm leading-relaxed text-muted-foreground md:block">
                {notes.map((note) => (
                  <li key={note}>{note}</li>
                ))}
              </ul>
            ) : null}
          </div>

          <div className="md:col-span-6 md:col-start-6">
            <h2
              id={`${id}-title`}
              className="text-5xl font-semibold tracking-[-0.04em] md:text-7xl"
            >
              {title}
            </h2>
            {context ? (
              <p className="mt-4 font-mono text-xs tracking-wider text-muted-foreground">
                {context}
              </p>
            ) : null}

            <div className="mt-12 max-w-md border-t pt-8">
              <p role="status" aria-live="polite" className="sr-only">
                {linkSent ? message : ""}
              </p>

              {linkSent ? (
                <div>
                  <p className="font-mono text-xs tracking-wider text-muted-foreground uppercase">
                    Link sent
                  </p>
                  <h3
                    ref={inboxHeadingRef}
                    tabIndex={-1}
                    className="mt-3 text-2xl font-semibold tracking-tight outline-none"
                  >
                    Check your inbox
                  </h3>
                  <p className="mt-3 text-sm leading-relaxed text-muted-foreground">
                    We sent a sign-in link to{" "}
                    <span className="font-medium text-foreground">{values.email.trim()}</span>.
                    It works once and expires in 15 minutes. Nothing there? Check spam, or
                    ask your admin whether Larch emails are filtered.
                  </p>
                  <div className="mt-8 flex flex-wrap gap-x-6 gap-y-3 text-sm">
                    <button
                      type="button"
                      onClick={() => {
                        setStatus("idle");
                        setMessage("");
                      }}
                      className={cn(linkClass, "font-medium")}
                    >
                      Use a different email
                    </button>
                    <button type="button" onClick={switchMethod} className={cn(linkClass, "text-muted-foreground hover:text-foreground")}>
                      Sign in with a password
                    </button>
                  </div>
                </div>
              ) : (
                <form
                  noValidate
                  onSubmit={handleSubmit}
                  aria-busy={submitting}
                  className="grid gap-6"
                >
                  <div className="grid gap-2">
                    <Label htmlFor={fieldId("email")}>Email</Label>
                    <Input
                      {...fieldProps("email")}
                      type="email"
                      autoComplete="email"
                      inputMode="email"
                      required
                    />
                    {errorText("email")}
                  </div>

                  {method === "password" ? (
                    <div className="grid gap-2">
                      <div className="flex items-baseline justify-between gap-4">
                        <Label htmlFor={fieldId("password")}>Password</Label>
                        <a
                          href={forgotPasswordHref}
                          className={cn(linkClass, "text-xs text-muted-foreground hover:text-foreground")}
                        >
                          Forgot password?
                        </a>
                      </div>
                      <div className="relative">
                        <Input
                          {...fieldProps("password")}
                          type={showPassword ? "text" : "password"}
                          autoComplete="current-password"
                          required
                          className="pr-10"
                        />
                        <button
                          type="button"
                          aria-label={showPassword ? "Hide password" : "Show password"}
                          onClick={() => setShowPassword((current) => !current)}
                          className="absolute inset-y-0 right-0 flex w-10 items-center justify-center rounded-r-md text-muted-foreground outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
                        >
                          {showPassword ? (
                            <EyeOff aria-hidden="true" className="size-4" />
                          ) : (
                            <Eye aria-hidden="true" className="size-4" />
                          )}
                        </button>
                      </div>
                      {errorText("password")}
                    </div>
                  ) : (
                    <p className="text-sm leading-relaxed text-muted-foreground">
                      We&apos;ll email you a link that signs you in once. No password needed.
                    </p>
                  )}

                  <div className="flex flex-wrap items-center justify-between gap-x-6 gap-y-4">
                    <Button
                      type="submit"
                      size="lg"
                      disabled={submitting}
                      className="rounded-none bg-[var(--block-accent)] text-[var(--block-accent-fg)] shadow-none hover:bg-[var(--block-accent)] hover:underline hover:underline-offset-4"
                    >
                      {submitting ? (
                        <>
                          <Loader2 aria-hidden="true" className="animate-spin motion-reduce:animate-none" />
                          {method === "password" ? "Signing in…" : "Sending link…"}
                        </>
                      ) : method === "password" ? (
                        "Sign in"
                      ) : (
                        "Email me a sign-in link"
                      )}
                    </Button>
                    {allowMagicLink ? (
                      <button
                        type="button"
                        onClick={switchMethod}
                        className={cn(linkClass, "text-sm text-muted-foreground hover:text-foreground")}
                      >
                        {method === "password"
                          ? "Email me a sign-in link instead"
                          : "Use my password instead"}
                      </button>
                    ) : null}
                  </div>

                  <p
                    role="status"
                    aria-live="polite"
                    className={cn(
                      "text-sm empty:hidden",
                      status === "error" && "border-l-2 border-destructive pl-3 text-destructive"
                    )}
                  >
                    {message}
                  </p>
                </form>
              )}

              {providers.length > 0 && !linkSent ? (
                <div className="mt-10 border-t pt-6">
                  <p className="font-mono text-[11px] tracking-wider text-muted-foreground uppercase">
                    Or
                  </p>
                  <ul className="mt-3 flex flex-wrap gap-x-6 gap-y-2 text-sm">
                    {providers.map((provider) => (
                      <li key={provider.id}>
                        <button
                          type="button"
                          onClick={() => onProviderClick?.(provider.id)}
                          className={cn(linkClass, "font-medium")}
                        >
                          {provider.label}
                        </button>
                      </li>
                    ))}
                  </ul>
                </div>
              ) : null}

              <p className="mt-10 text-sm text-muted-foreground">
                No account yet?{" "}
                <a href={signUpHref} className={cn(linkClass, "font-medium text-foreground")}>
                  Create one
                </a>
              </p>
            </div>
          </div>
        </div>

        <div className="mt-16 flex flex-wrap items-center justify-between gap-4 border-t pt-6 font-mono text-[11px] tracking-wider text-muted-foreground uppercase md:mt-24">
          <nav aria-label="Legal" className="flex gap-6">
            <a href={termsHref} className={cn(linkClass, "hover:text-foreground")}>
              Terms
            </a>
            <a href={privacyHref} className={cn(linkClass, "hover:text-foreground")}>
              Privacy
            </a>
          </nav>
          {statusLabel ? (
            <p className="flex items-center gap-2">
              <span aria-hidden="true" className="size-1.5 bg-[var(--block-accent)]" />
              {statusLabel}
            </p>
          ) : null}
        </div>
      </div>
    </section>
  );
}
```
