webdevpuneet.com

Character Wobble Hover Text — Per-Letter Animation Snippet

Character Wobble Hover Text · Animations · Plain HTML, CSS & JS · Live preview

CategoryAnimations

Copy & paste

Source Code

<div class="cw-stage">
  <p class="cw-hint">Hover the heading — each letter wobbles independently as your cursor passes over it</p>
  <h1 class="cw-text" id="cwText">Wobble on hover</h1>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Each character is an independent span with its own hover listener and settle timer
Randomized rotation, lift, and scale per wobble — never the same motion twice
Independent settle timers stored per-span (span._settleTimer) so letters never block each other
Randomized color pulse using HSL hue variation on every wobble
Overshoot cubic-bezier transition gives a natural spring-back rather than a flat ease
Text is re-split from live textContent, so any heading text works automatically
Spaces preserved via non-breaking space spans so word gaps do not collapse
No @keyframes — all motion is transition-driven from JS-set target values
Export as HTML file, React JSX, or React + Tailwind CSS
Mobile (375px), Tablet (768px), Desktop device preview buttons
Live split-pane editor — preview updates as you type

About this UI Snippet

Character Wobble Hover Text — Independent Per-Letter Spring-Back Rotation on Hover

Screenshot of the Character Wobble Hover Text snippet rendered live

This effect gives each letter of a heading its own physical reaction to the cursor: as the pointer sweeps across the text, individual characters lift, rotate at a slightly random angle, and briefly tint a new color before springing back — not a text-scramble effect that swaps characters, and not one shared @keyframes animation applied identically to the whole string. Every letter's wobble is independent, so several letters can be mid-motion at once while the rest sit still.

Splitting text into per-character spans

The script reads heading.textContent, clears the element, then rebuilds it by wrapping every character (including spaces, which get a non-breaking space and a fixed width class so word gaps don't collapse) in its own <span class="cw-char">. This is the same "split into spans" foundation used by scramble and stagger-reveal text effects, but here each span keeps its own hover listener instead of being driven by a single shared timeline.

Randomizing the wobble per letter

The wobble(span) function picks fresh random values every time it runs: rot between -16° and 16°, rise (an upward translateY) between 6px and 14px, and scale between 1.08 and 1.22. Because Math.random() runs independently for every character on every hover, no two letters — and no two hovers of the same letter — wobble identically, which is what keeps the effect feeling organic rather than mechanically repeated.

The color pulse

Alongside the transform, each wobbling letter briefly switches to hsl(hue, 90%, 72%) with a randomized hue in the 200-280 range (blues through purples), then reverts to the heading's normal color. This is a cheap way to add visual energy without needing a gradient text mask or an extra layer.

Independent settle timers

Each span tracks its own _settleTimer directly on the DOM node. wobble() clears any existing timer for that specific span before setting a new one — so re-hovering the same letter quickly restarts its own wobble-and-settle cycle without interfering with any other letter's timer. After 260ms, that letter's transform and color reset to neutral, independent of what every other letter is doing.

Why CSS transition instead of @keyframes

Rather than authoring a fixed keyframe sequence, the JS simply sets a target transform and lets the CSS transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) interpolate to it — and back again when the timer resets it. That cubic-bezier includes a controlled overshoot (values above 1), giving the spring-back a small bounce rather than a flat ease.

Step by step

How to Use

  1. 1
    Hover across the headingMove your cursor slowly across the letters — each one lifts, rotates, and tints color independently as the pointer passes over it.
  2. 2
    Hover quickly back and forthSweep back and forth over the same letters and notice each wobble is freshly randomized, never identical to the last one.
  3. 3
    Change the heading textEdit the text inside the h1 in the HTML panel — the JS automatically re-splits whatever text is present into individual character spans.
  4. 4
    Adjust the wobble rangeIn the JS panel, change the random ranges for rot, rise, and scale inside the wobble() function to make the motion subtler or wilder.
  5. 5
    Change the settle timingAdjust the 260ms setTimeout duration and the 0.5s CSS transition to make letters spring back faster or slower.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a React + Tailwind version.

Real-world uses

Common Use Cases

Playful hero headings
A hero headline with per-letter wobble on hover gives a brand with a fun, energetic voice a distinctive, memorable first impression.
CARD
404 and empty-state pages
A wobbling headline lightens the mood on an error or empty-state page without needing an illustration.
Learn per-element random animation timing
Study how independent settle timers stored directly on DOM nodes let many elements animate on their own schedules without a shared coordinator.
Kids, games, and entertainment brand sites
Character-level interactivity fits products aimed at a younger or more playful audience far better than a static heading.
Logo wordmark micro-interaction
Apply the same split-into-spans technique to a text-based logo for a small delight on hover in the site header.
Related: Text Scramble
For a different letter-level hover effect, see the Text Scramble snippet, which swaps characters rather than physically wobbling them.

Got questions?

Frequently Asked Questions

A scramble effect swaps each character through a sequence of random glyphs before landing on the real letter. This effect never changes the letters themselves — it physically rotates, lifts, and briefly recolors each real character, then springs it back, using CSS transforms rather than character substitution.

wobble(span) calls Math.random() fresh every time it runs, generating a new rotation, lift, and scale for that specific hover of that specific letter. There is no shared or looping animation — each wobble is a one-off random target that the CSS transition eases toward.

Each letter needs to settle back to neutral independently of every other letter and independently of how many times it has been re-hovered. Storing a per-span timer (span._settleTimer) and clearing it on each new hover lets rapid re-hovers restart that one letter's cycle cleanly without a shared animation timeline getting confused about which letter is doing what.

The split logic works on any string returned by textContent, splitting by JavaScript string character. For most Latin-script text this is fine; multi-codepoint characters (some emoji, certain combining accents) may split incorrectly, so keep to plain single-codepoint text for reliable results.

Wrapping each character in its own span for a heading can affect how screen readers announce the text (letter-by-letter in some cases). Consider adding aria-label with the full original string on the parent heading and aria-hidden="true" on the character spans to keep the accessible name intact.

Yes. Click "JSX" for a React component. Split the string into an array once (e.g. with useMemo), render one span per character with a key, and manage each span's wobble state via a ref-based approach or a small per-index state object rather than direct DOM writes, to stay idiomatic React.