More Forms Snippets
Glow Input — Free HTML CSS JS Spotlight Border Snippet
Glow Input · Forms · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Glow Input — Cursor-Tracking Radial Border Glow on Fields

The glow input is the polished form treatment where each field's border lights up with a soft radial glow that follows your cursor as it moves across the control — the spotlight-border effect popularized by developer-tool sign-in screens. This snippet implements it for inputs and a button using plain HTML, CSS, and a tiny vanilla JavaScript handler, with the glow drawn purely as a border via a mask trick.
Tracking the cursor with custom properties
The position of the glow is stored in two CSS custom properties, --x and --y, on each field. A single shared pointermove handler reads the cursor's position relative to the hovered element with getBoundingClientRect and writes those pixel values into the variables. CSS does the rest — the glow's radial gradient is centered at var(--x) var(--y), so updating the variables moves the light. Passing the position through CSS variables instead of restyling the gradient in JS keeps the handler trivially cheap.
Glow on the border only, via mask-composite
The clever part is confining the glow to the 1px border rather than filling the whole field. Each control's ::before is inset to cover the element, painted with the radial gradient, and given padding: 1px. Two masks are then composited with mask-composite: exclude (and the -webkit-mask-composite: xor equivalent): one mask covers the content box, the other the full box, and excluding one from the other leaves only the 1px padding ring visible. The result is a gradient that shows only as a glowing border outline — the standard CSS technique for gradient borders without an extra wrapper element.
Reveal on hover and focus
The glow's ::before sits at opacity: 0 and transitions to 1 on :hover, on :focus-within for the fields, and on :hover for the button. So the border lights up when you point at or tab into a field and fades out when you leave — giving keyboard users the same clear focus affordance as mouse users, which matters because the glow doubles as the focus indicator here.
Clean, borderless inputs
The actual <input> elements are transparent with no border or outline of their own; the visual frame comes entirely from the field shell and its glow. An icon sits inline before each input, and the placeholder uses a muted color. This keeps the markup simple — a <label> wrapping an icon and an input — while the glow lives on the label shell so the whole field reacts as one.
The submit flow
The button shares the exact same glow treatment via the data-glow attribute, so the spotlight follows the cursor across it too. On submit, JavaScript prevents navigation and swaps the button label to a "Signing in…" then "Welcome ✓" state — a minimal stand-in for a real auth call you'd drop a fetch into.
Customizing it
Change the gradient color to your accent, widen the 180px circle radius for a larger pool of light, increase the padding for a thicker glowing border, or adjust the reveal transition. Add the data-glow attribute to any element to give it the same effect — it's fully generic. Pair it with a glassmorphism login panel or an animated gradient CTA for a cohesive sign-in screen.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA sign-in form renders with dark, borderless fields.
- 2Move over a fieldA soft radial glow lights its border and follows the cursor.
- 3Tab into a fieldThe glow appears on focus too, acting as the focus ring.
- 4Hover the buttonThe same spotlight border tracks across the submit button.
- 5Submit the formThe button shows Signing in, then a Welcome confirmation.
- 6Reuse the effectAdd data-glow to any element to light its border.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Each field stores the pointer position in two CSS custom properties, --x and --y. A shared pointermove handler reads the cursor's position relative to the element with getBoundingClientRect and writes those pixel values into the variables. The glow's radial gradient is centered at var(--x) var(--y), so CSS moves the light automatically — the JS only updates two numbers.
Each control's ::before is painted with the radial gradient and given padding: 1px, then masked with mask-composite: exclude (xor on WebKit): one mask covers the content box, another the full box, and excluding one from the other leaves only the 1px padding ring visible. That confines the gradient to a border outline without any extra wrapper element.
Yes. The glow's reveal is tied to :focus-within on the fields as well as :hover, so tabbing into a field lights its border. The native input outline is removed, so the glow intentionally serves as the focus indicator, giving keyboard users the same clear affordance as mouse users.
Yes — it's generic. Any element with the data-glow attribute gets the pointermove listener, and any element carrying the ::before glow CSS will show the border light. The demo uses it on both the input shells and the submit button, but you can apply it to cards, search bars, or buttons the same way.
Render the fields and attach a pointermove handler that sets the --x and --y inline style values via a ref, or use event delegation on the form. Keep the mask-composite CSS as-is. Handle submit with a framework event that calls your auth API and toggles the button label. In Tailwind, express the gradient and masks with arbitrary values and drive the variables through an inline style.
Glow Input — Export as HTML, React, Vue, Angular & Tailwind
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Glow Input</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#08080f;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px}
.gi-form{width:100%;max-width:320px;display:flex;flex-direction:column;gap:14px}
/* Each field is a relatively-positioned shell that reveals a radial glow
tracking the cursor via two CSS custom properties --x and --y. */
.gi-field,.gi-btn{position:relative;border-radius:13px;border:1px solid #24243a;background:#101019;overflow:hidden}
.gi-field::before,.gi-btn::before{content:'';position:absolute;inset:0;border-radius:inherit;padding:1px;background:radial-gradient(180px circle at var(--x,-100px) var(--y,-100px),#6366f1,transparent 40%);-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask-composite:exclude;opacity:0;transition:opacity .25s}
.gi-field:hover::before,.gi-btn:hover::before,.gi-field:focus-within::before{opacity:1}
.gi-field{display:flex;align-items:center;gap:10px;padding:0 14px}
.gi-icon{font-size:15px;opacity:.7;flex-shrink:0}
.gi-input{flex:1;background:none;border:none;outline:none;color:#fff;font-family:inherit;font-size:14.5px;padding:14px 0}
.gi-input::placeholder{color:#6b6b85}
.gi-btn{padding:14px;color:#fff;font-family:inherit;font-size:14.5px;font-weight:700;cursor:pointer;background:linear-gradient(#1a1a2e,#101019)}
.gi-btn:hover{color:#c7d2fe}
</style>
</head>
<body>
<div class="gi-stage">
<form class="gi-form" id="giForm">
<label class="gi-field" data-glow>
<span class="gi-icon">✉</span>
<input type="email" class="gi-input" placeholder="Email address" required>
</label>
<label class="gi-field" data-glow>
<span class="gi-icon">🔒</span>
<input type="password" class="gi-input" placeholder="Password" required>
</label>
<button type="submit" class="gi-btn" data-glow>Sign in</button>
</form>
</div>
<script>
// One shared pointermove updates the --x/--y of whichever glowing element the
// cursor is over, so the border light follows the mouse precisely.
var glows = document.querySelectorAll('[data-glow]');
glows.forEach(function (el) {
el.addEventListener('pointermove', function (e) {
var r = el.getBoundingClientRect();
el.style.setProperty('--x', (e.clientX - r.left) + 'px');
el.style.setProperty('--y', (e.clientY - r.top) + 'px');
});
});
var form = document.getElementById('giForm');
form.addEventListener('submit', function (e) {
e.preventDefault();
var btn = form.querySelector('.gi-btn');
btn.textContent = 'Signing in…';
setTimeout(function () { btn.textContent = 'Welcome ✓'; }, 900);
});
</script>
</body>
</html><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Glow Input</title>
<!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
* {
box-sizing:border-box;margin:0;padding:0
}
body {
font-family:system-ui,-apple-system,sans-serif;background:#08080f;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px
}
.gi-field,.gi-btn {
position:relative;border-radius:13px;border:1px solid #24243a;background:#101019;overflow:hidden
}
.gi-field::before,.gi-btn::before {
content:'';position:absolute;inset:0;border-radius:inherit;padding:1px;background:radial-gradient(180px circle at var(--x,-100px) var(--y,-100px),#6366f1,transparent 40%);-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask-composite:exclude;opacity:0;transition:opacity .25s
}
.gi-field:hover::before,.gi-btn:hover::before,.gi-field:focus-within::before {
opacity:1
}
.gi-input::placeholder {
color:#6b6b85
}
</style>
</head>
<body>
<div class="gi-stage">
<form class="w-full max-w-xs flex flex-col gap-3.5" id="giForm">
<label class="gi-field flex items-center gap-2.5 py-0 px-3.5" data-glow>
<span class="text-[15px] opacity-70 shrink-0">✉</span>
<input type="email" class="gi-input flex-1 bg-transparent border-0 outline-none text-[#fff] font-[inherit] text-[14.5px] py-3.5 px-0" placeholder="Email address" required>
</label>
<label class="gi-field flex items-center gap-2.5 py-0 px-3.5" data-glow>
<span class="text-[15px] opacity-70 shrink-0">🔒</span>
<input type="password" class="gi-input flex-1 bg-transparent border-0 outline-none text-[#fff] font-[inherit] text-[14.5px] py-3.5 px-0" placeholder="Password" required>
</label>
<button type="submit" class="gi-btn p-3.5 text-[#fff] font-[inherit] text-[14.5px] font-bold cursor-pointer [background:linear-gradient(#1a1a2e,#101019)] hover:text-[#c7d2fe]" data-glow>Sign in</button>
</form>
</div>
<script>
// One shared pointermove updates the --x/--y of whichever glowing element the
// cursor is over, so the border light follows the mouse precisely.
var glows = document.querySelectorAll('[data-glow]');
glows.forEach(function (el) {
el.addEventListener('pointermove', function (e) {
var r = el.getBoundingClientRect();
el.style.setProperty('--x', (e.clientX - r.left) + 'px');
el.style.setProperty('--y', (e.clientY - r.top) + 'px');
});
});
var form = document.getElementById('giForm');
form.addEventListener('submit', function (e) {
e.preventDefault();
var btn = form.querySelector('.gi-btn');
btn.textContent = 'Signing in…';
setTimeout(function () { btn.textContent = 'Welcome ✓'; }, 900);
});
</script>
</body>
</html>import React, { useEffect } from 'react';
// CSS — optionally move to GlowInput.module.css
const css = `
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#08080f;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px}
.gi-form{width:100%;max-width:320px;display:flex;flex-direction:column;gap:14px}
/* Each field is a relatively-positioned shell that reveals a radial glow
tracking the cursor via two CSS custom properties --x and --y. */
.gi-field,.gi-btn{position:relative;border-radius:13px;border:1px solid #24243a;background:#101019;overflow:hidden}
.gi-field::before,.gi-btn::before{content:'';position:absolute;inset:0;border-radius:inherit;padding:1px;background:radial-gradient(180px circle at var(--x,-100px) var(--y,-100px),#6366f1,transparent 40%);-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask-composite:exclude;opacity:0;transition:opacity .25s}
.gi-field:hover::before,.gi-btn:hover::before,.gi-field:focus-within::before{opacity:1}
.gi-field{display:flex;align-items:center;gap:10px;padding:0 14px}
.gi-icon{font-size:15px;opacity:.7;flex-shrink:0}
.gi-input{flex:1;background:none;border:none;outline:none;color:#fff;font-family:inherit;font-size:14.5px;padding:14px 0}
.gi-input::placeholder{color:#6b6b85}
.gi-btn{padding:14px;color:#fff;font-family:inherit;font-size:14.5px;font-weight:700;cursor:pointer;background:linear-gradient(#1a1a2e,#101019)}
.gi-btn:hover{color:#c7d2fe}
`;
export default function GlowInput() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
// One shared pointermove updates the --x/--y of whichever glowing element the
// cursor is over, so the border light follows the mouse precisely.
var glows = document.querySelectorAll('[data-glow]');
glows.forEach(function (el) {
el.addEventListener('pointermove', function (e) {
var r = el.getBoundingClientRect();
el.style.setProperty('--x', (e.clientX - r.left) + 'px');
el.style.setProperty('--y', (e.clientY - r.top) + 'px');
});
});
var form = document.getElementById('giForm');
form.addEventListener('submit', function (e) {
e.preventDefault();
var btn = form.querySelector('.gi-btn');
btn.textContent = 'Signing in…';
setTimeout(function () { btn.textContent = 'Welcome ✓'; }, 900);
});
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
}, []);
return (
<>
<style>{css}</style>
<div className="gi-stage">
<form className="gi-form" id="giForm">
<label className="gi-field" data-glow>
<span className="gi-icon">✉</span>
<input type="email" className="gi-input" placeholder="Email address" required />
</label>
<label className="gi-field" data-glow>
<span className="gi-icon">🔒</span>
<input type="password" className="gi-input" placeholder="Password" required />
</label>
<button type="submit" className="gi-btn" data-glow>Sign in</button>
</form>
</div>
</>
);
}import React, { useEffect } from 'react';
// Requires Tailwind CSS v3+ — https://tailwindcss.com/docs/installation
// Arbitrary value classes (e.g. bg-[#0f172a]) are valid Tailwind v3+
export default function GlowInput() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
// One shared pointermove updates the --x/--y of whichever glowing element the
// cursor is over, so the border light follows the mouse precisely.
var glows = document.querySelectorAll('[data-glow]');
glows.forEach(function (el) {
el.addEventListener('pointermove', function (e) {
var r = el.getBoundingClientRect();
el.style.setProperty('--x', (e.clientX - r.left) + 'px');
el.style.setProperty('--y', (e.clientY - r.top) + 'px');
});
});
var form = document.getElementById('giForm');
form.addEventListener('submit', function (e) {
e.preventDefault();
var btn = form.querySelector('.gi-btn');
btn.textContent = 'Signing in…';
setTimeout(function () { btn.textContent = 'Welcome ✓'; }, 900);
});
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
}, []);
return (
<>
<style>{`
* {
box-sizing:border-box;margin:0;padding:0
}
body {
font-family:system-ui,-apple-system,sans-serif;background:#08080f;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px
}
.gi-field,.gi-btn {
position:relative;border-radius:13px;border:1px solid #24243a;background:#101019;overflow:hidden
}
.gi-field::before,.gi-btn::before {
content:'';position:absolute;inset:0;border-radius:inherit;padding:1px;background:radial-gradient(180px circle at var(--x,-100px) var(--y,-100px),#6366f1,transparent 40%);-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask-composite:exclude;opacity:0;transition:opacity .25s
}
.gi-field:hover::before,.gi-btn:hover::before,.gi-field:focus-within::before {
opacity:1
}
.gi-input::placeholder {
color:#6b6b85
}
`}</style>
<div className="gi-stage">
<form className="w-full max-w-xs flex flex-col gap-3.5" id="giForm">
<label className="gi-field flex items-center gap-2.5 py-0 px-3.5" data-glow>
<span className="text-[15px] opacity-70 shrink-0">✉</span>
<input type="email" className="gi-input flex-1 bg-transparent border-0 outline-none text-[#fff] font-[inherit] text-[14.5px] py-3.5 px-0" placeholder="Email address" required />
</label>
<label className="gi-field flex items-center gap-2.5 py-0 px-3.5" data-glow>
<span className="text-[15px] opacity-70 shrink-0">🔒</span>
<input type="password" className="gi-input flex-1 bg-transparent border-0 outline-none text-[#fff] font-[inherit] text-[14.5px] py-3.5 px-0" placeholder="Password" required />
</label>
<button type="submit" className="gi-btn p-3.5 text-[#fff] font-[inherit] text-[14.5px] font-bold cursor-pointer [background:linear-gradient(#1a1a2e,#101019)] hover:text-[#c7d2fe]" data-glow>Sign in</button>
</form>
</div>
</>
);
}<template>
<div class="gi-stage">
<form class="gi-form" id="giForm">
<label class="gi-field" data-glow>
<span class="gi-icon">✉</span>
<input type="email" class="gi-input" placeholder="Email address" required>
</label>
<label class="gi-field" data-glow>
<span class="gi-icon">🔒</span>
<input type="password" class="gi-input" placeholder="Password" required>
</label>
<button type="submit" class="gi-btn" data-glow>Sign in</button>
</form>
</div>
</template>
<script setup>
import { onMounted } from 'vue';
let glows;
let form;
onMounted(() => {
glows = document.querySelectorAll('[data-glow]');
glows.forEach(function (el) {
el.addEventListener('pointermove', function (e) {
var r = el.getBoundingClientRect();
el.style.setProperty('--x', (e.clientX - r.left) + 'px');
el.style.setProperty('--y', (e.clientY - r.top) + 'px');
});
});
form = document.getElementById('giForm');
form.addEventListener('submit', function (e) {
e.preventDefault();
var btn = form.querySelector('.gi-btn');
btn.textContent = 'Signing in…';
setTimeout(function () { btn.textContent = 'Welcome ✓'; }, 900);
});
});
</script>
<style scoped>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#08080f;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px}
.gi-form{width:100%;max-width:320px;display:flex;flex-direction:column;gap:14px}
/* Each field is a relatively-positioned shell that reveals a radial glow
tracking the cursor via two CSS custom properties --x and --y. */
.gi-field,.gi-btn{position:relative;border-radius:13px;border:1px solid #24243a;background:#101019;overflow:hidden}
.gi-field::before,.gi-btn::before{content:'';position:absolute;inset:0;border-radius:inherit;padding:1px;background:radial-gradient(180px circle at var(--x,-100px) var(--y,-100px),#6366f1,transparent 40%);-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask-composite:exclude;opacity:0;transition:opacity .25s}
.gi-field:hover::before,.gi-btn:hover::before,.gi-field:focus-within::before{opacity:1}
.gi-field{display:flex;align-items:center;gap:10px;padding:0 14px}
.gi-icon{font-size:15px;opacity:.7;flex-shrink:0}
.gi-input{flex:1;background:none;border:none;outline:none;color:#fff;font-family:inherit;font-size:14.5px;padding:14px 0}
.gi-input::placeholder{color:#6b6b85}
.gi-btn{padding:14px;color:#fff;font-family:inherit;font-size:14.5px;font-weight:700;cursor:pointer;background:linear-gradient(#1a1a2e,#101019)}
.gi-btn:hover{color:#c7d2fe}
</style>// @ts-nocheck
// Note: vanilla JS DOM manipulation is preserved as-is inside ngAfterViewInit().
// For idiomatic Angular, replace document.getElementById() with @ViewChild() refs
// and move state into component properties with two-way binding.
import { Component, AfterViewInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-glow-input',
standalone: true,
imports: [CommonModule],
encapsulation: ViewEncapsulation.None,
template: `
<div class="gi-stage">
<form class="gi-form" id="giForm">
<label class="gi-field" data-glow>
<span class="gi-icon">✉</span>
<input type="email" class="gi-input" placeholder="Email address" required>
</label>
<label class="gi-field" data-glow>
<span class="gi-icon">🔒</span>
<input type="password" class="gi-input" placeholder="Password" required>
</label>
<button type="submit" class="gi-btn" data-glow>Sign in</button>
</form>
</div>
`,
styles: [`
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#08080f;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px}
.gi-form{width:100%;max-width:320px;display:flex;flex-direction:column;gap:14px}
/* Each field is a relatively-positioned shell that reveals a radial glow
tracking the cursor via two CSS custom properties --x and --y. */
.gi-field,.gi-btn{position:relative;border-radius:13px;border:1px solid #24243a;background:#101019;overflow:hidden}
.gi-field::before,.gi-btn::before{content:'';position:absolute;inset:0;border-radius:inherit;padding:1px;background:radial-gradient(180px circle at var(--x,-100px) var(--y,-100px),#6366f1,transparent 40%);-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask-composite:exclude;opacity:0;transition:opacity .25s}
.gi-field:hover::before,.gi-btn:hover::before,.gi-field:focus-within::before{opacity:1}
.gi-field{display:flex;align-items:center;gap:10px;padding:0 14px}
.gi-icon{font-size:15px;opacity:.7;flex-shrink:0}
.gi-input{flex:1;background:none;border:none;outline:none;color:#fff;font-family:inherit;font-size:14.5px;padding:14px 0}
.gi-input::placeholder{color:#6b6b85}
.gi-btn{padding:14px;color:#fff;font-family:inherit;font-size:14.5px;font-weight:700;cursor:pointer;background:linear-gradient(#1a1a2e,#101019)}
.gi-btn:hover{color:#c7d2fe}
`]
})
export class GlowInputComponent implements AfterViewInit {
ngAfterViewInit(): void {
// One shared pointermove updates the --x/--y of whichever glowing element the
// cursor is over, so the border light follows the mouse precisely.
var glows = document.querySelectorAll('[data-glow]');
glows.forEach(function (el) {
el.addEventListener('pointermove', function (e) {
var r = el.getBoundingClientRect();
el.style.setProperty('--x', (e.clientX - r.left) + 'px');
el.style.setProperty('--y', (e.clientY - r.top) + 'px');
});
});
var form = document.getElementById('giForm');
form.addEventListener('submit', function (e) {
e.preventDefault();
var btn = form.querySelector('.gi-btn');
btn.textContent = 'Signing in…';
setTimeout(function () { btn.textContent = 'Welcome ✓'; }, 900);
});
}
}