# Login form (Caseroom block)

Email and password with autocomplete hints, a show/hide toggle, keep me signed in, reset and sign-up links. The full auth flow is in Flightdeck. Black and white, dark mode, shadcn-compatible; React, Astro and plain HTML.

39% of the 2,062 sites in The UI Index have a form (806 sites).

## Install

- React: `npx shadcn@latest add https://www.themes.dev/caseroom/r/login-form.json`
- Astro: `npx shadcn@latest add https://www.themes.dev/caseroom/r/astro/login-form.json`
- HTML: https://www.themes.dev/caseroom/r/html/login-form.html (with https://www.themes.dev/caseroom/r/html/caseroom.css)

Page: https://www.themes.dev/caseroom/blocks/login-form/ · Rules for agents: https://www.themes.dev/caseroom/AGENT.md

## Code

### login-form.astro

```astro
---
import Button from '@/components/ui/button.astro';
import Checkbox from '@/components/ui/checkbox.astro';
import Input from '@/components/ui/input.astro';
import Label from '@/components/ui/label.astro';
import { cn } from '@/lib/utils';
import { loginForm as c, type LoginContent } from './login-form.classes';

type Props = LoginContent & { class?: string };
const { title, description, action, forgotHref, signupHref, id, class: className } = Astro.props;
---

<section aria-labelledby={`${id}-title`} class={cn(c.root, className)}>
  <div class={c.card}>
    <h2 id={`${id}-title`} class={c.title}>{title}</h2>
    {description && <p class={c.description}>{description}</p>}
    <form action={action} method="post" class={c.form}>
      <div class={c.field}>
        <Label for={`${id}-email`}>Email</Label>
        <Input id={`${id}-email`} name="email" type="email" autocomplete="username" required />
      </div>
      <div class={c.field}>
        <div class={c.labelRow}>
          <Label for={`${id}-password`}>Password</Label>
          <a href={forgotHref} class={c.forgot}>Forgot it?</a>
        </div>
        <div class={c.password}>
          <Input id={`${id}-password`} name="password" type="password" autocomplete="current-password" required class="pr-16" />
          <button type="button" data-password-toggle aria-controls={`${id}-password`} aria-pressed="false" aria-label="Show password" class={c.reveal}>Show</button>
        </div>
      </div>
      <Label><Checkbox name="remember" /> Keep me signed in</Label>
      <Button type="submit" class={c.submit}>Sign in</Button>
    </form>
  </div>
  <p class={c.footer}>New here? <a href={signupHref} class={c.signup}>Create an account</a></p>
</section>

<script>
  import './login-form.client';
</script>
```

### login-form.classes.ts

```tsx
/** A simple sign-in form (free; the full auth flow with sign-up, reset and two-factor is part of Flightdeck): a
 *  heading, email and password with the right autocomplete hints, a show/hide toggle for the password, "Keep me
 *  signed in", one primary button, and links to reset the password and to sign up. Native validation. */
export const loginForm = {
  root: 'mx-auto w-full max-w-sm',
  card: 'rounded-xl border bg-card p-6 text-card-foreground shadow-sm sm:p-8',
  title: 'text-xl font-semibold tracking-[-0.02em]',
  description: 'mt-1 text-sm text-pretty text-muted-foreground',
  form: 'mt-6 grid gap-5',
  field: 'grid gap-2',
  labelRow: 'flex items-baseline justify-between gap-3',
  forgot: '-mx-1 inline-flex min-h-6 items-center rounded-sm px-1 text-sm text-muted-foreground underline decoration-transparent underline-offset-4 transition-colors hover:text-foreground hover:decoration-foreground active:bg-accent',
  password: 'relative',
  reveal: 'absolute top-1/2 right-1 inline-flex h-7 -translate-y-1/2 cursor-pointer items-center rounded-md px-2 text-xs font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground active:bg-pressed',
  submit: 'w-full',
  footer: 'mt-6 text-center text-sm text-muted-foreground',
  signup: '-mx-1 inline-flex min-h-6 items-center rounded-sm px-1 font-medium text-foreground underline decoration-foreground/30 underline-offset-4 hover:decoration-foreground active:bg-accent',
};
export type LoginContent = { title: string; description?: string; action: string; forgotHref: string; signupHref: string; id: string };
```

### login-form.client.ts

```tsx
// Login form: the Show/Hide button switches the password field between hidden and plain text, says which in its
// label (aria-pressed), and keeps focus in the field's place.
declare global {
  interface Window {
    __caseroomLogin?: boolean;
  }
}

if (typeof window !== 'undefined' && !window.__caseroomLogin) {
  window.__caseroomLogin = true;
  document.addEventListener('click', (e) => {
    const b = (e.target as Element).closest<HTMLButtonElement>('[data-password-toggle]');
    // A click a framework already handled (the React form keeps the state) is left alone.
    if (!b || e.defaultPrevented) return;
    const input = document.getElementById(b.getAttribute('aria-controls') ?? '') as HTMLInputElement | null;
    if (!input) return;
    const show = input.type === 'password';
    input.type = show ? 'text' : 'password';
    b.setAttribute('aria-pressed', String(show));
    b.textContent = show ? 'Hide' : 'Show';
  });
}

export {};
```

### login-form.tsx

```tsx
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { cn } from '@/lib/utils';
import { loginForm as c, type LoginContent } from './login-form.classes';

export type LoginFormProps = LoginContent & { className?: string };

/** A simple sign-in form with autocomplete hints, a show/hide password toggle and native validation. */
export function LoginForm({ title, description, action, forgotHref, signupHref, id, className }: LoginFormProps) {
  const [shown, setShown] = useState(false);
  return (
    <section aria-labelledby={`${id}-title`} className={cn(c.root, className)}>
      <div className={c.card}>
        <h2 id={`${id}-title`} className={c.title}>{title}</h2>
        {description && <p className={c.description}>{description}</p>}
        <form action={action} method="post" className={c.form}>
          <div className={c.field}>
            <Label htmlFor={`${id}-email`}>Email</Label>
            <Input id={`${id}-email`} name="email" type="email" autoComplete="username" required />
          </div>
          <div className={c.field}>
            <div className={c.labelRow}>
              <Label htmlFor={`${id}-password`}>Password</Label>
              <a href={forgotHref} className={c.forgot}>Forgot it?</a>
            </div>
            <div className={c.password}>
              <Input id={`${id}-password`} name="password" type={shown ? 'text' : 'password'} autoComplete="current-password" required className="pr-16" />
              <button type="button" data-password-toggle="" aria-controls={`${id}-password`} aria-pressed={shown ? 'true' : 'false'} aria-label="Show password" className={c.reveal} onClick={(e) => { e.preventDefault(); setShown(!shown); }}>{shown ? 'Hide' : 'Show'}</button>
            </div>
          </div>
          <Label><Checkbox name="remember" /> Keep me signed in</Label>
          <Button type="submit" className={c.submit}>Sign in</Button>
        </form>
      </div>
      <p className={c.footer}>New here? <a href={signupHref} className={c.signup}>Create an account</a></p>
    </section>
  );
}
```
