Skip to Content

Ticker

A Spotify-style ticker for long text. The component measures both its container and its content; when the text overflows, it scrolls back-and-forth at a configurable speed with gradient fade masks on the edges. When it fits, the component does nothing; no animation, no mask, no layout cost.

News
Health Closes Pre-Seed Funding to Automate Healthcare Compliance and Risk Management
import { Ticker } from "@togetheragency/ui/ticker"; export function NewsTicker() { return ( <div className="flex items-center gap-3 rounded-lg bg-neutral-900 px-3 py-2.5 text-white"> <span className="shrink-0 rounded border border-white/30 px-2.5 py-1 text-sm font-medium"> News </span> <Ticker.Root className="flex-1" startDelay={2000} endDelay={2000} scrollSpeed={40} > <Ticker.Content> Health Closes Pre-Seed Funding to Automate Healthcare Compliance and Risk Management </Ticker.Content> </Ticker.Root> </div> ); }

Import

import { Ticker } from "@togetheragency/ui/ticker";

The component is exported as a compound object; both sub-components are properties of Ticker:

Ticker.Root; Ticker.Content;

Composition

Ticker is a two-part component. Root measures and animates; Content renders the actual text.

<Ticker.Root> <Ticker.Content>Some long text that may overflow…</Ticker.Content> </Ticker.Root>
  • Ticker.Root renders a <div> with overflow: hidden; white-space: nowrap. Watches its width and the inner content’s width via a ResizeObserver; when content exceeds container, it kicks off the scroll loop.
  • Ticker.Content renders a <span> that receives a string child. The string is required (not arbitrary JSX) so the component can detect content changes and reset the animation cleanly.

The animation loop drives transform and the CSS mask-image via direct DOM writes inside requestAnimationFrame — no React state updates per frame. The only React state is the overflow flag, which only flips on resize or text change. Even on long lists of tickers, this stays cheap.

Usage

Basic usage

Drop the component into any container that has a constrained width. When the text fits, nothing animates.

<Ticker.Root className="flex-1"> <Ticker.Content> Some long string that may or may not overflow its container. </Ticker.Content> </Ticker.Root>

Short text (no scrolling)

If the content fits, the component stays static — no mask, no transform, no animation. This is the same component, just with shorter text.

Short text that fits
<Ticker.Root> <Ticker.Content>Short text that fits</Ticker.Content> </Ticker.Root>

Fixed container width

Pass containerWidth (in pixels) to pin the track to an exact width regardless of parent layout — useful inside flex parents that would otherwise let the child grow.

This is a much longer text that will definitely overflow the 200px container
<Ticker.Root containerWidth={200} className="rounded bg-neutral-100 px-2 py-1" startDelay={1500} scrollSpeed={30} > <Ticker.Content> This is a much longer text that will definitely overflow the 200px container </Ticker.Content> </Ticker.Root>

Tuning the speed

scrollSpeed is in pixels per second. Same text, three tempos.

Slow (25px/s)

A slow scrolling text that takes its time to reveal all the content gradually

Medium (50px/s)

A medium speed scrolling text that moves at a comfortable pace for reading

Fast (100px/s)

A fast scrolling text that quickly reveals the content - good for shorter delays
<Ticker.Root scrollSpeed={25}>…</Ticker.Root> <Ticker.Root scrollSpeed={50}>…</Ticker.Root> <Ticker.Root scrollSpeed={100}>…</Ticker.Root>

Edge fades

fadeWidth controls the gradient mask on each edge — a narrow value gives a subtle hint of clipping, a wide value reads as a deliberate dramatic effect.

Narrow fade (12px)

This text has a narrow fade mask on the edges creating a subtle transition effect

Wide fade (48px)

This text has a wide fade mask on the edges creating a more dramatic gradient effect
<Ticker.Root fadeWidth={12}>…</Ticker.Root> <Ticker.Root fadeWidth={48}>…</Ticker.Root>

Delays

startDelay and endDelay (in milliseconds) control how long the component pauses before starting and at the end of each pass. Defaults are 2000ms for both — feel free to lengthen for ambient tickers or shorten for active feeds.

<Ticker.Root startDelay={3000} endDelay={1500}> <Ticker.Content>…</Ticker.Content> </Ticker.Root>

Dynamic text

Updating the children of Ticker.Content cleanly resets the animation. The component re-measures and decides whether to scroll based on the new content’s width.

This text can be changed dynamically to test re-renders and animation reset!
const [text, setText] = useState("Original text"); <Ticker.Root> <Ticker.Content>{text}</Ticker.Content> </Ticker.Root>;

Examples

News ticker

Inline label, scrolling headline, trailing icon. Only the headline runs — the surrounding chrome stays put.

News
Health Closes Pre-Seed Funding to Automate Healthcare Compliance and Risk Management
<div className="flex items-center gap-3 rounded-lg bg-neutral-900 px-3 py-2.5 text-white"> <span className="shrink-0 rounded border border-white/30 px-2.5 py-1 text-sm font-medium"> News </span> <Ticker.Root className="flex-1" startDelay={2000} scrollSpeed={40}> <Ticker.Content> Health Closes Pre-Seed Funding to Automate Healthcare Compliance and Risk Management </Ticker.Content> </Ticker.Root> <ArrowRight className="size-5 shrink-0 opacity-60" /> </div>

Player row

Two stacked scrollers with independent delays — title runs first, artist follows. Each Root has its own animation loop and measurements.

Bohemian Rhapsody - 2011 Remaster (Super Deluxe Edition)
Queen • A Night at the Opera (Deluxe Remastered Version)
<div className="flex items-center gap-4 rounded-xl bg-emerald-900 p-4"> <div className="size-14 shrink-0 rounded-md bg-emerald-700" /> <div className="min-w-0 flex-1 space-y-1"> <Ticker.Root className="font-medium text-white" startDelay={3000} endDelay={2000} scrollSpeed={35} > <Ticker.Content> Bohemian Rhapsody - 2011 Remaster (Super Deluxe Edition) </Ticker.Content> </Ticker.Root> <Ticker.Root className="text-sm text-emerald-200" startDelay={3500} scrollSpeed={30} > <Ticker.Content> Queen • A Night at the Opera (Deluxe Remastered Version) </Ticker.Content> </Ticker.Root> </div> </div>

Notification banner

A pulsing status dot paired with a scrolling message — the mask hides clipping at both edges so the strip reads as live.

System update available: Version 2.4.1 includes performance improvements and bug fixes. Click here to learn more.
<div className="flex items-center gap-3 rounded-lg bg-blue-600 px-4 py-3"> <span className="relative flex size-2 shrink-0"> <span className="absolute inline-flex size-full animate-ping rounded-full bg-white opacity-75" /> <span className="relative inline-flex size-2 rounded-full bg-white" /> </span> <Ticker.Root className="flex-1 font-medium text-white" startDelay={2500} scrollSpeed={45}> <Ticker.Content> System update available: Version 2.4.1 includes performance improvements and bug fixes. Click here to learn more. </Ticker.Content> </Ticker.Root> </div>

API Reference

Ticker.Root

Renders a <div> with relative overflow-hidden whitespace-nowrap. Measures its width and the inner content’s scrollWidth, then drives a back-and-forth scroll animation when content overflows. Extends all native div props.

PropTypeDefaultDescription
containerWidthnumberundefinedOptional fixed container width in pixels. When unset, the width is inferred from the parent layout via offsetWidth + ResizeObserver.
startDelaynumber2000Milliseconds to wait before starting each scroll pass.
endDelaynumber2000Milliseconds to pause at the end of a pass before scrolling back.
scrollSpeednumber50Animation speed in pixels per second.
fadeWidthnumber24Width of the gradient fade mask on each edge, in pixels. Clamped to at most 20% of the container width.
fadeTransitionDurationnumber300Milliseconds for the mask fade transition when scrolling starts/stops.
classNamestringMerged via cn. Defaults include relative overflow-hidden whitespace-nowrap.
styleReact.CSSPropertiesMerged with the component’s inline width (when containerWidth is set) and mask-image transition.
...restReact.ComponentPropsWithoutRef<"div">Forwarded to the underlying <div>.

Built-in attributes: aria-live="polite".

Data attributes: data-slot="ticker-root", data-state="idle" | "scrolling", data-overflowing (empty string when content overflows, otherwise absent).

Ticker.Content

Renders an inline-block <span> that holds the text. The component registers itself with the root via context — there’s only one Ticker.Content per root.

PropTypeDefaultDescription
childrenstringrequiredThe text content. Must be a string — changing it cleanly resets the animation and re-measures whether scrolling is needed.
classNamestringMerged via cn. Default is inline-block.
styleReact.CSSPropertiesMerged with the component’s will-change: transform (only applied while scrolling).
...restOmit<React.ComponentPropsWithoutRef<"span">, "children">Forwarded to the underlying <span>.

Data attributes: data-slot="ticker-content".

Accessibility

  • Ticker.Root sets aria-live="polite" so screen readers announce content updates without interrupting.
  • Because animation runs on transform only (no text recreation), screen readers see a stable string regardless of scroll state.
  • The component intentionally does not animate when content fits — the mask is removed, transform resets to 0, and there’s no will-change hint on the inner span. Static content costs nothing.

Exported types

import type { TickerContentProps, TickerRootProps, } from "@togetheragency/ui/ticker";
Last updated on