Buttons
OrbitButton
A glowing dot circles the button, passing behind it on top and in front below. Hover or focus speeds it up.
Download
- React
- Tailwind CSS v4
- motion
components/ui/orbit-button.tsx
"use client";
import { useRef, type ReactNode } from "react";
import { useAnimationFrame, useReducedMotion } from "motion/react";
type OrbitButtonProps = {
children: ReactNode;
className?: string;
onClick?: () => void;
/** Colour of the orbiting dot. */
color?: string;
/** Seconds for one lap at rest. Hover and focus run it 2.5x faster. */
speed?: number;
/** Pixels between the button edge and the orbit. */
gap?: number;
};
/* OrbitButton: a glowing dot circles the button, passing behind it on the top half and in front on the bottom half. */
export function OrbitButton({
children,
className = "rounded-full border border-[#a78bfa]/40 bg-[#1a1530] px-9 py-4 text-[15px] font-medium text-[#ede9fe]",
onClick,
color = "#a78bfa",
speed = 4,
gap = 10,
}: OrbitButtonProps) {
const reduce = useReducedMotion();
const btn = useRef<HTMLButtonElement>(null);
const dot = useRef<HTMLSpanElement>(null);
const fast = useRef(false);
const angle = useRef(-Math.PI / 4);
useAnimationFrame((_, delta) => {
const b = btn.current;
const d = dot.current;
if (!b || !d) return;
if (!reduce) angle.current += (delta / 1000) * ((2 * Math.PI) / speed) * (fast.current ? 2.5 : 1);
const s = Math.sin(angle.current);
const x = Math.cos(angle.current) * (b.offsetWidth / 2 + gap);
const y = s * (b.offsetHeight / 2 + gap);
// Bigger in front, smaller behind: reads as depth.
d.style.transform = `translate(${x}px, ${y}px) scale(${0.75 + 0.2 * (s + 1)})`;
d.style.zIndex = s < 0 ? "0" : "2";
});
return (
<span className="relative isolate inline-block">
<button
ref={btn}
type="button"
onClick={onClick}
onPointerEnter={() => (fast.current = true)}
onPointerLeave={() => (fast.current = false)}
onFocus={() => (fast.current = true)}
onBlur={() => (fast.current = false)}
className={`relative z-[1] ${className}`}
>
{children}
</button>
<span
ref={dot}
aria-hidden
className="pointer-events-none absolute left-1/2 top-1/2 -ml-1 -mt-1 size-2 rounded-full"
style={{ background: color, boxShadow: `0 0 12px 2px ${color}`, zIndex: 0 }}
/>
</span>
);
}
1. Install the dependency.
npm i motion2. Paste the Code tab into
components/ui/orbit-button.tsx. The styles are Tailwind CSS v4 classes.3. Use it. This is the preview above:
app/orbit-button-demo.tsx
import { OrbitButton } from "@/components/ui/orbit-button"; export default function OrbitButtonExample() { return ( <div className="grid h-full place-items-center bg-[radial-gradient(circle_at_50%_60%,#1d1a33,#0d0c14_70%)]"> <OrbitButton>Explore the orbit</OrbitButton> </div> ); }
Props
| Name | Type | Default | What it does |
|---|---|---|---|
| children | ReactNode | — | Button label. |
| onClick | () => void | — | Click handler. |
| color | string | "#a78bfa" | Colour of the dot. |
| speed | number | 4 | Seconds for one lap at rest. |
| gap | number | 10 | Pixels between the edge and the orbit. |
| className | string | dark violet pill | Replaces the default look. |