Text✨ Premium

Text Whirlpool

Characters fly out to a circular orbit then converge back into readable text in a single spring-like motion — 100% CSS transitions, no canvas.

JSAnimationOrbit

You'll get access to the interactive demo with a free account.

This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Text has 56 effects, including 4 free. Explore the category →

3 usage examples

Creative portfolio — Animated main heading on page entry — Text Whirlpool example 1

① Creative portfolio — Animated main heading on page entry

WhenThe page loads or the visitor reaches the first screen — a large keyword (STUDIO, MOTION, VISUAL…) emerges from its orbit and settles, capturing attention immediately.
WhyOn a wide blank surface, the circular convergence creates a strong typographic moment without visual overload; text is perfectly readable as soon as the chars settle.
Settingsfont-size 5–7rem, orbital radius ≈ 120 px (adjust RADIUS in init), auto-triggered via IntersectionObserver, monochrome white-on-dark palette.
Premium product page — Name revealed on page open — Text Whirlpool example 2

② Premium product page — Name revealed on page open

WhenThe visitor opens a high-end product page — the name whirls from its orbit and lands in a single motion, like a solemn unveiling unique to that visit.
WhyA one-time event per page, never repeated: the circular convergence lends the name a gravitas that no fade or slide can match. The spectacular animation stays exceptional because it is never looped.
Settingsfont-size 3–4rem, orbital radius ≈ 90 px (RADIUS 90 in the JS init), transition 1.2s cubic-bezier(.34,1.56,.64,1), orbital opacity 0.424, gold or platinum-white palette on near-black background — auto-triggered on page open, no click replay.
Web game — Level or title reveal — Text Whirlpool example 3

③ Web game — Level or title reveal

WhenA mini-game's title screen appears or a new level is announced — letters whirl before snapping to the level or game name.
WhyThe circular orbit and spring rebound feel cinematic and match game visual conventions; click-to-replay lets the title re-animate on every level reveal without reloading.
Settingsfont-size 3rem bold, vivid colors (gold or neon), radius ≈ 90 px, repeat on click for each level announcement.

How it works

The utility function splitChars() breaks the data-text attribute into individual <span class="char"> elements. The JS init then computes an angular position on a circle of radius ≈ 77 px for each span using the formula angle = phase + index × (2π / n) (n = character count, phase ≈ 14.4°), converted to Cartesian coordinates via Math.cos / Math.sin.

Each char receives an inline style position:absolute; transform:translate(x,y) rotate(14.4deg); opacity:0.424; transition:none. The inline transition:none is critical: it blocks any animation on load even though the CSS rule .kt-whirlpool .char { transition:1.2s cubic-bezier(.34,1.56,.64,1) } is already present. The JS sets the .kt-whirlpool parent to position:relative, collapsing it to zero size inside its flex-centered container — the chars therefore orbit around the visual center of the area.

To trigger the animation, two nested requestAnimationFrame calls remove the transition:none override (via c.style.removeProperty('transition')) then add the .active class to the container. The rules .kt-whirlpool.active .char { position:relative!important; transform:translate(0,0) rotate(0deg)!important; opacity:1!important } take effect: each char leaves its orbit and returns to its natural position in the HTML text flow. The cubic-bezier(.34,1.56,.64,1) produces a slight overshoot then rebound — the spring effect.

Clicking the container replays the cycle: .active is removed, circular inline styles are re-applied with transition:none, then the double-rAF re-triggers the animation. The effect is entirely DOM-based — no canvas, no external library.

Accessibility

  • prefers-reduced-motion: not implemented in the provided code. Minimal solution to add: @media (prefers-reduced-motion: reduce) { .kt-whirlpool .char { transition: none !important; } } and trigger .active immediately without the double-rAF delay.
  • No aria-* attributes in the base HTML structure — add role="img" and aria-label="[your text]" on the container if the effect serves as a decorative heading.
  • Text content lives in the DOM as readable <span> elements — screen readers access it regardless of the animation state.
  • Keyboard interaction is not natively supported (non-focusable <div> container) — add tabindex="0" and a keydown listener (Enter/Space) to make replay accessible.
  • No aria-live region: the visual state change is not announced to assistive technologies.

Browser compatibility

Requires CSS transitions and requestAnimationFrame — supported in all modern browsers. Zero external dependencies.

Chrome 90+✓ Full
Firefox 90+✓ Full
Safari 15+✓ Full
Edge 90+✓ Full
Mobile iOS✓ Touch OK
Android Chrome✓ Full

Without CSS transitions, characters jump directly to their readable final state with no animation — no JS errors, text remains displayed and legible.

The code

HTML structure to paste into your page (CSS + JS available with a premium account):

index.html — structure
<div class="demo-preview">
  <div class="kt-text kt-whirlpool" id="whirlpool1" data-text="VORTEX" style="cursor: pointer;">
    <!-- One <span class="char" data-char="V">V</span> per character
         orbit-positioned by the JS init (position:absolute + translate) -->
  </div>
  <span class="demo-hint">Click to replay</span>
</div>
🔒 Unlock the full code — from €2.99 the first month

Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
data-text "VORTEX" Text to animate — change the HTML attribute directly. Character count automatically adjusts the angular spacing (360° / n).
font-size on .kt-text 2.2rem Character size. A larger size calls for a wider orbital radius — increase the RADIUS constant in the JS init proportionally.
color on .kt-text #fff Character color in the readable state. Orbit opacity (0.424) is set by the JS init inline style and applies regardless of color.
transition in .kt-whirlpool .char 1.2s cubic-bezier(.34, 1.56, .64, 1) Duration and curve of convergence. Increase to 1.8s for slow cinematic motion, decrease to 0.6s for a snappy animation. Modify the cubic-bezier for more or less rebound (2nd value > 1 = overshoot).
RADIUS (JS init constant) ~77 Orbital circle radius in pixels. Calibrate against character count and font-size — a 4-letter word at 5rem needs a radius of 100–120 px.
opacity: 0.424 (JS init inline style) 0.424 Character opacity during orbit before convergence. 0 = invisible at start (sudden appearance), 0.7 = semi-visible (softer effect).
rotate(14.4deg) (JS init inline style) 14.4deg Uniform rotation applied to all characters in orbit. 0deg = upright letters during orbit, 45deg = strongly tilted characters.

FAQ

Yes — the effect is 100% vanilla: DOM <span> elements, CSS transitions, and a few lines of JS for the orbital position math. Zero npm dependencies, no canvas. Compatible with any framework: wrap the container in a component, trigger .active class addition from your own logic, and remove it to reset.
Edit the data-text attribute directly in the HTML (data-text="STUDIO"). The JS init reads that attribute, recreates the <span class="char"> elements, and recalculates the angular positions automatically — no script changes needed.
Yes — the JS init must target each .kt-whirlpool by element (not by a shared global variable): give each container a distinct id. Each then gets its own .active state, isolated click listener, and independent chars. No state variable is shared between instances.