Rotating Text
Rotating Text brings controlled expression to your interface.
Make it yours.
Install the editable source and its dependencies into your React + Tailwind project.
Scroll horizontally for long lines
pnpm dlx shadcn@4.0.8 add http://localhost:3000/r/rotating-text.jsonUse it
Scroll horizontally for long lines
"use client";
import { RotatingText } from "@/components/jez-ui/ui/rotating-text";
export default function Example() {
return (
<>
<div className="w-full rounded-xl bg-muted p-8 md:p-14">
<div className="font-display text-5xl font-medium leading-tight tracking-tight md:text-6xl">
A place for
<br />
<RotatingText
words={["bold ideas.", "small details.", "your next thing."]}
className="text-primary"
/>
</div>
<p className="mt-9 text-sm text-muted-foreground">Keep making room.</p>
</div>
</>
);
}
Props & types
Scroll horizontally for long lines
Omit<
React.ComponentProps<"span">,
keyof {
words: string[];
interval?: number;
className?: string;
}
> & {
words: string[];
interval?: number;
className?: string;
}Source
View 2 source files
registry/ui/rotating-text.tsx
Scroll horizontally for long lines
"use client";
import * as React from "react";
import { cn } from "./utils";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
export function RotatingText({
children,
words,
interval = 2400,
className,
...rootProps
}: Omit<
React.ComponentProps<"span">,
keyof {
words: string[];
interval?: number;
className?: string;
}
> & {
words: string[];
interval?: number;
className?: string;
}) {
const [index, setIndex] = React.useState(0);
const reduce = useReducedMotion();
React.useEffect(() => {
if (reduce || words.length < 2) return;
const timer = setInterval(
() => setIndex((i) => (i + 1) % words.length),
interval,
);
return () => clearInterval(timer);
}, [reduce, words.length, interval]);
return (
<span
{...rootProps}
className={cn("inline-grid overflow-hidden align-bottom", className)}
aria-label={words.join(", ")}
>
{children !== undefined ? (
children
) : (
<>
<AnimatePresence mode="wait">
<motion.span
aria-hidden="true"
key={index}
initial={{ y: reduce ? 0 : 24, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: reduce ? 0 : -24, opacity: 0 }}
transition={{ duration: 0.25 }}
>
{words[index]}
</motion.span>
</AnimatePresence>
</>
)}
</span>
);
}
registry/ui/utils.ts
Scroll horizontally for long lines
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...values: ClassValue[]) {
return twMerge(clsx(values));
}
Dependencies
motion@13.2.0 · clsx@2.1.1 · tailwind-merge@3.5.0
Accessibility & behaviour
Respect reduced-motion preferences. Decorative effects must not carry essential information. WebGL scenes include a static fallback and only render while visible.
Read the accessibility and performance guide →