Glass login
A frosted sign-in card that follows your cursor
Three soft blobs drift behind a sheet of glass and lean toward your pointer. The card tilts a little and a light follows you across it. Plain HTML, CSS and JavaScript, no libraries.
Free to use in personal and client work. No credit needed. Put the three files in one folder and open index.html, or copy them into your project below.
Want all of them, plus React versions? Join the Motion Pack waitlist.
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Glass login</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="aura" aria-hidden="true">
<span class="blob"></span>
<span class="blob"></span>
<span class="blob"></span>
</div>
<main class="scene">
<form class="card" novalidate>
<div class="sheen" aria-hidden="true"></div>
<h1>Welcome back</h1>
<p class="sub">Sign in to pick up where you left off.</p>
<label class="field">
<span>Email</span>
<input type="email" name="email" autocomplete="email" placeholder="you@studio.com" />
</label>
<label class="field">
<span>Password</span>
<span class="password">
<input type="password" name="password" autocomplete="current-password" placeholder="Your password" />
<button class="peek" type="button" aria-label="Show password">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12Z" />
<circle cx="12" cy="12" r="3" />
</svg>
</button>
</span>
</label>
<button class="submit" type="submit"><span class="label">Sign in</span></button>
<div class="or"><span>or</span></div>
<button class="ghost" type="button">Continue with Google</button>
<p class="alt">New here? <a href="#">Make an account</a></p>
</form>
</main>
<script src="script.js"></script>
</body>
</html>style.css
:root {
--ground: #f4f0ea;
--ink: #1d1b24;
--muted: rgba(29, 27, 36, 0.58);
--lilac: #b9a0ff;
--sky: #8ccdff;
--peach: #ffb892;
--ring: rgba(150, 125, 255, 0.28);
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100dvh;
display: grid;
place-items: center;
overflow: hidden;
background: var(--ground);
color: var(--ink);
font: 15px/1.45 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
-webkit-font-smoothing: antialiased;
}
/* Three soft blobs behind the glass. script.js moves them. */
.aura { position: fixed; inset: 0; pointer-events: none; }
.blob {
position: absolute;
left: 0;
top: 0;
width: 42vmax;
aspect-ratio: 1;
border-radius: 50%;
filter: blur(56px);
opacity: 0.8;
will-change: transform;
}
.blob:nth-child(1) { background: var(--lilac); }
.blob:nth-child(2) { background: var(--sky); width: 36vmax; }
.blob:nth-child(3) { background: var(--peach); width: 30vmax; }
.scene { position: relative; padding: 24px; perspective: 900px; }
.card {
position: relative;
width: min(380px, calc(100vw - 48px));
padding: 36px 32px 28px;
border-radius: 28px;
overflow: hidden;
background: rgba(255, 255, 255, 0.36);
border: 1px solid rgba(255, 255, 255, 0.65);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.8),
0 40px 80px -36px rgba(50, 35, 90, 0.38);
-webkit-backdrop-filter: blur(26px) saturate(1.4);
backdrop-filter: blur(26px) saturate(1.4);
transform: rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
}
/* A soft light that follows the pointer across the glass. */
.sheen {
position: absolute;
inset: 0;
pointer-events: none;
background: radial-gradient(260px circle at var(--mx, 50%) var(--my, 0%), rgba(255, 255, 255, 0.7), transparent 65%);
opacity: var(--glow, 0.4);
transition: opacity 0.5s ease;
}
h1 { position: relative; margin: 0; font-size: 26px; font-weight: 500; letter-spacing: -0.02em; }
.sub { position: relative; margin: 6px 0 24px; color: var(--muted); }
.field { position: relative; display: grid; gap: 6px; margin-bottom: 14px; font-size: 13px; color: var(--muted); }
.field input {
width: 100%;
height: 46px;
padding: 0 16px;
border: 1px solid rgba(255, 255, 255, 0.75);
border-radius: 14px;
background: rgba(255, 255, 255, 0.5);
color: var(--ink);
font: inherit;
font-size: 15px;
outline: none;
transition: background 0.2s ease, box-shadow 0.2s ease;
}
.field input::placeholder { color: rgba(29, 27, 36, 0.35); }
.field input:focus { background: rgba(255, 255, 255, 0.85); box-shadow: 0 0 0 4px var(--ring); }
.password { position: relative; display: block; }
.password input { padding-right: 48px; }
.peek {
position: absolute;
right: 6px;
top: 50%;
width: 36px;
height: 36px;
margin-top: -18px;
display: grid;
place-items: center;
border: 0;
border-radius: 10px;
background: none;
color: var(--muted);
cursor: pointer;
}
.peek:hover, .peek[aria-pressed="true"] { color: var(--ink); background: rgba(255, 255, 255, 0.6); }
.submit, .ghost {
position: relative;
width: 100%;
height: 48px;
border-radius: 999px;
font: inherit;
font-weight: 500;
cursor: pointer;
transition: transform 0.25s cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 0.25s ease, background 0.25s ease;
}
.submit {
margin-top: 8px;
border: 0;
background: var(--ink);
color: #fff;
box-shadow: 0 12px 24px -12px rgba(29, 27, 36, 0.6);
}
.submit:hover { transform: translateY(-1px); box-shadow: 0 16px 30px -12px rgba(29, 27, 36, 0.6); }
.submit:active { transform: translateY(1px) scale(0.99); }
.submit.is-loading .label::after { content: ""; display: inline-block; width: 1.2em; text-align: left; animation: dots 1s steps(4) infinite; }
.submit.is-done { background: #2e8f63; }
@keyframes dots {
0% { content: ""; }
25% { content: "."; }
50% { content: ".."; }
75% { content: "..."; }
}
.or { position: relative; display: flex; align-items: center; gap: 12px; margin: 18px 0; color: var(--muted); font-size: 13px; }
.or::before, .or::after { content: ""; flex: 1; height: 1px; background: rgba(29, 27, 36, 0.12); }
.ghost { border: 1px solid rgba(255, 255, 255, 0.8); background: rgba(255, 255, 255, 0.45); color: var(--ink); }
.ghost:hover { background: rgba(255, 255, 255, 0.75); }
.alt { position: relative; margin: 20px 0 0; text-align: center; color: var(--muted); font-size: 13px; }
.alt a { color: var(--ink); text-underline-offset: 3px; }
@media (prefers-reduced-motion: reduce) {
.card { transform: none; }
}script.js
// Glass login: the blobs drift and lean toward your pointer,
// and the card tilts a little and catches the light.
const card = document.querySelector(".card");
const blobs = [...document.querySelectorAll(".blob")];
const still = matchMedia("(prefers-reduced-motion: reduce)").matches;
// Where each blob circles (as a share of the screen) and how hard it follows you.
const orbits = [
{ x: 0.3, y: 0.32, r: 0.1, speed: 0.00012, pull: 0.35 },
{ x: 0.72, y: 0.4, r: 0.12, speed: 0.0001, pull: -0.25 },
{ x: 0.5, y: 0.75, r: 0.08, speed: 0.00016, pull: 0.5 },
];
const pointer = { x: innerWidth / 2, y: innerHeight / 2, over: false };
const eased = { x: pointer.x, y: pointer.y };
addEventListener("pointermove", (event) => {
pointer.x = event.clientX;
pointer.y = event.clientY;
});
card.addEventListener("pointerenter", () => (pointer.over = true));
card.addEventListener("pointerleave", () => (pointer.over = false));
function frame(time) {
eased.x += (pointer.x - eased.x) * 0.06;
eased.y += (pointer.y - eased.y) * 0.06;
const nx = eased.x / innerWidth - 0.5;
const ny = eased.y / innerHeight - 0.5;
blobs.forEach((blob, i) => {
const o = orbits[i];
const t = still ? 0 : time * o.speed + i * 2;
const x = (o.x + Math.cos(t) * o.r + nx * o.pull) * innerWidth;
const y = (o.y + Math.sin(t * 1.3) * o.r + ny * o.pull) * innerHeight;
blob.style.transform = `translate3d(${x}px, ${y}px, 0) translate(-50%, -50%)`;
});
const box = card.getBoundingClientRect();
card.style.setProperty("--ry", `${nx * 10}deg`);
card.style.setProperty("--rx", `${-ny * 10}deg`);
card.style.setProperty("--mx", `${eased.x - box.left}px`);
card.style.setProperty("--my", `${eased.y - box.top}px`);
card.style.setProperty("--glow", pointer.over ? "1" : "0.4");
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
// Show or hide the password.
const peek = card.querySelector(".peek");
const password = card.querySelector('input[name="password"]');
peek.addEventListener("click", () => {
const show = password.type === "password";
password.type = show ? "text" : "password";
peek.setAttribute("aria-pressed", String(show));
peek.setAttribute("aria-label", show ? "Hide password" : "Show password");
});
// A pretend sign in, so the button has somewhere to go. Swap in your own request.
const submit = card.querySelector(".submit");
const label = submit.querySelector(".label");
card.addEventListener("submit", (event) => {
event.preventDefault();
if (submit.classList.contains("is-loading")) return;
submit.classList.add("is-loading");
label.textContent = "Signing in";
setTimeout(() => {
submit.classList.replace("is-loading", "is-done");
label.textContent = "You're in";
}, 1200);
});