/Click feedback
Click feedback • React
Cursor Edge Glow Button
A spring-tracked pill light with mirrored edge glows that crossfade as the pointer changes sides.
Installation
Terminal
npx shadcn@latest add https://microkit.co/r/cursor-edge-glow-button.jsonCode
1"use client";23import { useEffect, useRef } from "react";45const FREQUENCY = 3.4;6const DAMPING = 0.78;7const GLOW_RISE = 0.5;8const EDGE_SATURATION = 0.55;9const EDGE_BRIGHTNESS = 0.12;10const HUE_SHIFT = -5;1112export function CursorEdgeGlowButton() {13const containerRef = useRef<HTMLDivElement>(null);14const buttonRef = useRef<HTMLButtonElement>(null);15const lightTrackRef = useRef<HTMLSpanElement>(null);16const rightGlowRef = useRef<HTMLSpanElement>(null);17const leftGlowRef = useRef<HTMLSpanElement>(null);18const animationFrameRef = useRef<number | null>(null);1920useEffect(() => {21const container = containerRef.current;22const button = buttonRef.current;23const lightTrack = lightTrackRef.current;24const rightGlow = rightGlowRef.current;25const leftGlow = leftGlowRef.current;26if (!container || !button || !lightTrack || !rightGlow || !leftGlow) return;2728let bound = container.getBoundingClientRect().width / 2 + 12;29let x = bound;30let velocity = 0;31let target = bound;32let inside = false;33let last = 0;3435const measure = () => {36bound = container.getBoundingClientRect().width / 2 + 12;37};3839const paint = () => {40lightTrack.style.setProperty("--light-x", x.toFixed(2) + "px");41const normalized = Math.max(-1, Math.min(1, x / bound));42const magnitude = Math.abs(normalized);43const intensity = Math.pow(magnitude, GLOW_RISE);44const colorTuning =45"hue-rotate(" + HUE_SHIFT + "deg)" +46" saturate(" + (1 + EDGE_SATURATION * magnitude).toFixed(3) + ")" +47" brightness(" + (1 + EDGE_BRIGHTNESS * magnitude).toFixed(3) + ")";4849rightGlow.style.opacity = (normalized > 0 ? intensity : 0).toFixed(3);50leftGlow.style.opacity = (normalized < 0 ? intensity : 0).toFixed(3);51rightGlow.style.filter = colorTuning;52leftGlow.style.filter = colorTuning;53};5455measure();56paint();57const resizeObserver = new ResizeObserver(measure);58resizeObserver.observe(container);5960if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {61return () => resizeObserver.disconnect();62}6364const frame = (now = performance.now()) => {65const delta = Math.min((now - last) / 1000, 0.032);66last = now;67const angularFrequency = 2 * Math.PI * FREQUENCY;68velocity +=69(angularFrequency * angularFrequency * (target - x) -702 * DAMPING * angularFrequency * velocity) *71delta;72x += velocity * delta;73paint();7475if (inside || Math.abs(target - x) > 0.15 || Math.abs(velocity) > 0.6) {76animationFrameRef.current = requestAnimationFrame(frame);77} else {78animationFrameRef.current = null;79x = target;80velocity = 0;81paint();82}83};8485const kick = () => {86if (animationFrameRef.current === null) {87last = performance.now();88animationFrameRef.current = requestAnimationFrame(frame);89}90};9192container.onpointermove = (event) => {93const bounds = container.getBoundingClientRect();94inside = true;95target = Math.max(-bound, Math.min(bound, event.clientX - (bounds.left + bounds.width / 2)));96kick();97};9899container.onpointerleave = () => {100inside = false;101target = x;102kick();103};104105button.onfocus = () => {106inside = false;107target = 0;108kick();109};110111button.onblur = () => {112inside = false;113target = x;114kick();115};116117return () => {118resizeObserver.disconnect();119container.onpointermove = null;120container.onpointerleave = null;121button.onfocus = null;122button.onblur = null;123if (animationFrameRef.current !== null) cancelAnimationFrame(animationFrameRef.current);124};125}, []);126127return (128<div ref={containerRef} className="relative z-10 inline-flex items-center">129<span ref={rightGlowRef} aria-hidden="true" className="pointer-events-none absolute left-1/2 top-1/2 h-[calc(100%+9px)] w-[calc(100%+9px)] rounded-full border-[3px] border-transparent opacity-100 will-change-transform [transform:translate(-50%,-50%)]">130<span className="absolute left-[-3px] top-[-3px] z-20 box-content h-full w-full rounded-full border-[3px] border-transparent blur-[15px] [background:linear-gradient(transparent,transparent)_padding-box,linear-gradient(91.88deg,rgba(255,137,100,.2)_46.45%,#cd3100_98.59%)_border-box]" />131<span className="absolute left-[-2px] top-[-2px] z-10 box-content h-full w-full rounded-full border-2 border-transparent blur-[2px] [background:linear-gradient(transparent,transparent)_padding-box,linear-gradient(97.68deg,rgba(255,177,153,0)_38.1%,rgba(255,177,153,.2)_82.47%,#ff7950_93.3%)_border-box]" />132<span className="relative block h-full w-full rounded-full border border-transparent [background:linear-gradient(transparent,transparent)_padding-box,linear-gradient(103.7deg,rgba(188,155,143,.1)_38.66%,rgba(233,132,99,.1)_68.55%,#e98463_85.01%,#fff_92.12%)_border-box]">133<span className="absolute left-[-2px] top-[-2px] z-30 box-content h-full w-full rounded-full border-2 border-transparent blur-[7px] [background:linear-gradient(transparent,transparent)_padding-box,linear-gradient(91.96deg,rgba(255,177,153,0)_6.11%,rgba(255,177,153,.2)_53.57%,#ff7950_93.6%)_border-box]" />134</span>135</span>136<span ref={leftGlowRef} aria-hidden="true" className="pointer-events-none absolute left-1/2 top-1/2 h-[calc(100%+9px)] w-[calc(100%+9px)] rounded-full border-[3px] border-transparent opacity-0 will-change-transform [transform:translate(-50%,-50%)_scaleX(-1)]">137<span className="absolute left-[-3px] top-[-3px] z-20 box-content h-full w-full rounded-full border-[3px] border-transparent blur-[15px] [background:linear-gradient(transparent,transparent)_padding-box,linear-gradient(91.88deg,rgba(255,137,100,.2)_46.45%,#cd3100_98.59%)_border-box]" />138<span className="absolute left-[-2px] top-[-2px] z-10 box-content h-full w-full rounded-full border-2 border-transparent blur-[2px] [background:linear-gradient(transparent,transparent)_padding-box,linear-gradient(97.68deg,rgba(255,177,153,0)_38.1%,rgba(255,177,153,.2)_82.47%,#ff7950_93.3%)_border-box]" />139<span className="relative block h-full w-full rounded-full border border-transparent [background:linear-gradient(transparent,transparent)_padding-box,linear-gradient(103.7deg,rgba(188,155,143,.1)_38.66%,rgba(233,132,99,.1)_68.55%,#e98463_85.01%,#fff_92.12%)_border-box]">140<span className="absolute left-[-2px] top-[-2px] z-30 box-content h-full w-full rounded-full border-2 border-transparent blur-[7px] [background:linear-gradient(transparent,transparent)_padding-box,linear-gradient(91.96deg,rgba(255,177,153,0)_6.11%,rgba(255,177,153,.2)_53.57%,#ff7950_93.6%)_border-box]" />141</span>142</span>143<button ref={buttonRef} type="button" className="relative z-10 flex h-10 cursor-pointer items-center justify-center gap-1 overflow-hidden rounded-full border border-white/60 bg-[#d1d1d1] px-[3.25rem] text-xs font-bold uppercase leading-none tracking-[-.015em] text-[#5a250a] transition-colors duration-200 ease-[cubic-bezier(.4,0,.2,1)] [font-family:Inter,-apple-system,'Segoe_UI',sans-serif] focus-visible:outline-2 focus-visible:outline-offset-8 focus-visible:outline-[#ff8964]">144<span ref={lightTrackRef} aria-hidden="true" className="absolute left-1/2 top-0 z-[-10] ml-[-102px] flex h-full w-[204px] items-center justify-center [--light-x:120px] [transform:translateX(var(--light-x))_translateZ(0)]">145<span className="absolute top-1/2 h-[121px] w-[121px] -translate-y-1/2 [background:radial-gradient(50%_50%_at_50%_50%,#fffff5_3.5%,#ffaa81_26.5%,#ffda9f_37.5%,rgba(255,170,129,.5)_49%,rgba(210,106,58,0)_92.5%)]" />146<span className="absolute top-1/2 h-[103px] w-[204px] -translate-y-1/2 blur-[5px] [background:radial-gradient(43.3%_44.23%_at_50%_49.51%,#fffff7_29%,#fffacd_48.5%,#f4d2bf_60.71%,rgba(214,211,210,0)_100%)]" />147</span>148<span>See in Action</span>149<svg className="h-[9px] w-[17px] flex-none text-[#5a250a]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 9" aria-hidden="true">150<path fill="currentColor" fillRule="evenodd" d="m12.495 0 4.495 4.495-4.495 4.495-.99-.99 2.805-2.805H0v-1.4h14.31L11.505.99z" clipRule="evenodd" />151</svg>152</button>153</div>154);155}