A heading drawn as an outline that floods with solid colour from left to right, on hover or as it scrolls into view.
Customize
Usage
import { OutlineFillText } from "@/components/motionkit/outline-fill-text";
<OutlineFillText text="Make it move" className="text-7xl font-bold" />Motion
The exact curves this component runs, plotted with Motion's own maths.
Colour flood
Hover
Display headings on portfolios, agency sites and launch pages, and hover states on large navigation or project links.
The visible heading is real text drawn with a transparent fill and a -webkit-text-stroke outline. Exactly on top sits a filled copy, hidden from assistive tech, clipped with clip-path: inset() from the right. Animating that inset from 100% to 0% pours the colour across the word from left to right, with an ease that starts and finishes gently. In hover mode it drains back when the pointer leaves; in view mode it fills once.
Works in the App Router as is. Requires Tailwind CSS v4.
One command adds the file and installs its dependencies. Needs a components.json (run npx shadcn@latest init once if you don't have one).
npx shadcn@latest add https://www.motionkitui.com/r/outline-fill-text.jsonnpm install motionSave as components/motionkit/outline-fill-text.tsx.
"use client";
import { useRef, useState, type ElementType } from "react";
import { motion, useInView, useReducedMotion } from "motion/react";
type OutlineFillTextProps = {
text: string;
/** "hover" fills while the pointer or focus is on it; "view" fills once when it scrolls into view. */
trigger?: "hover" | "view";
/** Colour of the fill. */
color?: string;
/** Colour of the outline. */
stroke?: string;
/** Outline width in pixels. */
strokeWidth?: number;
/** Seconds for the fill to cross the word. */
duration?: number;
as?: ElementType;
className?: string;
};
/**
* A heading drawn as an outline that floods with solid colour from left to right, on hover or when it
* comes into view, with a bright leading edge as the colour pours in.
*/
export function OutlineFillText({
text,
trigger = "hover",
color = "#F6F1E8",
stroke = "#5FAEB4",
strokeWidth = 1.5,
duration = 0.7,
as: Tag = "h2",
className = "",
}: OutlineFillTextProps) {
const reduceMotion = useReducedMotion();
const ref = useRef<HTMLSpanElement>(null);
const inView = useInView(ref, { once: true, amount: 0.6 });
const [hovered, setHovered] = useState(false);
const filled = reduceMotion || (trigger === "view" ? inView : hovered);
return (
<Tag
className={`relative inline-block ${className}`}
onPointerEnter={trigger === "hover" ? () => setHovered(true) : undefined}
onPointerLeave={trigger === "hover" ? () => setHovered(false) : undefined}
>
{/* the outline is the real, readable text; the filled copy above it is decoration */}
<span ref={ref} style={{ color: "transparent", WebkitTextStroke: `${strokeWidth}px ${stroke}` }}>
{text}
</span>
<motion.span
aria-hidden
className="pointer-events-none absolute inset-0"
style={{ color, WebkitTextStroke: `${strokeWidth}px ${color}` }}
initial={false}
animate={{ clipPath: filled ? "inset(0 0% 0 0)" : "inset(0 100% 0 0)" }}
transition={{ duration: reduceMotion ? 0 : duration, ease: [0.65, 0, 0.35, 1] }}
>
{text}
</motion.span>
</Tag>
);
}
| Prop | Type | Default | Description |
|---|---|---|---|
| text | string | — | The heading. |
| trigger | "hover" | "view" | "hover" | Fill on hover, or once when it scrolls into view. |
| color | string | "#F6F1E8" | Fill colour. |
| stroke | string | "#5FAEB4" | Outline colour. |
| strokeWidth | number | 1.5 | Outline width in pixels. |
| duration | number | 0.7 | Seconds for the fill. |
| as | ElementType | "h2" | The element to render. |
How we audit: the WCAG 2.2 animation checklist and respecting reduced motion in React.
Yes. The file starts with "use client", so you can import it straight into a Server Component page or layout. It works in Vite as is, and in Astro as a React island.
Only the motion package, beside React and Tailwind CSS v4. There is no MotionKit package and no Tailwind config to add.
Yes. Run npx shadcn@latest add https://www.motionkitui.com/r/outline-fill-text.json in any project with a components.json. It saves the file to components/motionkit and installs its dependencies.
With reduced motion on, the heading is shown filled straight away.