Shutter Text
A cinematic shutter-style text reveal animation with layered color slices that sweep across each character. Supports auto, scroll, click, and hover triggers.
Last updated on
import ShutterText from "@/components/ui/shutter-text";
export function ShutterTextDemo() {
return (
<div className="flex min-h-[300px] w-full items-center justify-center">
<ShutterText text="IMMERSE" className="text-7xl" />
</div>
);
}Installation
CLI
npx shadcn@latest add "https://jolyui.dev/r/shutter-text"Manual
Install the required dependencies:
npm install motionCopy and paste the following code into your project.
"use client";
import { AnimatePresence, motion, useInView } from "framer-motion";
import {
type HTMLAttributes,
useCallback,
useEffect,
useRef,
useState,
} from "react";
interface ShutterTextProps extends HTMLAttributes<HTMLDivElement> {
text?: string;
trigger?: "auto" | "scroll" | "click" | "hover";
}
export function ShutterText({
text = "IMMERSE",
trigger = "auto",
className = "",
...props
}: ShutterTextProps) {
const [count, setCount] = useState(0);
const [active, setActive] = useState(trigger === "auto");
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: false, amount: 0.5 });
const characters = text.split("");
// Handle scroll trigger
useEffect(() => {
if (trigger === "scroll" && isInView) {
setActive(true);
setCount((c) => c + 1);
}
if (trigger === "scroll" && !isInView) {
setActive(false);
}
}, [trigger, isInView]);
// Handle auto trigger – animate once on mount
useEffect(() => {
if (trigger === "auto") {
setActive(true);
setCount((c) => c + 1);
}
}, [trigger]);
const handleClick = useCallback(() => {
if (trigger === "click") {
setActive(true);
setCount((c) => c + 1);
}
}, [trigger]);
const handleMouseEnter = useCallback(() => {
if (trigger === "hover") {
setActive(true);
setCount((c) => c + 1);
}
}, [trigger]);
const handleMouseLeave = useCallback(() => {
if (trigger === "hover") {
setActive(false);
}
}, [trigger]);
return (
<div
ref={ref}
role="button"
tabIndex={0}
onClick={handleClick}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className={`relative inline-flex flex-wrap items-center justify-center ${className}`}
{...props}
>
<AnimatePresence mode="wait">
{active && (
<motion.span
key={count}
className="flex flex-wrap items-center justify-center"
>
{characters.map((char, i) => (
<span
key={i}
className="relative inline-block overflow-hidden px-[0.1vw]"
>
{/* Main Character */}
<motion.span
initial={{ opacity: 0, filter: "blur(10px)" }}
animate={{ opacity: 1, filter: "blur(0px)" }}
transition={{ delay: i * 0.04 + 0.3, duration: 0.8 }}
className="inline-block font-black text-zinc-900 leading-none tracking-tighter dark:text-white"
>
{char === " " ? "\u00A0" : char}
</motion.span>
{/* Top Slice Layer */}
<motion.span
initial={{ x: "-100%", opacity: 0 }}
animate={{ x: "100%", opacity: [0, 1, 0] }}
transition={{
duration: 0.7,
delay: i * 0.04,
ease: "easeInOut",
}}
className="pointer-events-none absolute inset-0 z-10 inline-block font-black text-indigo-600 leading-none dark:text-emerald-400"
style={{ clipPath: "polygon(0 0, 100% 0, 100% 35%, 0 35%)" }}
>
{char}
</motion.span>
{/* Middle Slice Layer */}
<motion.span
initial={{ x: "100%", opacity: 0 }}
animate={{ x: "-100%", opacity: [0, 1, 0] }}
transition={{
duration: 0.7,
delay: i * 0.04 + 0.1,
ease: "easeInOut",
}}
className="pointer-events-none absolute inset-0 z-10 inline-block font-black text-zinc-800 leading-none dark:text-zinc-200"
style={{
clipPath: "polygon(0 35%, 100% 35%, 100% 65%, 0 65%)",
}}
>
{char}
</motion.span>
{/* Bottom Slice Layer */}
<motion.span
initial={{ x: "-100%", opacity: 0 }}
animate={{ x: "100%", opacity: [0, 1, 0] }}
transition={{
duration: 0.7,
delay: i * 0.04 + 0.2,
ease: "easeInOut",
}}
className="pointer-events-none absolute inset-0 z-10 inline-block font-black text-indigo-600 leading-none dark:text-emerald-400"
style={{
clipPath: "polygon(0 65%, 100% 65%, 100% 100%, 0 100%)",
}}
>
{char}
</motion.span>
</span>
))}
</motion.span>
)}
</AnimatePresence>
</div>
);
}Layout
import ShutterText from "@/components/ui/shutter-text";
<ShutterText text="IMMERSE" trigger="auto" className="text-7xl" />Examples
Scroll Triggered
The animation triggers when the component scrolls into view.
Scroll down to trigger the animation
"use client";
import ShutterText from "@/components/ui/shutter-text";
export function ShutterTextScrollDemo() {
return (
<div className="flex min-h-[400px] w-full flex-col items-center justify-center gap-4">
<p className="text-muted-foreground text-sm">
Scroll down to trigger the animation
</p>
<ShutterText text="SHUTTER" trigger="scroll" className="text-7xl" />
</div>
);
}Click Triggered
Trigger the shutter animation on click for interactive experiences.
Click to trigger the shutter effect
"use client";
import ShutterText from "@/components/ui/shutter-text";
export function ShutterTextClickDemo() {
return (
<div className="flex min-h-[300px] w-full flex-col items-center justify-center gap-4">
<p className="text-muted-foreground text-sm">
Click to trigger the shutter effect
</p>
<ShutterText
text="CLICK ME"
trigger="click"
className="cursor-pointer text-7xl"
/>
</div>
);
}Hover Triggered
Trigger the shutter animation on hover for subtle interactivity.
Hover to trigger the shutter effect
"use client";
import ShutterText from "@/components/ui/shutter-text";
export function ShutterTextHoverDemo() {
return (
<div className="flex min-h-[300px] w-full flex-col items-center justify-center gap-4">
<p className="text-muted-foreground text-sm">
Hover to trigger the shutter effect
</p>
<ShutterText
text="HOVER"
trigger="hover"
className="cursor-pointer text-8xl"
/>
</div>
);
}API Reference
ShutterText
The main component that creates a layered shutter text reveal animation.
Prop
Type
Notes
- Font size is controlled via
className(e.g.text-7xl), making the component fully composable. - Each character is animated independently with staggered timing for a cinematic reveal.
- Three colored slice layers (top, middle, bottom) sweep across each character during the reveal.
- The component extends standard
divprops — you can pass any HTML div attribute. - Works with both light and dark themes out of the box.
How is this guide?
Scroll Text
Scroll-triggered text animations for React. Includes fade, blur, scale, parallax, and sticky reveal effects. Built with Framer Motion.
Text Morphing
Smooth text morphing animation for React. Words transition with blur and scale effects. Perfect for dynamic hero sections and attention-grabbing headlines.