Forms
Login Form
A simple, lightweight, animated login form. Fades and slides in on mount, has a password show/hide toggle, and shakes with an inline error on invalid submit. No form library, no validation library.
Loading preview
Advertisement
Ad space reserved for a future placement
Source code
"use client";
import { useId, useState, type FormEvent } from "react";
interface LoginFormProps {
accentColor?: string;
title?: string;
submitLabel?: string;
onSubmit?: (values: { email: string; password: string }) => void;
className?: string;
}
export function LoginForm({
accentColor = "#3f3cf2",
title = "Sign in",
submitLabel = "Sign in",
onSubmit,
className,
}: LoginFormProps) {
const emailId = useId();
const passwordId = useId();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [error, setError] = useState<string | null>(null);
const [shake, setShake] = useState(false);
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!email.trim() || !password.trim()) {
setError("Enter both your email and password.");
setShake(true);
window.setTimeout(() => setShake(false), 400);
return;
}
setError(null);
onSubmit?.({ email, password });
}
return (
<form
onSubmit={handleSubmit}
className={[
"login-form-card",
shake ? "login-form-card--shake" : "",
"w-full max-w-sm rounded-xl border border-gray-200 bg-white p-6 shadow-sm",
className,
]
.filter(Boolean)
.join(" ")}
style={{ ["--login-accent" as string]: accentColor }}
>
<h2 className="mb-5 text-lg font-semibold text-gray-900">{title}</h2>
<div className="mb-4">
<label htmlFor={emailId} className="mb-1.5 block text-sm font-medium text-gray-700">
Email
</label>
<input
id={emailId}
type="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
placeholder="you@example.com"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 outline-none transition-colors focus:border-[var(--login-accent)] focus:ring-2 focus:ring-[var(--login-accent)]/20"
/>
</div>
<div className="mb-2">
<label htmlFor={passwordId} className="mb-1.5 block text-sm font-medium text-gray-700">
Password
</label>
<div className="relative">
<input
id={passwordId}
type={showPassword ? "text" : "password"}
value={password}
onChange={(event) => setPassword(event.target.value)}
placeholder="••••••••"
className="w-full rounded-lg border border-gray-300 px-3 py-2 pr-10 text-sm text-gray-900 outline-none transition-colors focus:border-[var(--login-accent)] focus:ring-2 focus:ring-[var(--login-accent)]/20"
/>
<button
type="button"
onClick={() => setShowPassword((prev) => !prev)}
aria-label={showPassword ? "Hide password" : "Show password"}
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-gray-400 transition-colors hover:text-gray-600"
>
{showPassword ? (
<svg width={18} height={18} viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M3 3l18 18M10.6 10.6a2 2 0 002.8 2.8M9.4 5.5A9.9 9.9 0 0112 5c5 0 9 4 10 7-.4 1.2-1.2 2.6-2.3 3.9M6.3 6.3C4.2 7.7 2.7 9.6 2 12c1 3 5 7 10 7 1.3 0 2.5-.2 3.6-.7"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
) : (
<svg width={18} height={18} viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M2 12c1-3 5-7 10-7s9 4 10 7c-1 3-5 7-10 7s-9-4-10-7z"
stroke="currentColor"
strokeWidth="1.6"
strokeLinejoin="round"
/>
<circle cx="12" cy="12" r="3" stroke="currentColor" strokeWidth="1.6" />
</svg>
)}
</button>
</div>
</div>
{error ? <p className="mb-3 text-xs text-red-500">{error}</p> : null}
<button
type="submit"
className="login-form-submit mt-3 w-full rounded-lg py-2.5 text-sm font-semibold text-white transition-transform active:scale-[0.98]"
>
{submitLabel}
</button>
<style jsx>{`
.login-form-card {
animation: login-form-in 380ms cubic-bezier(0.16, 1, 0.3, 1);
}
.login-form-card--shake {
animation: login-form-shake 400ms ease;
}
.login-form-submit {
background: var(--login-accent);
}
.login-form-submit:hover {
filter: brightness(0.92);
}
@keyframes login-form-in {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes login-form-shake {
10%,
90% {
transform: translateX(-1px);
}
20%,
80% {
transform: translateX(2px);
}
30%,
50%,
70% {
transform: translateX(-4px);
}
40%,
60% {
transform: translateX(4px);
}
}
@media (prefers-reduced-motion: reduce) {
.login-form-card,
.login-form-card--shake {
animation: none;
}
}
`}</style>
</form>
);
}
Usage
Import the component and use it like this.
import { LoginForm } from "./LoginForm";
export default function Example() {
return (
<LoginForm
accentColor="#3f3cf2"
title="Sign in"
submitLabel="Sign in"
onSubmit={({ email, password }) => {
console.log(email, password);
}}
/>
);
}
Dependencies
No external dependencies.
Usage notes
Pass accentColor, title, submitLabel, and an onSubmit({ email, password }) callback. Only client-side presence validation is built in; wire real auth via onSubmit.