30 CSS Text Hover Effects23 / 30

CSS onlyMIT licensed

Smoke / Dissolve Particle Effect

Hold the cursor on the title and it exhales: each letter blurs, lightens, drifts up along its own slightly different heading, and thins to nothing — smoke leaving a cooling fire. Pull away mid-dissolve and it re-condenses through the exact same path, because everything is transitions (never keyframes): per-letter drift vectors and rotations live in inline custom properties, delays stagger 35ms per letter, and filter:blur() is the actual smoke. The full word stays in the accessibility tree via aria-label while the letter spans do the vanishing.

Published

Live Demo
Try it

The code

<section class="cth-23" aria-label="Smoke dissolve text demo">
  <div class="cth-23__stage">
    <div class="cth-23__scene">
      <p class="cth-23__eyebrow">A film by Ines Marlowe</p>
      <h1 class="cth-23__title" tabindex="0" aria-label="AFTERGLOW">
        <span aria-hidden="true"><i style="--i:0;--dx:-.1em;--dr:-8deg">A</i><i style="--i:1;--dx:.06em;--dr:5deg">F</i><i style="--i:2;--dx:-.04em;--dr:-4deg">T</i><i style="--i:3;--dx:.14em;--dr:9deg">E</i><i style="--i:4;--dx:-.12em;--dr:-6deg">R</i><i style="--i:5;--dx:.05em;--dr:7deg">G</i><i style="--i:6;--dx:-.07em;--dr:-9deg">L</i><i style="--i:7;--dx:.11em;--dr:4deg">O</i><i style="--i:8;--dx:-.05em;--dr:-5deg">W</i></span>
      </h1>
      <p class="cth-23__sub">In cinemas October &mdash; the story of everything we let cool.</p>
      <p class="cth-23__hint">Rest the cursor on the title and hold; pull away mid-dissolve and it re-condenses along the same path. Transitions, never keyframes &mdash; that's the whole trick.</p>
    </div>
  </div>
</section>
.cth-23,
.cth-23 *,
.cth-23 *::before,
.cth-23 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cth-23 {
  --bg: oklch(0.15 0.008 270);
  --ink: oklch(0.9 0.01 80);
  --mut: oklch(0.56 0.015 275);
  font-family: Georgia,'Times New Roman',serif;
  color: var(--ink);
}

.cth-23__stage {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  background: radial-gradient(100% 90% at 50% 100%,oklch(0.2 0.02 50/.55),transparent 55%),radial-gradient(90% 70% at 50% 20%,oklch(0.18 0.01 275),var(--bg) 75%);
  padding: clamp(20px,4vw,56px);
  container: cth23/inline-size;
}

.cth-23__scene {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: clamp(16px,3cqi,26px);
}

.cth-23__eyebrow {
  font-size: 12px;
  letter-spacing: .34em;
  text-transform: uppercase;
  color: var(--mut);
  font-family: 'Segoe UI',system-ui,sans-serif;
  font-weight: 600;
}

.cth-23__title {
  padding: clamp(24px,5cqi,48px) clamp(16px,3cqi,32px);
  letter-spacing: .16em;
  border-radius: 14px;
}

.cth-23__title:focus-visible {
  outline: 1.5px solid var(--ink);
  outline-offset: 4px;
}

.cth-23__title i {
  display: inline-block;
  font-style: normal;
  font-size: clamp(44px,10cqi,104px);
  font-weight: 500;
  line-height: 1;
  translate: 0 0;
  rotate: 0deg;
  opacity: 1;
  filter: blur(0);
  transition: translate 1s cubic-bezier(.3,.6,.2,1),rotate 1s ease,opacity 1s ease,filter 1s ease,color 1s ease;
  transition-delay: calc(var(--i)*35ms);
}

.cth-23__title:hover i,
.cth-23__title:focus-visible i {
  translate: var(--dx) -1.25em;
  rotate: var(--dr);
  opacity: 0;
  filter: blur(.14em);
  color: oklch(0.98 0.005 80);
}

.cth-23__sub {
  font-size: 14.5px;
  font-style: italic;
  color: var(--mut);
  letter-spacing: .02em;
}

.cth-23__hint {
  font-size: 12.5px;
  line-height: 1.7;
  color: var(--mut);
  max-width: 56ch;
  font-family: 'Segoe UI',system-ui,sans-serif;
}

@media (prefers-reduced-motion: reduce) {
  .cth-23__title i {
    transition: opacity .4s ease !important;
    transition-delay: 0s !important;
  }

  .cth-23__title:hover i,
    .cth-23__title:focus-visible i {
    translate: 0 0;
    rotate: 0deg;
    filter: blur(0);
    opacity: .45;
  }
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Text Hover Effect from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Smoke / Dissolve Particle Effect
Source: https://codefronts.com/motion/css-text-hover-effects/smoke-dissolve-particle-effect/

Hold the cursor on the title and it exhales: each letter blurs, lightens, drifts up along its own slightly different heading, and thins to nothing — smoke leaving a cooling fire. Pull away mid-dissolve and it re-condenses through the exact same path, because everything is transitions (never keyframes): per-letter drift vectors and rotations live in inline custom properties, delays stagger 35ms per letter, and filter:blur() is the actual smoke. The full word stays in the accessibility tree via aria-label while the letter spans do the vanishing.
## HTML
```html
<section class="cth-23" aria-label="Smoke dissolve text demo">
  <div class="cth-23__stage">
    <div class="cth-23__scene">
      <p class="cth-23__eyebrow">A film by Ines Marlowe</p>
      <h1 class="cth-23__title" tabindex="0" aria-label="AFTERGLOW">
        <span aria-hidden="true"><i style="--i:0;--dx:-.1em;--dr:-8deg">A</i><i style="--i:1;--dx:.06em;--dr:5deg">F</i><i style="--i:2;--dx:-.04em;--dr:-4deg">T</i><i style="--i:3;--dx:.14em;--dr:9deg">E</i><i style="--i:4;--dx:-.12em;--dr:-6deg">R</i><i style="--i:5;--dx:.05em;--dr:7deg">G</i><i style="--i:6;--dx:-.07em;--dr:-9deg">L</i><i style="--i:7;--dx:.11em;--dr:4deg">O</i><i style="--i:8;--dx:-.05em;--dr:-5deg">W</i></span>
      </h1>
      <p class="cth-23__sub">In cinemas October &mdash; the story of everything we let cool.</p>
      <p class="cth-23__hint">Rest the cursor on the title and hold; pull away mid-dissolve and it re-condenses along the same path. Transitions, never keyframes &mdash; that's the whole trick.</p>
    </div>
  </div>
</section>
```
## CSS
```css
.cth-23,
.cth-23 *,
.cth-23 *::before,
.cth-23 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cth-23 {
  --bg: oklch(0.15 0.008 270);
  --ink: oklch(0.9 0.01 80);
  --mut: oklch(0.56 0.015 275);
  font-family: Georgia,'Times New Roman',serif;
  color: var(--ink);
}

.cth-23__stage {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  background: radial-gradient(100% 90% at 50% 100%,oklch(0.2 0.02 50/.55),transparent 55%),radial-gradient(90% 70% at 50% 20%,oklch(0.18 0.01 275),var(--bg) 75%);
  padding: clamp(20px,4vw,56px);
  container: cth23/inline-size;
}

.cth-23__scene {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: clamp(16px,3cqi,26px);
}

.cth-23__eyebrow {
  font-size: 12px;
  letter-spacing: .34em;
  text-transform: uppercase;
  color: var(--mut);
  font-family: 'Segoe UI',system-ui,sans-serif;
  font-weight: 600;
}

.cth-23__title {
  padding: clamp(24px,5cqi,48px) clamp(16px,3cqi,32px);
  letter-spacing: .16em;
  border-radius: 14px;
}

.cth-23__title:focus-visible {
  outline: 1.5px solid var(--ink);
  outline-offset: 4px;
}

.cth-23__title i {
  display: inline-block;
  font-style: normal;
  font-size: clamp(44px,10cqi,104px);
  font-weight: 500;
  line-height: 1;
  translate: 0 0;
  rotate: 0deg;
  opacity: 1;
  filter: blur(0);
  transition: translate 1s cubic-bezier(.3,.6,.2,1),rotate 1s ease,opacity 1s ease,filter 1s ease,color 1s ease;
  transition-delay: calc(var(--i)*35ms);
}

.cth-23__title:hover i,
.cth-23__title:focus-visible i {
  translate: var(--dx) -1.25em;
  rotate: var(--dr);
  opacity: 0;
  filter: blur(.14em);
  color: oklch(0.98 0.005 80);
}

.cth-23__sub {
  font-size: 14.5px;
  font-style: italic;
  color: var(--mut);
  letter-spacing: .02em;
}

.cth-23__hint {
  font-size: 12.5px;
  line-height: 1.7;
  color: var(--mut);
  max-width: 56ch;
  font-family: 'Segoe UI',system-ui,sans-serif;
}

@media (prefers-reduced-motion: reduce) {
  .cth-23__title i {
    transition: opacity .4s ease !important;
    transition-delay: 0s !important;
  }

  .cth-23__title:hover i,
    .cth-23__title:focus-visible i {
    translate: 0 0;
    rotate: 0deg;
    filter: blur(0);
    opacity: .45;
  }
}
```

How this works

One transition set, four channels, per-letter randomness injected as data:

<i style='--i:3; --dx:.14em; --dr:9deg'>E</i>

.title i{ display:inline-block;
  translate:0 0; rotate:0deg; opacity:1; filter:blur(0);
  transition:translate 1s cubic-bezier(.3,.6,.2,1),
             rotate 1s ease, opacity 1s ease, filter 1s ease;
  transition-delay:calc(var(--i) * 35ms); }

.title:hover i,
.title:focus-visible i{
  translate:var(--dx) -1.25em;   /* each letter's own drift */
  rotate:var(--dr);
  opacity:0;
  filter:blur(.14em); }

Real smoke never rises straight: the --dx values alternate small positive/negative drifts and --dr tips each letter a few degrees clockwise or counter — hand-authored 'randomness' that costs nothing at runtime and renders identically every visit (deterministic beats Math.random() for brand motion). Blur is what makes it smoke rather than confetti: edges soften as the glyph thins, exactly how vapor loses definition. The em-based drift distances scale with the font automatically.

The reversibility is the emotional payoff and it's free: transitions run backward when the hover ends, same delays, same curve — leave halfway and the word inhales back together. This is the demo to study for the transitions-vs-keyframes decision: demo 19 chose keyframes because a bounce is a script; smoke chooses transitions because dissolving is a STATE you can enter and leave. Under reduced motion the letters simply fade a little — the metaphor survives without the motion.

Make it yours

  • Dissolve distance: -1.25em rises like a candle; -0.4em with double blur reads as fog on glass; positive Y values SINK the word — dissolve-down reads as drowning, striking for horror one-sheets.
  • Tempo: 1s here. 2.5s with 60ms delays is a long cinematic exhale for landing-page titles; 400ms is a UI-grade puff for deleting items from a list.
  • Direction sets meaning: all --dx positive = wind from the left; alternating = still air. Author the weather.
  • Trigger it by class instead of :hover (.is-leaving) and the same CSS becomes an exit animation for route changes — the letters genuinely leave the page.

Gotchas — read before shipping

  • Transitions, not keyframes, are load-bearing: keyframes would restart from solid on every hover and snap on exit. If someone 'optimizes' this into an animation, the re-condense dies.
  • blur() past ~.2em at display sizes costs real paint on 4K screens × letter count — this is a hero-title effect, not a nav treatment. Budget by (letters × blur radius × font size).
  • The hover target must stay stable while letters fly: the drift happens INSIDE the padded title box, so the hitbox doesn't move — pad generously or the effect self-cancels at the edges (same jitter class as demo 14's magnet bug).
  • Per-letter spans kill kerning and ligatures — acceptable for spaced display caps like this; wrong for a script face where connections would visibly break at rest.

Browser support

ChromeSafariFirefoxEdge
111+16.4+113+111+

Individual transform properties (translate/rotate) are Chrome 104+/Safari 14.1+/Firefox 72+; everything else is ancient. Floor reflects oklch() tokens.

Techniques used in this demo

Search CodeFronts

Loading…