A marker-pen sweep draws behind your key words as they scroll into view, line by line like a real pen.
We build systems that sell for brands that are growing.
Customize
Usage
import { Highlighter } from "@/components/motionkit/highlighter";
<p>
We build{" "}
<Highlighter>
systems that sell
</Highlighter>{" "}
for brands that are growing.
</p>Motion
The exact curves this component runs, plotted with Motion's own maths.
Marker sweep
Scrolls into view
Works in the App Router as is. Requires Tailwind CSS v4.
npm install motionSave as components/motionkit/highlighter.tsx.
"use client";
import { useRef, type ReactNode } from "react";
import { motion, useInView, useReducedMotion } from "motion/react";
type HighlighterProps = {
children: ReactNode;
/** Marker colour. Translucent colours read best, e.g. "#0E7C8666". */
color?: string;
/** "marker" fills most of the line height; "underline" sits low, like a thick pen underline. */
variant?: "marker" | "underline";
/** Seconds for one full sweep. */
duration?: number;
/** Seconds to wait once in view, so several highlights can go in turn. */
delay?: number;
className?: string;
};
/** A hand-drawn marker stroke: slightly uneven edges, so it reads as ink rather than a box. */
function markerStroke(color: string, variant: "marker" | "underline") {
const shape =
variant === "marker"
? "M2 6 Q 30 2 60 5 T 118 4 Q 119 12 117 21 Q 88 24 60 22 T 3 23 Q 1 14 2 6 Z"
: "M2 15 Q 30 12 60 14 T 118 13 Q 119 18 117 22 Q 88 24 60 23 T 3 23 Q 1 19 2 15 Z";
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 26" preserveAspectRatio="none"><path d="${shape}" fill="${color}"/></svg>`;
return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
}
export function Highlighter({
children,
color = "#0E7C8673",
variant = "marker",
duration = 0.8,
delay = 0,
className = "",
}: HighlighterProps) {
const ref = useRef<HTMLSpanElement>(null);
const inView = useInView(ref, { once: true, amount: "all" });
const reduceMotion = useReducedMotion();
const drawn = inView || reduceMotion;
return (
<motion.span
ref={ref}
// left as one continuous strip across wrapped lines, so the sweep runs line by line like a real pen
className={`bg-no-repeat px-[0.15em] [-webkit-box-decoration-break:slice] [box-decoration-break:slice] ${className}`}
style={{ backgroundImage: markerStroke(color, variant), backgroundPosition: "0 55%" }}
initial={{ backgroundSize: reduceMotion ? "100% 100%" : "0% 100%" }}
animate={{ backgroundSize: drawn ? "100% 100%" : "0% 100%" }}
transition={{ duration: reduceMotion ? 0 : duration, delay: reduceMotion ? 0 : delay, ease: [0.65, 0, 0.35, 1] }}
>
{children}
</motion.span>
);
}
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | — | The words to highlight. Use it inline, inside a sentence. |
| color | string | "#0E7C8673" | Marker colour. Translucent colours read best. |
| variant | "marker" | "underline" | "marker" | A full marker band, or a thick low underline. |
| duration | number | 0.8 | Seconds for one sweep. |
| delay | number | 0 | Seconds to wait, so several highlights go in turn. |
Ship it today
Underline Draw
A hand-drawn scribble, swoosh, double line, circle or strike-through traces itself around your words.
Monthly revenue
Number Ticker
Counts up to a value with an eased finish when it scrolls into view.
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.