Bars cover the key facts like a classified file, then lift one by one on scroll, hover or click.
We cut load time by 62% in six weeks.
Customize
Usage
import { RedactedReveal } from "@/components/motionkit/redacted-reveal";
<RedactedReveal
text="We cut [load time] by [62%] in [six weeks]."
/>Motion
The exact curves this component runs, plotted with Motion's own maths.
Bar lift
Scrolls into view
Works in the App Router as is. Requires Tailwind CSS v4.
npm install motionSave as components/motionkit/redacted-reveal.tsx.
"use client";
import { Fragment, useRef, useState, type ElementType } from "react";
import { motion, useInView, useReducedMotion } from "motion/react";
type RedactedRevealProps = {
/** The sentence. Wrap the parts to redact in square brackets: "We cut [load time] by [62%]." */
text: string;
/** "view" lifts the bars on scroll into view; "hover" and "click" wait for the reader. */
trigger?: "view" | "hover" | "click";
/** Bar colour. On a dark page a light bar reads as redaction, as black does on paper. */
barColor?: string;
/** Seconds each bar takes to lift. */
duration?: number;
/** Seconds between one bar and the next. */
stagger?: number;
/** Element to render, e.g. "p". */
as?: ElementType;
className?: string;
};
export function RedactedReveal({
text,
trigger = "view",
barColor = "#F6F1E8",
duration = 0.5,
stagger = 0.25,
as: Tag = "p",
className = "",
}: RedactedRevealProps) {
const ref = useRef<HTMLElement>(null);
const inView = useInView(ref, { once: true, amount: 0.8 });
const reduceMotion = useReducedMotion();
const [asked, setAsked] = useState(false);
const revealed = trigger === "view" ? inView : asked;
// odd entries are the bracketed, redacted parts
const parts = text.split(/\[([^\]]+)\]/);
let barIndex = 0;
const interactive = trigger !== "view" && !revealed;
return (
<Tag
ref={ref}
className={`${className} ${interactive ? "cursor-pointer" : ""}`}
onPointerEnter={trigger === "hover" ? () => setAsked(true) : undefined}
onClick={trigger === "click" ? () => setAsked(true) : undefined}
>
{parts.map((part, i) => {
if (i % 2 === 0) return <Fragment key={i}>{part}</Fragment>;
const delay = barIndex++ * stagger;
return (
<span key={i} className="relative inline-block whitespace-nowrap">
{part}
<motion.span
aria-hidden
className="absolute inset-x-[-0.1em] inset-y-[0.08em] origin-right rounded-[2px]"
style={{ backgroundColor: barColor }}
initial={false}
animate={{ scaleX: revealed ? 0 : 1 }}
transition={{ duration: reduceMotion ? 0 : duration, delay: reduceMotion || !revealed ? 0 : delay, ease: [0.65, 0, 0.35, 1] }}
/>
</span>
);
})}
{/* a real button for keyboard users when the reader has to ask for the reveal */}
{interactive && (
<button type="button" onClick={() => setAsked(true)} className="sr-only focus:not-sr-only focus:ml-2 focus:underline">
Reveal redacted text
</button>
)}
</Tag>
);
}
| Prop | Type | Default | Description |
|---|---|---|---|
| text | string | — | The sentence. Wrap redacted parts in [square brackets]. |
| trigger | "view" | "hover" | "click" | "view" | What lifts the bars. |
| barColor | string | "#F6F1E8" | Bar colour. Light bars read as redaction on a dark page. |
| duration | number | 0.5 | Seconds each bar takes to lift. |
| stagger | number | 0.25 | Seconds between bars. |
| as | ElementType | "p" | Element to render. |
Small details make the whole thing feel alive.
Word Rise
Words lift out of a blur one after another as the line scrolls into view.
Small details, big impact.
Kinetic Text
Headlines that rise letter by letter, slide up line by line, blur into focus or light up as you scroll.
Think beyond ordinary.
Gradient Text
Headline text with a slowly flowing, seamlessly looping gradient.