Blocks · Forms
Login form
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.
39% of the 2,062 sites in The UI Index have a form (806 sites).
Sign in to Northwind
Use the email you signed up with.
New here? Create an account
Sign in to Northwind
Use the email you signed up with.
New here? Create an account
Both previews render the same markup; the HTML tab below is that markup without a framework.
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
Or copy the files below into your project. They need clsx, tailwind-merge and the components they import, and the Caseroom theme. The HTML needs only the compiled CSS and the script.
/** 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 };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>
);
}---
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>/** 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: 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 {};<div class="px-6 py-16">
<section aria-labelledby="ex-login-title" class="mx-auto w-full max-w-sm">
<div class="rounded-xl border bg-card p-6 text-card-foreground shadow-sm sm:p-8">
<h2 id="ex-login-title" class="text-xl font-semibold tracking-[-0.02em]">Sign in to Northwind</h2>
<p class="mt-1 text-sm text-pretty text-muted-foreground">Use the email you signed up with.</p>
<form action="#" method="post" class="mt-6 grid gap-5">
<div class="grid gap-2">
<label class="flex items-center gap-2 text-sm leading-none font-medium has-[input]:items-start has-[input]:leading-snug has-[input]:[&>*:first-child]:mt-px select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50" for="ex-login-email">
Email
</label>
<input type="email" class="w-full min-w-0 rounded-md border border-input bg-background px-3 text-sm text-foreground transition-colors placeholder:text-muted-foreground enabled:hover:border-foreground/85 enabled:active:bg-muted aria-invalid:enabled:hover:border-destructive aria-invalid:enabled:hover:bg-[color-mix(in_oklab,var(--destructive)_6%,var(--background))] aria-invalid:enabled:active:bg-[color-mix(in_oklab,var(--destructive)_10%,var(--background))] focus-visible:border-ring focus-visible:outline-2 focus-visible:outline-offset-0 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:outline-destructive h-9 py-1 file:mr-3 file:border-0 file:bg-transparent file:text-sm file:font-medium" id="ex-login-email" name="email" autocomplete="username" required>
</div>
<div class="grid gap-2">
<div class="flex items-baseline justify-between gap-3">
<label class="flex items-center gap-2 text-sm leading-none font-medium has-[input]:items-start has-[input]:leading-snug has-[input]:[&>*:first-child]:mt-px select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50" for="ex-login-password">
Password
</label>
<a href="#reset" class="-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">
Forgot it?
</a>
</div>
<div class="relative">
<input type="password" class="w-full min-w-0 rounded-md border border-input bg-background px-3 text-sm text-foreground transition-colors placeholder:text-muted-foreground enabled:hover:border-foreground/85 enabled:active:bg-muted aria-invalid:enabled:hover:border-destructive aria-invalid:enabled:hover:bg-[color-mix(in_oklab,var(--destructive)_6%,var(--background))] aria-invalid:enabled:active:bg-[color-mix(in_oklab,var(--destructive)_10%,var(--background))] focus-visible:border-ring focus-visible:outline-2 focus-visible:outline-offset-0 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:outline-destructive h-9 py-1 file:mr-3 file:border-0 file:bg-transparent file:text-sm file:font-medium pr-16" id="ex-login-password" name="password" autocomplete="current-password" required>
<button type="button" data-password-toggle aria-controls="ex-login-password" aria-pressed="false" aria-label="Show password" class="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">
Show
</button>
</div>
</div>
<label class="flex items-center gap-2 text-sm leading-none font-medium has-[input]:items-start has-[input]:leading-snug has-[input]:[&>*:first-child]:mt-px select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50">
<span class="relative inline-flex size-4 shrink-0">
<input type="checkbox" class="peer size-4 shrink-0 cursor-pointer appearance-none rounded-[4px] border border-input bg-background transition-[color,background-color,border-color,transform] enabled:not-checked:hover:border-foreground enabled:not-checked:hover:bg-accent enabled:active:scale-90 enabled:not-checked:active:bg-pressed checked:border-primary checked:enabled:hover:border-[color-mix(in_oklab,var(--primary)_var(--hover-amount),var(--background))] checked:enabled:hover:bg-[color-mix(in_oklab,var(--primary)_var(--hover-amount),var(--background))] checked:enabled:active:border-[color-mix(in_oklab,var(--primary)_var(--press-amount),var(--press-mix))] checked:enabled:active:bg-[color-mix(in_oklab,var(--primary)_var(--press-amount),var(--press-mix))] checked:bg-primary indeterminate:border-primary indeterminate:bg-primary disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive" name="remember">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="pointer-events-none absolute inset-0 hidden size-4 text-primary-foreground peer-checked:block"><path d="M3.5 8.5l3 3 6-7"></path></svg>
</span>
Keep me signed in
</label>
<button type="submit" class="inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,background-color,border-color,transform] select-none active:scale-[0.97] aria-busy:cursor-progress disabled:pointer-events-none disabled:opacity-50 aria-busy:disabled:opacity-80 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-[color-mix(in_oklab,var(--primary)_var(--hover-amount),var(--background))] active:bg-[color-mix(in_oklab,var(--primary)_var(--press-amount),var(--press-mix))] h-9 px-4 w-full">
Sign in
</button>
</form>
</div>
<p class="mt-6 text-center text-sm text-muted-foreground">
New here?
<a href="#signup" class="-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">
Create an account
</a>
</p>
</section>
</div><script type="module" src="/caseroom/r/html/login-form.js"></script>