Skip to content
CodeAndBuild LogoCodeAndBuild

CSS

Build a Glass 3D Login Card in Next.js

A frosted login card with stacked depth layers, pointer tilt, and a readable form. The live demo never sends the password.

CodeAndBuild Team11 min read
  • CSS
  • Next.js
  • UI
  • React
On this page
  1. Build the stage before the form
  2. Stack two plates behind the glass
  3. Tilt from the pointer, then let it rest
  4. Keep the form honest

A glass login feels expensive when three things happen together. The card is translucent enough to show color behind it. Two quieter panels sit behind it in real 3D space. The whole stack leans toward the pointer by a few degrees, with a highlight that follows. Open the live glass login and move across the card before you copy the code. The password you type there is checked for length and then cleared. It is not sent anywhere.

Glass without a plan becomes gray text on a gray blur. This screen keeps the fields on a dark fill inside the glass, uses white labels, and puts the color in the orbs behind the card. The blur is the material. The contrast is the product. If a sentence is hard to read, the blur is too strong or the text is sitting on the busy part of the background.

Build the stage before the form

The stage is a dark panel, three soft orbs, and a perspective parent. The orbs are large blurred circles. They drift slowly so the glass has something to refract. perspective on the parent is what makes the child rotations look like a physical card instead of a flat skew. 1200px is a calm camera. A smaller number looks like a fisheye.

app/globals.csscss
@keyframes glass-drift {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
  50% { transform: translate3d(16px, -22px, 0) scale(1.08); }
}

.glass-orb {
  position: absolute;
  border-radius: 9999px;
  filter: blur(36px);
  pointer-events: none;
  animation: glass-drift 10s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  .glass-orb { animation: none; }
}

Stack two plates behind the glass

Depth comes from translateZ, not from a drop shadow alone. The parent needs transform-style: preserve-3d. The back plates are the same size as the card, pushed away from the camera and rotated a few degrees. They do not receive clicks. The front plate is the form: white at low opacity, a bright border, a heavy blur, and a positive translateZ so it sits in front of the plates.

components/GlassLogin.tsxtsx
<div className="relative w-full [transform-style:preserve-3d]" style={{ transform: `rotateX(${tilt.x}deg) rotateY(${tilt.y}deg)` }}>
  <div className="absolute inset-0 rounded-[1.8rem] border border-white/10 bg-white/5 [transform:translate3d(-18px,16px,-48px)_rotateZ(-7deg)]" />
  <div className="absolute inset-0 rounded-[1.8rem] border border-cyan-100/20 bg-cyan-200/10 [transform:translate3d(22px,-18px,-28px)_rotateZ(5deg)]" />
  <form className="relative rounded-[1.8rem] border border-white/30 bg-white/10 shadow-[0_30px_90px_rgba(0,0,0,0.5)] backdrop-blur-2xl [transform:translateZ(24px)]">
    {/* fields */}
  </form>
</div>

Tilt from the pointer, then let it rest

Measure the pointer inside the stage, not the window. A value of 0 in the center and about 8 degrees at the edge is enough. Larger angles make the fields hard to click. On pointer leave, return to zero. If the visitor prefers reduced motion, skip the update and leave the card still. The glare is a radial gradient positioned at the same pointer fraction, painted on a layer that ignores clicks.

tilt.tsts
function onPointerMove(event: React.PointerEvent<HTMLDivElement>) {
  if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
  const rect = event.currentTarget.getBoundingClientRect();
  const px = (event.clientX - rect.left) / rect.width;
  const py = (event.clientY - rect.top) / rect.height;
  setTilt({ x: (0.5 - py) * 12, y: (px - 0.5) * 16 });
}

Keep the form honest

Labels are visible. The email field is type email. The password field can be revealed with a button that sets aria-pressed. Submit is a real submit button. Validation explains the problem in an alert and does not invent an account. On success, clear the password from state immediately and show the email the visitor typed. A demo that keeps the password on screen after submit is teaching the wrong habit.

  • Put a dark translucent fill on each input so the orb colors do not wash out the characters.
  • Use a light gradient on the button and dark text on it. White text on a pale glass button fails in daylight.
  • Mark the back plates and the glare aria-hidden so a screen reader stays on the form.
  • Offer a still card when prefers-reduced-motion is set. The login still has to work without the tilt.

The same shell can hold a create-account form. Share one layout between login and signup, then send the reader who liked the motion to the tilt note and the reader who squints at the type to the contrast note.

More guides

PublishingSEOUX

Give Every Page One Obvious Next Click

How a technical site earns another pageview: one next step on the hero, in the article, and after the last paragraph.

8 min read
ReactUICSS

Use One Glass Shell for Login and Signup

One glass stage, two forms. How to share the shell between sign-in and create-account without cloning the CSS.

8 min read