Circular Text
Animated circular text animation for modern UI layouts.
Last updated: February 13, 2026
Preview
VAIBHAV*LABS*COMPONENTS*
import CircularText from "../components/circular-text";export default function CircularTextDemo() { return ( <CircularText text="VAIBHAV*LABS*COMPONENTS*" onHover="speedUp" spinDuration={20} className="custom-class" /> );}Features
- Smooth circular text animation
- Fully customizable text content
- Lightweight and performant
- Works with light and dark themes
- Easy drop-in usage
Installation
npx shadcn@latest add https://vaibhavkesarwani.vercel.app/r/circular-text.json Import the dependecies
npm install motionCopy and paste the following code into your project.
"use client";import { motion, MotionValue, Transition, useAnimation, useMotionValue,} from "motion/react";import React, { useEffect } from "react";interface CircularTextProps { text?: string; spinDuration?: number; onHover?: "slowDown" | "speedUp" | "pause" | "goBonkers"; className?: string;}const getRotationTransition = ( duration: number, from: number, loop: boolean = true,) => ({ from, to: from + 360, ease: "linear" as const, duration, type: "tween" as const, repeat: loop ? Infinity : 0,});const getTransition = (duration: number, from: number) => ({ rotate: getRotationTransition(duration, from), scale: { type: "spring" as const, damping: 20, stiffness: 300, },});const CircularText: React.FC<CircularTextProps> = ({ text = "VAIBHAV*LABS*COMPONENTS*", spinDuration = 20, onHover = "speedUp", className = "",}) => { const letters = Array.from(text); const controls = useAnimation(); const rotation: MotionValue<number> = useMotionValue(0); useEffect(() => { const start = rotation.get(); controls.start({ rotate: start + 360, scale: 1, transition: getTransition(spinDuration, start), }); }, [spinDuration, text, onHover, controls, rotation]); const handleHoverStart = () => { const start = rotation.get(); if (!onHover) return; let transitionConfig: ReturnType<typeof getTransition> | Transition; let scaleVal = 1; switch (onHover) { case "slowDown": transitionConfig = getTransition(spinDuration * 2, start); break; case "speedUp": transitionConfig = getTransition(spinDuration / 4, start); break; case "pause": transitionConfig = { rotate: { type: "spring", damping: 20, stiffness: 300 }, scale: { type: "spring", damping: 20, stiffness: 300 }, }; break; case "goBonkers": transitionConfig = getTransition(spinDuration / 20, start); scaleVal = 0.8; break; default: transitionConfig = getTransition(spinDuration, start); } controls.start({ rotate: start + 360, scale: scaleVal, transition: transitionConfig, }); }; const handleHoverEnd = () => { const start = rotation.get(); controls.start({ rotate: start + 360, scale: 1, transition: getTransition(spinDuration, start), }); }; return ( <motion.div className={`m-0 mx-auto rounded-full w-50 h-50 relative font-black text-white text-center cursor-pointer origin-center ${className}`} style={{ rotate: rotation }} initial={{ rotate: 0 }} animate={controls} onMouseEnter={handleHoverStart} onMouseLeave={handleHoverEnd} > {letters.map((letter, i) => { const rotationDeg = (360 / letters.length) * i; const factor = Math.PI / letters.length; const x = factor * i; const y = factor * i; const transform = `rotateZ(${rotationDeg}deg) translate3d(${x}px, ${y}px, 0)`; return ( <span key={i} className="absolute inline-block inset-0 text-2xl transition-all duration-500 ease-[cubic-bezier(0,0,0,1)]" style={{ transform, WebkitTransform: transform }} > {letter} </span> ); })} </motion.div> );};export default CircularText;Update the import paths to match your project setup.
Usage
import CircularText from "@/components/ui/circular-text";<CircularText
text="VAIBHAV*LABS*COMPONENTS*"
onHover="speedUp"
spinDuration={20}
className="custom-class"
/>Props
Prop
Type
