26 CSS Link Effects08 / 26

Pure CSSMIT licensed

CSS Morphing Symbol / Tilde Hover Effect

The editorial-site signature: a small glyph before the label that becomes a different glyph on hover. A tilde that rolls up into an arrow, a plus that spins into a crossmark, and a download arrow that drops away as a checkmark pops in. Pure CSS — no icon font, no SVG sprite, just two stacked characters and a transform.

Published Updated

Live Demo
Try it

The code

<section class="cle-08" aria-label="Morphing symbol hover demo">
  <div class="cle-08__stage">
    <div class="cle-08__var">
      <a href="#a" class="cle-08__roll"><span class="cle-08__slot" aria-hidden="true"><span class="cle-08__gA">~</span><span class="cle-08__gB">→</span></span>Explore the archive</a>
      <small class="cle-08__cap">A · tilde rolls into arrow</small>
    </div>
    <div class="cle-08__var">
      <a href="#b" class="cle-08__spin"><span class="cle-08__slot" aria-hidden="true"><span class="cle-08__gC">+</span></span>Expand details</a>
      <small class="cle-08__cap">B · plus spins to cross</small>
    </div>
    <div class="cle-08__var">
      <a href="#c" class="cle-08__drop"><span class="cle-08__slot" aria-hidden="true"><span class="cle-08__gA">↓</span><span class="cle-08__gB">✓</span></span>Download kit</a>
      <small class="cle-08__cap">C · arrow drops, check lands</small>
    </div>
  </div>
</section>
.cle-08,
.cle-08 *,
.cle-08 *::before,
.cle-08 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cle-08 {
  width: 100%;
  min-height: 100vh;
  --ink: oklch(0.27 0.02 150);
  --brand: oklch(0.55 0.16 150);
  font-family: 'Segoe UI',system-ui,sans-serif;
}

.cle-08__stage {
  width: 100%;
  height: 100vh;
  display: grid;
  align-content: center;
  justify-items: start;
  gap: 38px;
  border: 1px solid oklch(0.9 0.015 150);
  background: linear-gradient(170deg,oklch(0.98 0.008 140),oklch(0.95 0.018 155));
  padding: 44px clamp(36px,12%,140px);
}

.cle-08__var {
  display: grid;
  gap: 11px;
}

.cle-08__cap {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: oklch(0.55 0.04 150);
  padding-left: calc(1.1em + 10px);
}

.cle-08 a {
  display: inline-flex;
  align-items: baseline;
  gap: 10px;
  font-size: 22px;
  font-weight: 650;
  letter-spacing: -.01em;
  color: var(--ink);
  text-decoration: none;
  transition: color .3s;
}

.cle-08 a:hover,
.cle-08 a:focus-visible {
  color: var(--brand);
}

.cle-08 a:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 5px;
  border-radius: 3px;
}

.cle-08__slot {
  display: inline-grid;
  width: 1.1em;
  justify-items: center;
  overflow: hidden;
  color: var(--brand);
}

.cle-08__slot span {
  grid-area: 1/1;
  transition: translate .35s cubic-bezier(.3,1.3,.4,1),opacity .25s,rotate .4s cubic-bezier(.3,1.3,.4,1),scale .35s cubic-bezier(.3,1.3,.4,1);
}

.cle-08__gB {
  translate: 0 70%;
  opacity: 0;
}

.cle-08__roll:hover .cle-08__gA,
.cle-08__roll:focus-visible .cle-08__gA {
  translate: 0 -70%;
  opacity: 0;
}

.cle-08__roll:hover .cle-08__gB,
.cle-08__roll:focus-visible .cle-08__gB {
  translate: 0 0;
  opacity: 1;
}

.cle-08__spin:hover .cle-08__gC,
.cle-08__spin:focus-visible .cle-08__gC {
  rotate: 135deg;
}

.cle-08__drop .cle-08__gB {
  translate: 0 0;
  scale: .3;
  opacity: 0;
}

.cle-08__drop:hover .cle-08__gA,
.cle-08__drop:focus-visible .cle-08__gA {
  translate: 0 90%;
  opacity: 0;
}

.cle-08__drop:hover .cle-08__gB,
.cle-08__drop:focus-visible .cle-08__gB {
  scale: 1;
  opacity: 1;
  transition-delay: 90ms;
}

@media (prefers-reduced-motion: reduce) {
  .cle-08 * {
    transition-duration: .01s !important;
    transition-delay: 0s !important;
  }
}
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 Link 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: CSS Morphing Symbol / Tilde Hover Effect
Source: https://codefronts.com/motion/css-link-effects/tilde-morph/

The editorial-site signature: a small glyph before the label that becomes a different glyph on hover. A tilde that rolls up into an arrow, a plus that spins into a crossmark, and a download arrow that drops away as a checkmark pops in. Pure CSS — no icon font, no SVG sprite, just two stacked characters and a transform.
## HTML
```html
<section class="cle-08" aria-label="Morphing symbol hover demo">
  <div class="cle-08__stage">
    <div class="cle-08__var">
      <a href="#a" class="cle-08__roll"><span class="cle-08__slot" aria-hidden="true"><span class="cle-08__gA">~</span><span class="cle-08__gB">→</span></span>Explore the archive</a>
      <small class="cle-08__cap">A · tilde rolls into arrow</small>
    </div>
    <div class="cle-08__var">
      <a href="#b" class="cle-08__spin"><span class="cle-08__slot" aria-hidden="true"><span class="cle-08__gC">+</span></span>Expand details</a>
      <small class="cle-08__cap">B · plus spins to cross</small>
    </div>
    <div class="cle-08__var">
      <a href="#c" class="cle-08__drop"><span class="cle-08__slot" aria-hidden="true"><span class="cle-08__gA">↓</span><span class="cle-08__gB">✓</span></span>Download kit</a>
      <small class="cle-08__cap">C · arrow drops, check lands</small>
    </div>
  </div>
</section>
```
## CSS
```css
.cle-08,
.cle-08 *,
.cle-08 *::before,
.cle-08 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cle-08 {
  width: 100%;
  min-height: 100vh;
  --ink: oklch(0.27 0.02 150);
  --brand: oklch(0.55 0.16 150);
  font-family: 'Segoe UI',system-ui,sans-serif;
}

.cle-08__stage {
  width: 100%;
  height: 100vh;
  display: grid;
  align-content: center;
  justify-items: start;
  gap: 38px;
  border: 1px solid oklch(0.9 0.015 150);
  background: linear-gradient(170deg,oklch(0.98 0.008 140),oklch(0.95 0.018 155));
  padding: 44px clamp(36px,12%,140px);
}

.cle-08__var {
  display: grid;
  gap: 11px;
}

.cle-08__cap {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: oklch(0.55 0.04 150);
  padding-left: calc(1.1em + 10px);
}

.cle-08 a {
  display: inline-flex;
  align-items: baseline;
  gap: 10px;
  font-size: 22px;
  font-weight: 650;
  letter-spacing: -.01em;
  color: var(--ink);
  text-decoration: none;
  transition: color .3s;
}

.cle-08 a:hover,
.cle-08 a:focus-visible {
  color: var(--brand);
}

.cle-08 a:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 5px;
  border-radius: 3px;
}

.cle-08__slot {
  display: inline-grid;
  width: 1.1em;
  justify-items: center;
  overflow: hidden;
  color: var(--brand);
}

.cle-08__slot span {
  grid-area: 1/1;
  transition: translate .35s cubic-bezier(.3,1.3,.4,1),opacity .25s,rotate .4s cubic-bezier(.3,1.3,.4,1),scale .35s cubic-bezier(.3,1.3,.4,1);
}

.cle-08__gB {
  translate: 0 70%;
  opacity: 0;
}

.cle-08__roll:hover .cle-08__gA,
.cle-08__roll:focus-visible .cle-08__gA {
  translate: 0 -70%;
  opacity: 0;
}

.cle-08__roll:hover .cle-08__gB,
.cle-08__roll:focus-visible .cle-08__gB {
  translate: 0 0;
  opacity: 1;
}

.cle-08__spin:hover .cle-08__gC,
.cle-08__spin:focus-visible .cle-08__gC {
  rotate: 135deg;
}

.cle-08__drop .cle-08__gB {
  translate: 0 0;
  scale: .3;
  opacity: 0;
}

.cle-08__drop:hover .cle-08__gA,
.cle-08__drop:focus-visible .cle-08__gA {
  translate: 0 90%;
  opacity: 0;
}

.cle-08__drop:hover .cle-08__gB,
.cle-08__drop:focus-visible .cle-08__gB {
  scale: 1;
  opacity: 1;
  transition-delay: 90ms;
}

@media (prefers-reduced-motion: reduce) {
  .cle-08 * {
    transition-duration: .01s !important;
    transition-delay: 0s !important;
  }
}
```

How this works

CSS can't literally tween one character into another (content swaps are discrete), so the morph is choreography: both glyphs live in a fixed 1.1em slot, stacked on the same grid cell, and hover moves one out while the other arrives:

.slot{ display:inline-grid; width:1.1em; overflow:hidden; }
.slot span{ grid-area:1/1; transition: translate .35s …, opacity .25s, rotate .35s; }
.gB{ translate: 0 70%; opacity: 0; }        /* waiting below */
a:hover .gA{ translate: 0 -70%; opacity: 0; } /* rolls up and out */
a:hover .gB{ translate: 0 0;  opacity: 1; }   /* takes its place */

Variant B is the one true transform morph: + rotated 135° is a × (with a bonus half-spin for drama) — one span, one transition. C plays the same stack vertically with a spring curve, timed so the checkmark lands just as the arrow clears. Glyph spans are aria-hidden; the accessible name is the label text alone.

Make it yours

  • Any glyph pair works in the roller: ~, §#, ?! — mind the slot width for wide glyphs.
  • Roll direction: swap the 70%/-70% translate signs for a downward roll.
  • B's spin amount: 135deg reads as ×; 405deg adds a full extra revolution.
  • Slot sizing is em-based, so it scales with the link's font-size automatically.

Gotchas — read before shipping

  • Mark decorative glyph spans aria-hidden="true" — otherwise screen readers announce 'tilde' before every link.
  • Fix the slot's width: glyphs have different advance widths, and an unsized slot makes the label shift sideways mid-morph.
  • Pick glyphs from the font you already load; exotic Unicode (✓ is safe, ⇢ often isn't) can fall back to a mismatched face.

Browser support

ChromeSafariFirefoxEdge
111+16.2+113+111+

inline-grid stacking, individual translate/rotate properties (Chrome 104, Safari 14.1, Firefox 72) and transitions — all Baseline; oklch sets the stated floor.

Search CodeFronts

Loading…