Easily animate text circularly.
1'use client';
2import { cn } from '@/lib/utils';
3import { motion, Transition, Variants } from 'motion/react';
4import React, { CSSProperties } from 'react';
5
6export type SpinningTextProps = {
7 children: string;
8 style?: CSSProperties;
9 duration?: number;
10 className?: string;
11 reverse?: boolean;
12 fontSize?: number;
13 radius?: number;
14 transition?: Transition;
15 variants?: {
16 container?: Variants;
17 item?: Variants;
18 };
19};
20
21const BASE_TRANSITION = {
22 repeat: Infinity,
23 ease: 'linear',
24};
25
26const BASE_ITEM_VARIANTS = {
27 hidden: {
28 opacity: 1,
29 },
30 visible: {
31 opacity: 1,
32 },
33};
34
35export function SpinningText({
36 children,
37 duration = 10,
38 style,
39 className,
40 reverse = false,
41 fontSize = 1,
42 radius = 5,
43 transition,
44 variants,
45}: SpinningTextProps) {
46 const letters = children.split('');
47 const totalLetters = letters.length;
48
49 const finalTransition = {
50 ...BASE_TRANSITION,
51 ...transition,
52 duration: (transition as { duration?: number })?.duration ?? duration,
53 };
54
55 const containerVariants = {
56 visible: { rotate: reverse ? -360 : 360 },
57 ...variants?.container,
58 };
59
60 const itemVariants = {
61 ...BASE_ITEM_VARIANTS,
62 ...variants?.item,
63 };
64
65 return (
66 <motion.div
67 className={cn('relative', className)}
68 style={{
69 ...style,
70 }}
71 initial='hidden'
72 animate='visible'
73 variants={containerVariants}
74 transition={finalTransition}
75 >
76 {letters.map((letter, index) => (
77 <motion.span
78 aria-hidden='true'
79 key={`${index}-${letter}`}
80 variants={itemVariants}
81 className='absolute left-1/2 top-1/2 inline-block'
82 style={
83 {
84 '--index': index,
85 '--total': totalLetters,
86 '--font-size': fontSize,
87 '--radius': radius,
88 fontSize: `calc(var(--font-size, 2) * 1rem)`,
89 transform: `
90 translate(-50%, -50%)
91 rotate(calc(360deg / var(--total) * var(--index)))
92 translateY(calc(var(--radius, 5) * -1ch))
93 `,
94 transformOrigin: 'center',
95 } as React.CSSProperties
96 }
97 >
98 {letter}
99 </motion.span>
100 ))}
101 <span className='sr-only'>{children}</span>
102 </motion.div>
103 );
104}
105