26 CSS Link Effects26 / 26

Pure CSSMIT licensed

CSS Typewriter / Type-On Link Hover Effect

Typing without JavaScript: steps() easing plus ch units — one step per character, one ch per character, monospace makes them equal. Hover a link and it types an extra clause after itself, caret blinking at the live edge; or watch it backspace 'Projects' and retype 'Selected work'; or decrypt a proportional-font phrase in chunky clip-path blocks. The math is the tutorial.

Published

Live Demo
Try it

The code

<section class="cle-26" aria-label="Typewriter hover demo">
  <div class="cle-26__stage">
    <div class="cle-26__var"><a href="#a" class="cle-26__type">Contact<span class="cle-26__added" aria-hidden="true"> — say hi</span></a><small class="cle-26__cap">A · types a suffix</small></div>
    <div class="cle-26__var"><a href="#b" class="cle-26__retype"><span class="cle-26__old">Projects</span><span class="cle-26__new" aria-hidden="true">Selected work</span><span class="cle-26__edge" aria-hidden="true"></span></a><small class="cle-26__cap">B · backspace, retype</small></div>
    <div class="cle-26__var"><a href="#c" class="cle-26__chunk" data-text="Decrypt the archive">Decrypt the archive</a><small class="cle-26__cap">C · chunked reveal (any font)</small></div>
  </div>
</section>
.cle-26,
.cle-26 *,
.cle-26 *::before,
.cle-26 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cle-26 {
  width: 100%;
  min-height: 100vh;
  --ink: oklch(0.3 0.02 250);
  --accent: oklch(0.52 0.17 250);
  font-family: ui-monospace,'Cascadia Code',Menlo,Consolas,monospace;
}

.cle-26__stage {
  width: 100%;
  height: 100vh;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: 46px;
  border: 1px solid oklch(0.9 0.012 250);
  background: linear-gradient(175deg,oklch(0.985 0.005 250),oklch(0.95 0.012 250));
  padding: 44px 32px;
}

.cle-26__var {
  display: grid;
  gap: 13px;
  justify-items: center;
}

.cle-26__cap {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: oklch(0.55 0.03 250);
  font-family: 'Segoe UI',system-ui,sans-serif;
}

.cle-26 a {
  color: var(--ink);
  text-decoration: none;
  font-size: clamp(20px,2.8vw,26px);
  font-weight: 600;
  transition: color .3s;
}

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

.cle-26 a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 6px;
  border-radius: 3px;
}

.cle-26__added {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  white-space: pre;
  width: 0;
  color: var(--accent);
  border-right: 2px solid transparent;
}

.cle-26__type:hover .cle-26__added,
.cle-26__type:focus-visible .cle-26__added {
  animation: clh26-typeA .7s steps(9) forwards,clh26-caret 1s steps(1) infinite;
}

@keyframes clh26-typeA {
  to {
    width: 9ch;
  }
}

@keyframes clh26-caret {
  0%,
    49% {
    border-right-color: var(--accent);
  }

  50%,
    100% {
    border-right-color: transparent;
  }
}

.cle-26__retype .cle-26__old,
.cle-26__retype .cle-26__new {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  white-space: pre;
}

.cle-26__retype .cle-26__old {
  width: 8ch;
}

.cle-26__retype .cle-26__new {
  width: 0;
  color: var(--accent);
}

.cle-26__retype .cle-26__edge {
  display: inline-block;
  vertical-align: bottom;
  width: 2px;
  height: 1.15em;
  background: transparent;
}

.cle-26__retype:hover .cle-26__old,
.cle-26__retype:focus-visible .cle-26__old {
  animation: clh26-del .35s steps(8) forwards;
}

.cle-26__retype:hover .cle-26__new,
.cle-26__retype:focus-visible .cle-26__new {
  animation: clh26-typeB .5s steps(13) .45s forwards;
}

.cle-26__retype:hover .cle-26__edge,
.cle-26__retype:focus-visible .cle-26__edge {
  animation: clh26-edge 1s steps(1) infinite;
}

@keyframes clh26-del {
  to {
    width: 0;
  }
}

@keyframes clh26-typeB {
  to {
    width: 13ch;
  }
}

@keyframes clh26-edge {
  0%,
    49% {
    background: var(--accent);
  }

  50%,
    100% {
    background: transparent;
  }
}

.cle-26__chunk {
  position: relative;
  font-family: 'Segoe UI',system-ui,sans-serif;
  font-weight: 800;
  letter-spacing: -.01em;
}

.cle-26__chunk::after {
  content: attr(data-text);
  position: absolute;
  inset: 0;
  color: var(--accent);
  clip-path: inset(0 100% 0 0);
  transition: clip-path .6s steps(12,end);
}

.cle-26__chunk:hover::after,
.cle-26__chunk:focus-visible::after {
  clip-path: inset(0 0 0 0);
}

@media (prefers-reduced-motion: reduce) {
  .cle-26 * {
    animation: none !important;
    transition-duration: .01s !important;
  }

  .cle-26__type:hover .cle-26__added {
    width: 9ch;
  }

  .cle-26__retype:hover .cle-26__old {
    width: 0;
  }

  .cle-26__retype:hover .cle-26__new {
    width: 13ch;
  }
}
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 Typewriter / Type-On Link Hover Effect
Source: https://codefronts.com/motion/css-link-effects/type-on-reveal/

Typing without JavaScript: steps() easing plus ch units — one step per character, one ch per character, monospace makes them equal. Hover a link and it types an extra clause after itself, caret blinking at the live edge; or watch it backspace 'Projects' and retype 'Selected work'; or decrypt a proportional-font phrase in chunky clip-path blocks. The math is the tutorial.
## HTML
```html
<section class="cle-26" aria-label="Typewriter hover demo">
  <div class="cle-26__stage">
    <div class="cle-26__var"><a href="#a" class="cle-26__type">Contact<span class="cle-26__added" aria-hidden="true"> — say hi</span></a><small class="cle-26__cap">A · types a suffix</small></div>
    <div class="cle-26__var"><a href="#b" class="cle-26__retype"><span class="cle-26__old">Projects</span><span class="cle-26__new" aria-hidden="true">Selected work</span><span class="cle-26__edge" aria-hidden="true"></span></a><small class="cle-26__cap">B · backspace, retype</small></div>
    <div class="cle-26__var"><a href="#c" class="cle-26__chunk" data-text="Decrypt the archive">Decrypt the archive</a><small class="cle-26__cap">C · chunked reveal (any font)</small></div>
  </div>
</section>
```
## CSS
```css
.cle-26,
.cle-26 *,
.cle-26 *::before,
.cle-26 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cle-26 {
  width: 100%;
  min-height: 100vh;
  --ink: oklch(0.3 0.02 250);
  --accent: oklch(0.52 0.17 250);
  font-family: ui-monospace,'Cascadia Code',Menlo,Consolas,monospace;
}

.cle-26__stage {
  width: 100%;
  height: 100vh;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: 46px;
  border: 1px solid oklch(0.9 0.012 250);
  background: linear-gradient(175deg,oklch(0.985 0.005 250),oklch(0.95 0.012 250));
  padding: 44px 32px;
}

.cle-26__var {
  display: grid;
  gap: 13px;
  justify-items: center;
}

.cle-26__cap {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: oklch(0.55 0.03 250);
  font-family: 'Segoe UI',system-ui,sans-serif;
}

.cle-26 a {
  color: var(--ink);
  text-decoration: none;
  font-size: clamp(20px,2.8vw,26px);
  font-weight: 600;
  transition: color .3s;
}

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

.cle-26 a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 6px;
  border-radius: 3px;
}

.cle-26__added {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  white-space: pre;
  width: 0;
  color: var(--accent);
  border-right: 2px solid transparent;
}

.cle-26__type:hover .cle-26__added,
.cle-26__type:focus-visible .cle-26__added {
  animation: clh26-typeA .7s steps(9) forwards,clh26-caret 1s steps(1) infinite;
}

@keyframes clh26-typeA {
  to {
    width: 9ch;
  }
}

@keyframes clh26-caret {
  0%,
    49% {
    border-right-color: var(--accent);
  }

  50%,
    100% {
    border-right-color: transparent;
  }
}

.cle-26__retype .cle-26__old,
.cle-26__retype .cle-26__new {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  white-space: pre;
}

.cle-26__retype .cle-26__old {
  width: 8ch;
}

.cle-26__retype .cle-26__new {
  width: 0;
  color: var(--accent);
}

.cle-26__retype .cle-26__edge {
  display: inline-block;
  vertical-align: bottom;
  width: 2px;
  height: 1.15em;
  background: transparent;
}

.cle-26__retype:hover .cle-26__old,
.cle-26__retype:focus-visible .cle-26__old {
  animation: clh26-del .35s steps(8) forwards;
}

.cle-26__retype:hover .cle-26__new,
.cle-26__retype:focus-visible .cle-26__new {
  animation: clh26-typeB .5s steps(13) .45s forwards;
}

.cle-26__retype:hover .cle-26__edge,
.cle-26__retype:focus-visible .cle-26__edge {
  animation: clh26-edge 1s steps(1) infinite;
}

@keyframes clh26-del {
  to {
    width: 0;
  }
}

@keyframes clh26-typeB {
  to {
    width: 13ch;
  }
}

@keyframes clh26-edge {
  0%,
    49% {
    background: var(--accent);
  }

  50%,
    100% {
    background: transparent;
  }
}

.cle-26__chunk {
  position: relative;
  font-family: 'Segoe UI',system-ui,sans-serif;
  font-weight: 800;
  letter-spacing: -.01em;
}

.cle-26__chunk::after {
  content: attr(data-text);
  position: absolute;
  inset: 0;
  color: var(--accent);
  clip-path: inset(0 100% 0 0);
  transition: clip-path .6s steps(12,end);
}

.cle-26__chunk:hover::after,
.cle-26__chunk:focus-visible::after {
  clip-path: inset(0 0 0 0);
}

@media (prefers-reduced-motion: reduce) {
  .cle-26 * {
    animation: none !important;
    transition-duration: .01s !important;
  }

  .cle-26__type:hover .cle-26__added {
    width: 9ch;
  }

  .cle-26__retype:hover .cle-26__old {
    width: 0;
  }

  .cle-26__retype:hover .cle-26__new {
    width: 13ch;
  }
}
```

How this works

In a monospace font every glyph is exactly 1ch wide, so revealing an N-character string is an N-step width animation:

.added{ display:inline-block; overflow:hidden;
  white-space:pre; width:0; border-right:2px solid transparent; }
a:hover .added{
  animation: clh26-type .7s steps(9) forwards,   /* ' — say hi' = 9 chars */
             clh26-caret 1s steps(1) infinite;    /* blinking edge */
}
@keyframes clh26-type{ to{ width: 9ch; } }

steps(9) divides the width tween into 9 discrete jumps — one per character — and the border-right caret rides the shrinking clip edge, blinking on its own square-wave animation.

The retype (B) sequences two of these: the visible word animates width → 0 (backspacing, 8 steps), then the hidden replacement animates open after a matching animation-delay — 13 steps for 13 characters. A shared caret pseudo-element sits after both spans in the inline flow, so it's always at the live edge. C handles proportional fonts, where ch math fails: a recolored duplicate is revealed by clip-path: inset() transitioning under steps(12, end) — not per-character, but the chunky data-feed rhythm suits display type.

Make it yours

  • Typed suffix: change the span text AND the step count + ch width to its new length — the three must agree.
  • Typing speed ≈ duration ÷ steps: .7s/9 ≈ 78ms per character; humans type ~60–200ms.
  • B's pause between delete and retype: the gap between animation 1's duration and animation 2's delay.
  • Caret style: 2px solid is a beam; use border-right:.55ch solid for a block cursor.

Gotchas — read before shipping

  • The count contract is fragile: copy changes silently break the effect if steps/ch aren't updated — leave a comment next to the string.
  • white-space:pre is required on the typed span: leading spaces are real characters in the count.
  • Reduced-motion users skip the theatre: the demo reveals the full text instantly (width override, no animation) — never hide content behind an animation they won't get.

Browser support

ChromeSafariFirefoxEdge
111+16.2+113+111+

steps(), ch units and clip-path are Baseline everywhere; oklch sets the stated floor.

Search CodeFronts

Loading…