Settle Text
Headline rows that slide in while their letters drop in and settle.
Customize
<SettleText />Changes show live and stay in the address, so a link keeps them.
Code
"use client";
import { useRef } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { SplitText } from "gsap/SplitText";
import { useGSAP } from "@gsap/react";
// Big headline rows. As a row scrolls into view it slides in from the side while its letters
// drop in from above and below (every other letter from the opposite side) and settle into the
// word. The next row slides in from the other side, so the page zig-zags as you scroll.
if (typeof window !== "undefined") gsap.registerPlugin(useGSAP, ScrollTrigger, SplitText);
export type SettleTextProps = {
/** The headlines, one per row. */
lines: string[];
/** Scroll smoothing in seconds: how long the text takes to catch up with the scrollbar. 0 follows it exactly. */
scrub?: number;
/** How far a row slides in from the side, as a percent of its width. 0 means no slide. */
offset?: number;
/** How much later each letter settles than the one before it, as a fraction of the row's scroll. */
letterStagger?: number;
/** How far, in pixels, the letters start above or below their place. */
drop?: number;
/** Slide every other row in from the opposite side, and settle its letters from the other end. */
alternate?: boolean;
/** The element that scrolls (an element or a CSS selector). The default is the window. */
scroller?: HTMLElement | string | null;
/** Extra classes for the outer box. */
className?: string;
/** Extra classes for every row. Sets the row height (85svh by default) and can set a background. */
rowClassName?: string;
/** Extra classes per row, repeating if there are more rows than entries. Handy for a background per row. */
rowClassNames?: string[];
/** Extra classes for the headline text, e.g. a font size or a font. */
headingClassName?: string;
};
export function SettleText({
lines,
scrub = 1,
offset = 100,
letterStagger = 0.04,
drop = 150,
alternate = true,
scroller = null,
className = "",
rowClassName = "h-[85svh]",
rowClassNames = [],
headingClassName = "text-[clamp(2.5rem,13vw,9rem)]",
}: SettleTextProps) {
const boxRef = useRef<HTMLDivElement>(null);
useGSAP(
() => {
const box = boxRef.current;
if (!box) return;
// Reduced motion: nothing is split or moved, the headlines just sit there, readable.
const mm = gsap.matchMedia();
mm.add("(prefers-reduced-motion: no-preference)", () => {
const splits: SplitText[] = [];
gsap.utils.toArray<HTMLElement>("[data-settle-row]", box).forEach((row, rowIndex) => {
const track = row.querySelector<HTMLElement>("[data-settle-track]");
const heading = row.querySelector<HTMLElement>("[data-settle-heading]");
if (!track || !heading) return;
// Each letter becomes its own inline-block, so it can move on its own.
// SplitText also keeps the whole word readable for screen readers.
const split = SplitText.create(heading, { type: "chars", charsClass: "settle-char" });
splits.push(split);
const letters = split.chars as HTMLElement[];
const count = letters.length;
if (count === 0) return;
// Odd rows go the other way: they slide in from the left and settle their last letter first.
const flipped = alternate && rowIndex % 2 === 1;
const side = flipped ? -1 : 1;
// The row's scroll is one timeline of length 1. The letters start at 0.1 and all
// finish by 1, so each letter's trip is whatever is left after the stagger.
// The stagger is capped so every letter still has a short trip.
const step = count > 1 ? Math.min(letterStagger, 0.7 / (count - 1)) : 0;
const trip = 0.9 - step * (count - 1);
const timeline = gsap.timeline({
defaults: { ease: "none" },
scrollTrigger: {
trigger: row,
...(scroller ? { scroller } : {}),
start: "top bottom",
end: "top top",
scrub: scrub > 0 ? scrub : true,
},
});
timeline.fromTo(track, { xPercent: offset * side }, { xPercent: 0, duration: 1 }, 0);
letters.forEach((letter, index) => {
const order = flipped ? count - 1 - index : index;
const from = index % 2 === 0 ? -drop : drop;
timeline.fromTo(letter, { y: from }, { y: 0, duration: trip }, 0.1 + order * step);
});
});
return () => splits.forEach((split) => split.revert());
});
return () => mm.revert();
},
{ scope: boxRef, dependencies: [lines.join("\n"), scrub, offset, letterStagger, drop, alternate, scroller], revertOnUpdate: true },
);
return (
<div ref={boxRef} className={`w-full ${className}`}>
{lines.map((line, index) => (
<div
key={`${index}-${line}`}
data-settle-row=""
className={`flex w-full items-center overflow-hidden ${rowClassName} ${rowClassNames.length ? (rowClassNames[index % rowClassNames.length] ?? "") : ""}`}
>
<div data-settle-track="" className="flex w-full justify-center will-change-transform">
<h2 data-settle-heading="" className={`m-0 font-extrabold leading-[0.92] tracking-[-0.03em] whitespace-nowrap ${headingClassName}`}>
{line}
</h2>
</div>
</div>
))}
</div>
);
}
The component. Installs to components/dimaac/settle-text.tsx.
Install
With the shadcn CLI
npx shadcn@latest add https://ui.dimaac.com/r/settle-text.jsonIt adds components/dimaac/settle-text.tsx and installs what it imports. With the @dimaac namespace in components.json (Docs) it's just npx shadcn@latest add @dimaac/settle-text.
By hand
npm install gsap @gsap/react- Install the packages it uses: gsap, @gsap/react.
- Copy the component file below into components/dimaac/settle-text.tsx.
- It's styled with Tailwind v4 classes. If that folder sits outside the places Tailwind scans, add it with @source in your CSS.
- Import it with import { SettleText } from "@/components/dimaac/settle-text" and give its parent a size: it fills it.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
lines (required) | string[] | - | The headlines, one per row. |
scrub | number | 1 | Scroll smoothing in seconds: how long the text takes to catch up with the scrollbar. 0 follows it exactly. |
offset | number | 100 | How far a row slides in from the side, as a percent of its width. 0 means no slide. |
letterStagger | number | 0.04 | How much later each letter settles than the one before it, as a fraction of the row's scroll. |
drop | number | 150 | How far, in pixels, the letters start above or below their place. |
alternate | boolean | true | Slide every other row in from the opposite side, and settle its letters from the other end. |
scroller | HTMLElement | string | null | null | The element that scrolls (an element or a CSS selector). The default is the window. |
className | string | "" | Extra classes for the outer box. |
rowClassName | string | "h-[85svh]" | Extra classes for every row. Sets the row height (85svh by default) and can set a background. |
rowClassNames | string[] | [] | Extra classes per row, repeating if there are more rows than entries. Handy for a background per row. |
headingClassName | string | "text-[clamp(2.5rem,13vw,9rem)]" | Extra classes for the headline text, e.g. a font size or a font. |
Use with AI
Agent prompt
Paste it into Claude Code, Cursor or any coding agent, or open it straight in a chat. It carries your Customize settings.
Add the Settle Text component (<SettleText />) from DiMaac UI to this project.
Docs: https://ui.dimaac.com/components/settle-text
Source and dependencies (shadcn registry JSON): https://ui.dimaac.com/r/settle-text.json
Install it with:
npx shadcn@latest add https://ui.dimaac.com/r/settle-text.json
Then use it like this:
import { SettleText } from "@/components/dimaac/settle-text";
<SettleText />
If the shadcn CLI isn't set up here, fetch the registry JSON above, write files[0].content to components/dimaac/settle-text.tsx and install gsap, @gsap/react.
It uses Tailwind v4 classes, so make sure Tailwind scans that folder. Give its parent a size, keep light and dark mode working and keep its prefers-reduced-motion fallback.
Full index for agents: https://ui.dimaac.com/llms.txtFiles agents read
- llms.txt
- Every component and how to install it
- settle-text.md
- This page as Markdown: install, props and the full source
- settle-text.json
- The shadcn registry item
About
Big headline rows driven by the scroll. As a row climbs into view it slides in from the side, and its letters fall in from above and below (every other letter from the opposite side) and settle one after another into the word. The next row comes in from the other side, so the page zig-zags as you scroll. Built with GSAP ScrollTrigger and SplitText, scrubbed so the motion follows the scrollbar with a little smoothing. It takes an optional scroller element, reverts the split text on unmount, keeps the headline readable for screen readers, and leaves the text still when the visitor prefers reduced motion. Recreated from the Scroll text YouTube tutorial.
- Installs to
components/dimaac/settle-text.tsx- Uses
- gsap, @gsap/react
- Tags
- gsap, scrolltrigger, splittext, headline, scrub