← Components

Settle Text

Headline rows that slide in while their letters drop in and settle.

Text · FreeInstall ↓
Open preview ↗
Loading the demoNew tab ↗

Customize

1.0 s
100 %
0.040
150 px
Your code
<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.json

It 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
  1. Install the packages it uses: gsap, @gsap/react.
  2. Copy the component file below into components/dimaac/settle-text.tsx.
  3. It's styled with Tailwind v4 classes. If that folder sits outside the places Tailwind scans, add it with @source in your CSS.
  4. Import it with import { SettleText } from "@/components/dimaac/settle-text" and give its parent a size: it fills it.

Props

PropTypeDefaultDescription
lines (required)string[]-The headlines, one per row.
scrubnumber1Scroll smoothing in seconds: how long the text takes to catch up with the scrollbar. 0 follows it exactly.
offsetnumber100How far a row slides in from the side, as a percent of its width. 0 means no slide.
letterStaggernumber0.04How much later each letter settles than the one before it, as a fraction of the row's scroll.
dropnumber150How far, in pixels, the letters start above or below their place.
alternatebooleantrueSlide every other row in from the opposite side, and settle its letters from the other end.
scrollerHTMLElement | string | nullnullThe element that scrolls (an element or a CSS selector). The default is the window.
classNamestring""Extra classes for the outer box.
rowClassNamestring"h-[85svh]"Extra classes for every row. Sets the row height (85svh by default) and can set a background.
rowClassNamesstring[][]Extra classes per row, repeating if there are more rows than entries. Handy for a background per row.
headingClassNamestring"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.

Prompt
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.txt

Files 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