WebberUI

3D Rolling Text

Scroll-driven 3D cylinder of text — lines are mounted on an invisible drum and rotate into view one after another as you scroll.

This is a WebberUI Pro component

Free during the launch campaign: sign up or sign in, then hit “Copy install command” in the preview above and it installs straight away — no payment, no credit card. The command below returns 401 while you are signed out.

How to install Pro components →See the plans →

Loading preview…
npx shadcn@latest add "https://webberui.com/r/3d-rolling-text.json?t=<install token>"

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

160
56
900
6
<3dRollingText />

Installation

npx shadcn@latest add "https://webberui.com/r/3d-rolling-text.json?t=<install token>"

Or, once registries are configured in components.json, install it as @webberui/3d-rolling-text.

Usage

import { RollingText3D } from "@/components/ui/3d-rolling-text";

<RollingText3D
  lines={["Design", "Animate", "Prototype", "Ship"]}
  lineClassName="text-5xl font-bold tracking-tight"
/>

The component itself is a scrollable track (total height = stageHeight + (number of lines - 1) * stepHeight) with a sticky stage pinning the visuals in place. Drop it into a page, and scrolling past the track completes one full rotation of the drum.

Nested scroll container

If the animation happens inside an overflow-y: auto container rather than the whole page, pass the container's ref to container:

const scrollerRef = React.useRef<HTMLDivElement>(null);

<div ref={scrollerRef} className="h-[300px] overflow-y-auto">
  <RollingText3D lines={words} container={scrollerRef} />
</div>

Props

PropTypeDefaultDescription
linesstring[]The lines of text on the cylinder's faces, rotating into view in order as you scroll
stepHeightnumber160Scroll distance allotted to each line (px); larger turns more slowly
stageHeightnumber200Height of the sticky stage (px)
lineHeightnumber56Line height (px), which also determines the cylinder radius
perspectivenumber9003D perspective distance (px); smaller exaggerates the perspective
direction"up" | "down""up"up rolls new lines in from below toward the front face, down rolls them in from above
minFacesnumber6Minimum number of faces the cylinder is divided into, so the drum keeps its curvature even with few lines
containerReact.RefObject<HTMLElement | null>Ref of the nested scroll container; defaults to the window as the scroll container
classNamestringClass applied to the whole scroll track
lineClassNamestringClass applied to each line of text (font size, weight, color)

How it works

  • Cylinder geometry: the radius is derived as lineHeight / 2 / tan(π / faces), so adjacent faces meet exactly on the drum with no gap and no overlap as it turns
  • Scroll mapping: useScroll takes the track's progress, and traversing the whole track rotates exactly the last line to the front ((number of lines - 1) × angle per face). Stopping partway holds the corresponding angle, and it can be reversed
  • Angular fade: each line's opacity comes from its angular distance from the front — fully lit at the front, half-transparent on the adjacent faces, and gone beyond that, with backface-visibility: hidden as a backstop against mirrored glyphs
  • With few lines: minFaces sets a floor on the number of faces, so three or four lines still keep the drum's curvature instead of degenerating into a flat page flip

Accessibility

  • When the user has "reduce motion" enabled at the system level, the 3D and sticky behavior are dropped and all lines are rendered as a static vertical list
  • The complete text is exposed sr-only to assistive technology and the entire 3D visual layer is aria-hidden, so what is announced is continuous, complete content
  • Purely scroll-driven, never autoplaying: animation progress follows the user's scrolling exactly and can be stopped or reversed at any time

On this page