Split Hover
On hover, each letter scatters randomly across the space — a CSS elastic spring snaps it back the moment the mouse leaves.
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



How it works
On load, initSplitHover() splits the string character by character, wrapping each letter in an inline-block <span data-index>. This makes every glyph independently transformable by the CSS engine without disturbing the surrounding text flow.
On mouseenter, the .scattered class is added to the container (dropping each span's opacity to 0.3 via CSS), and each letter receives a transform: translate(Xpx, Ypx) rotate(Rdeg) computed with Math.random(): X amplitude ±50 px, Y ±40 px, rotation ±45°. Values differ on every hover — no memorised trajectory.
On mouseleave, transforms are reset to translate(0,0) rotate(0deg) and .scattered is removed. The CSS transition cubic-bezier(.68, -.55, .265, 1.55) (a back-out curve) produces a brief overshoot past the target position before settling — the spring bounce that gives the animation its elastic character.
The whole effect runs on standard CSS transitions and DOM events: no requestAnimationFrame, no library. Removing the two mouseenter/mouseleave listeners cleanly disables it with no residual state.
Accessibility
- prefers-reduced-motion NOT IMPLEMENTED: the code does not detect the system preference. Letters always animate, even when the OS requests reduced motion. Wrap the listeners in a
matchMedia('(prefers-reduced-motion: reduce)')guard if the context requires it. - The letter
<span>elements carry no ARIA attributes. A screen reader will announce each letter individually ("H", "O", "V"…). Addaria-label="full text"on the container andaria-hidden="true"on the child spans for a natural reading experience. - The effect is only triggered by
mouseenter/mouseleave— it is inactive with keyboard navigation and on touch screens. Nofocusortouchstartequivalent is provided in the reference code. - Text contrast at rest depends entirely on the background provided by the implementer. Ensure a ≥ 4.5:1 ratio (WCAG AA) between text color and background.
- The
Resetbutton in the demo has noaria-label: the visible label "Reset" is sufficient in production, but the inlineonclickattribute should be replaced with an event listener for proper separation of concerns.
Browser compatibility
Requires CSS transitions and DOM events — natively supported in all modern browsers. No Canvas, no experimental APIs.
On mobile and touch screens, <code>mouseenter</code> never fires: letters remain in their original positions and the text stays readable. No JS errors, no broken layout.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="demo-preview">
<!-- JS rebuilds these spans via initSplitHover() -->
<span class="demo-text text-split-hover" id="splitHoverText">
<span data-index="0">H</span>
<span data-index="1">O</span>
<span data-index="2">V</span>
<span data-index="3">E</span>
<span data-index="4">R</span>
<span data-index="6">M</span>
<span data-index="7">E</span>
</span>
<!-- Reset button (demo) -->
<button class="replay-btn" onclick="replaySplitHover()">Reset</button>
</div>
Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
| JS constant randomX (line ~14) | 100 | Horizontal scatter amplitude. Each letter moves up to ±(value/2) px. Raise for an explosive burst, lower for a subtle shimmer. |
| JS constant randomY (line ~15) | 80 | Vertical scatter amplitude (±(value/2) px). Match randomX for isotropic scatter, or lower for a purely horizontal effect. |
| JS constant randomRotate (line ~16) | 90 | Rotation range (±(value/2) degrees). Set to 0 to remove rotation and keep translation only. |
| CSS transition-duration on .text-split-hover span | .4s | Duration of the elastic return. Reduce to .2s for a snappy feel, raise to .7s for a cinematic animation. |
| CSS cubic-bezier on .text-split-hover span | (.68,-.55,.265,1.55) | Elasticity curve of the return. Increase the negative values (e.g. -.8) for a stronger spring. Replace with ease-out to remove the overshoot. |
| CSS opacity on .text-split-hover.scattered span | 0.3 | Letter opacity in scattered state. 0 = full disappearance, 1 = no fade (translation only visible). |
| CSS font-size on .demo-text | 2.5rem | Text size — adapt to context (1.5 rem for a nav item, 6 rem for a full-page headline). |
FAQ
initSplitHover() rebuilds the spans from an internal constant ('HOVER ME'). Edit that string in the JS to change what is displayed — the span splitting and event binding regenerate automatically on the next call..text-split-hover span { transition: transform .4s cubic-bezier(.68, -.55, .265, 1.55) }. Replace cubic-bezier(.68, -.55, .265, 1.55) with ease-out or cubic-bezier(0,.8,.4,1) for a smooth return without overshoot.mouseenter and mouseleave do not fire on pointer-less devices. On mobile, letters remain static — the text stays readable and no error is thrown. For a touch version, add touchstart/touchend listeners that call the same scatter and reset logic.