Interactive button with a directional energy trail and perimeter glow that responds to pointer velocity without expensive particle emitters.
Preview
Customize
4 props0.45
260
Installation
Copies the component source and its CSS Module into your project.
npx motionfoundry add velocity-buttonWrites components/ui/velocity-button.tsx and components/styles/velocity-button.module.css.
Usage
Updates live as you change the controls — only props that differ from the defaults are included.
example.tsx
1import { VelocityButton } from "@motionfoundry/ui";2 3export function Example() {4 return (5 <VelocityButton />6 );7}Props
Accessibility
Updates CSS custom properties directly on the DOM node for zero React re-renders during pointer movement and disables the directional trail when prefers-reduced-motion is enabled.
Toggle prefers-reduced-motion in your OS or browser devtools (Rendering → Emulate CSS media feature) to verify the reduced-motion state.
Source
components/ui/velocity-button.tsx
1"use client";2 3import * as React from "react";4import styles from "../styles/velocity-button.module.css";5 6export interface VelocityButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {7 /** Button label */8 label?: string;9 /** Energy trail color */10 trailColor?: string;11 /** Maximum opacity multiplier (0.15 to 0.85) */12 trailIntensity?: number;13 /** Decay duration in milliseconds */14 decayMs?: number;15}16 17export function VelocityButton({18 children,19 label = "Execute Edge Deployment",20 trailColor = "#38bdf8",21 trailIntensity = 0.45,22 decayMs = 260,23 className = "",24 ...rest25}: VelocityButtonProps) {26 const btnRef = React.useRef<HTMLButtonElement>(null);27 const lastPoint = React.useRef<{ x: number; y: number; t: number } | null>(null);28 29 const handlePointerMove = (e: React.PointerEvent<HTMLButtonElement>) => {30 const el = btnRef.current;31 if (!el) return;32 const rect = el.getBoundingClientRect();33 const x = e.clientX - rect.left;34 const y = e.clientY - rect.top;35 const now = performance.now();36 37 let stretch = 1;38 let angle = 0;39 if (lastPoint.current) {40 const dx = x - lastPoint.current.x;41 const dy = y - lastPoint.current.y;42 const dt = Math.max(8, now - lastPoint.current.t);43 const speed = Math.min(2.4, Math.hypot(dx, dy) / dt);44 stretch = 1 + speed * 0.65;45 angle = Math.atan2(dy, dx) * (180 / Math.PI);46 }47 lastPoint.current = { x, y, t: now };48 49 el.style.setProperty("--mf-vel-x", `${x}px`);50 el.style.setProperty("--mf-vel-y", `${y}px`);51 el.style.setProperty("--mf-vel-stretch", stretch.toFixed(2));52 el.style.setProperty("--mf-vel-angle", `${angle.toFixed(1)}deg`);53 el.style.setProperty("--mf-vel-opacity", String(trailIntensity));54 };55 56 const handlePointerLeave = () => {57 lastPoint.current = null;58 btnRef.current?.style.setProperty("--mf-vel-opacity", "0");59 };60 61 return (62 <button63 ref={btnRef}64 type="button"65 onPointerMove={handlePointerMove}66 onPointerLeave={handlePointerLeave}67 style={68 {69 "--mf-vel-color": trailColor,70 "--mf-vel-decay": `${decayMs}ms`,71 } as React.CSSProperties72 }73 className={`${styles.button} ${className}`.trim()}74 {...rest}75 >76 <span aria-hidden="true" className={styles.energyTrail} />77 <span className={styles.content}>{children ?? label}</span>78 </button>79 );80}