30 CSS Text Animations16 / 30

CSS + JSMIT licensed

CSS Variable Font Weight Animation on Hover

Variable-font choreography with Recursive: a headline whose letters swell toward the cursor like type under a magnet (proximity-mapped 'wght'), and a zero-JS menu where each row transitions weight, slant and the CASL casual axis on hover.

Published Updated

Live Demo
Try it

The code

<div class="cat-16-group">
<link href="https://fonts.googleapis.com/css2?family=Recursive:slnt,wght,CASL@-15..0,300..1000,0..1&display=swap" rel="stylesheet">
<section class="cat-16a" aria-label="Variable font weight follows the cursor">
  <div class="cat-16a__stage" id="cat-16a">
    <h1 class="cat-16a__word" aria-label="come closer">
      <span aria-hidden="true"><i>c</i><i>o</i><i>m</i><i>e</i><i>&nbsp;</i><i>c</i><i>l</i><i>o</i><i>s</i><i>e</i><i>r</i></span>
    </h1>
    <p class="cat-16a__sub" aria-hidden="true">'wght' 320 → 900 mapped to pointer distance</p>
  </div>
</section>
<link href="https://fonts.googleapis.com/css2?family=Recursive:slnt,wght,CASL@-15..0,300..1000,0..1&display=swap" rel="stylesheet">
<section class="cat-16b" aria-label="Variable font menu hover">
  <nav class="cat-16b__menu" aria-label="Cafe menu">
    <p class="cat-16b__label" aria-hidden="true">— the counter —</p>
    <a class="cat-16b__item" href="#">Espresso<em>2.80</em></a>
    <a class="cat-16b__item" href="#">Cortado<em>3.60</em></a>
    <a class="cat-16b__item" href="#">Filter<em>3.20</em></a>
    <a class="cat-16b__item" href="#">Cold brew<em>4.40</em></a>
  </nav>
</section>
</div>
.cat-16-group {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  width: 100%;
}

.cat-16-group > section {
  flex: 1 1 480px;
  min-width: min(100%,360px);
}

.cat-16a,
.cat-16a *,
.cat-16a *::before,
.cat-16a *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cat-16a {
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 48px 24px;
  background: #f4f2ec;
  font-family: 'Recursive','Segoe UI',system-ui,sans-serif;
}

.cat-16a__stage {
  text-align: center;
  padding: 60px clamp(20px,6vw,90px);
  cursor: crosshair;
}

.cat-16a__word {
  font-size: clamp(40px,8vw,104px);
  color: #191714;
  letter-spacing: -.01em;
}

.cat-16a__word i {
  display: inline-block;
  font-style: normal;
  font-variation-settings: 'wght' 380,'CASL' 0,'slnt' 0;
  transition: font-variation-settings .22s ease-out;
}

.cat-16a__sub {
  margin-top: 22px;
  font-family: ui-monospace,Menlo,monospace;
  font-size: 11.5px;
  letter-spacing: .14em;
  color: rgba(25,23,20,.42);
}

@media (prefers-reduced-motion: reduce) {
  .cat-16a__word i {
    transition: none;
  }
}

.cat-16b,
.cat-16b *,
.cat-16b *::before,
.cat-16b *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cat-16b {
  --paper: #181512;
  --cream: #efe6d8;
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 48px 24px;
  background: var(--paper);
  font-family: 'Recursive','Segoe UI',system-ui,sans-serif;
}

.cat-16b__menu {
  display: grid;
  gap: 6px;
  width: min(460px,100%);
}

.cat-16b__label {
  font-size: 11px;
  letter-spacing: .34em;
  text-transform: uppercase;
  color: rgba(239,230,216,.4);
  text-align: center;
  margin-bottom: 18px;
  font-variation-settings: 'wght' 400,'CASL' 1,'slnt' 0;
}

.cat-16b__item {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 24px;
  padding: 14px 10px;
  font-size: clamp(24px,3.6vw,34px);
  color: var(--cream);
  text-decoration: none;
  border-bottom: 1px solid rgba(239,230,216,.12);
  font-variation-settings: 'wght' 350,'CASL' 0,'slnt' 0;
  transition: font-variation-settings .35s ease,color .3s;
}

.cat-16b__item em {
  font-style: normal;
  font-size: .55em;
  color: rgba(239,230,216,.5);
  font-variation-settings: 'wght' 400,'CASL' 0,'slnt' 0;
}

.cat-16b__item:hover,
.cat-16b__item:focus-visible {
  color: #fff;
  font-variation-settings: 'wght' 920,'CASL' 1,'slnt' -6;
}

.cat-16b__item:focus-visible {
  outline: 2px solid var(--cream);
  outline-offset: 4px;
  border-radius: 4px;
}

@media (prefers-reduced-motion: reduce) {
  .cat-16b__item {
    transition: color .2s;
  }
}
(() => {
  const wrap = document.querySelector('#cat-16a'); if (!wrap) return;
  const els = Array.from(wrap.querySelectorAll('i'));
  if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
  let raf = 0;
  const set = (x, y) => els.forEach((el) => {
    const r = el.getBoundingClientRect();
    const d = Math.hypot(x - (r.left + r.width / 2), y - (r.top + r.height / 2));
    const w = Math.max(320, 900 - d * 2.6);
    el.style.fontVariationSettings = "'wght' " + Math.round(w) + ",'CASL' 0,'slnt' 0";
  });
  wrap.addEventListener('pointermove', (e) => {
    cancelAnimationFrame(raf);
    raf = requestAnimationFrame(() => set(e.clientX, e.clientY));
  });
  wrap.addEventListener('pointerleave', () => els.forEach((el) => {
    el.style.fontVariationSettings = "'wght' 380,'CASL' 0,'slnt' 0";
  }));
})();

/* No JavaScript — font-variation-settings tweens 'wght' 350→920, 'CASL' 0→1 and 'slnt' 0→-6 on hover; identical axis lists in both states make it interpolate. */
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 Animation 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 Variable Font Weight Animation on Hover
Source: https://codefronts.com/motion/css-text-animations/css-morphing-text-animation/

Variable-font choreography with Recursive: a headline whose letters swell toward the cursor like type under a magnet (proximity-mapped 'wght'), and a zero-JS menu where each row transitions weight, slant and the CASL casual axis on hover.
## HTML
```html
<div class="cat-16-group">
<link href="https://fonts.googleapis.com/css2?family=Recursive:slnt,wght,CASL@-15..0,300..1000,0..1&display=swap" rel="stylesheet">
<section class="cat-16a" aria-label="Variable font weight follows the cursor">
  <div class="cat-16a__stage" id="cat-16a">
    <h1 class="cat-16a__word" aria-label="come closer">
      <span aria-hidden="true"><i>c</i><i>o</i><i>m</i><i>e</i><i>&nbsp;</i><i>c</i><i>l</i><i>o</i><i>s</i><i>e</i><i>r</i></span>
    </h1>
    <p class="cat-16a__sub" aria-hidden="true">'wght' 320 → 900 mapped to pointer distance</p>
  </div>
</section>
<link href="https://fonts.googleapis.com/css2?family=Recursive:slnt,wght,CASL@-15..0,300..1000,0..1&display=swap" rel="stylesheet">
<section class="cat-16b" aria-label="Variable font menu hover">
  <nav class="cat-16b__menu" aria-label="Cafe menu">
    <p class="cat-16b__label" aria-hidden="true">— the counter —</p>
    <a class="cat-16b__item" href="#">Espresso<em>2.80</em></a>
    <a class="cat-16b__item" href="#">Cortado<em>3.60</em></a>
    <a class="cat-16b__item" href="#">Filter<em>3.20</em></a>
    <a class="cat-16b__item" href="#">Cold brew<em>4.40</em></a>
  </nav>
</section>
</div>
```
## CSS
```css
.cat-16-group {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  width: 100%;
}

.cat-16-group > section {
  flex: 1 1 480px;
  min-width: min(100%,360px);
}

.cat-16a,
.cat-16a *,
.cat-16a *::before,
.cat-16a *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cat-16a {
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 48px 24px;
  background: #f4f2ec;
  font-family: 'Recursive','Segoe UI',system-ui,sans-serif;
}

.cat-16a__stage {
  text-align: center;
  padding: 60px clamp(20px,6vw,90px);
  cursor: crosshair;
}

.cat-16a__word {
  font-size: clamp(40px,8vw,104px);
  color: #191714;
  letter-spacing: -.01em;
}

.cat-16a__word i {
  display: inline-block;
  font-style: normal;
  font-variation-settings: 'wght' 380,'CASL' 0,'slnt' 0;
  transition: font-variation-settings .22s ease-out;
}

.cat-16a__sub {
  margin-top: 22px;
  font-family: ui-monospace,Menlo,monospace;
  font-size: 11.5px;
  letter-spacing: .14em;
  color: rgba(25,23,20,.42);
}

@media (prefers-reduced-motion: reduce) {
  .cat-16a__word i {
    transition: none;
  }
}

.cat-16b,
.cat-16b *,
.cat-16b *::before,
.cat-16b *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cat-16b {
  --paper: #181512;
  --cream: #efe6d8;
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 48px 24px;
  background: var(--paper);
  font-family: 'Recursive','Segoe UI',system-ui,sans-serif;
}

.cat-16b__menu {
  display: grid;
  gap: 6px;
  width: min(460px,100%);
}

.cat-16b__label {
  font-size: 11px;
  letter-spacing: .34em;
  text-transform: uppercase;
  color: rgba(239,230,216,.4);
  text-align: center;
  margin-bottom: 18px;
  font-variation-settings: 'wght' 400,'CASL' 1,'slnt' 0;
}

.cat-16b__item {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 24px;
  padding: 14px 10px;
  font-size: clamp(24px,3.6vw,34px);
  color: var(--cream);
  text-decoration: none;
  border-bottom: 1px solid rgba(239,230,216,.12);
  font-variation-settings: 'wght' 350,'CASL' 0,'slnt' 0;
  transition: font-variation-settings .35s ease,color .3s;
}

.cat-16b__item em {
  font-style: normal;
  font-size: .55em;
  color: rgba(239,230,216,.5);
  font-variation-settings: 'wght' 400,'CASL' 0,'slnt' 0;
}

.cat-16b__item:hover,
.cat-16b__item:focus-visible {
  color: #fff;
  font-variation-settings: 'wght' 920,'CASL' 1,'slnt' -6;
}

.cat-16b__item:focus-visible {
  outline: 2px solid var(--cream);
  outline-offset: 4px;
  border-radius: 4px;
}

@media (prefers-reduced-motion: reduce) {
  .cat-16b__item {
    transition: color .2s;
  }
}
```

## JavaScript
```js
(() => {
  const wrap = document.querySelector('#cat-16a'); if (!wrap) return;
  const els = Array.from(wrap.querySelectorAll('i'));
  if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
  let raf = 0;
  const set = (x, y) => els.forEach((el) => {
    const r = el.getBoundingClientRect();
    const d = Math.hypot(x - (r.left + r.width / 2), y - (r.top + r.height / 2));
    const w = Math.max(320, 900 - d * 2.6);
    el.style.fontVariationSettings = "'wght' " + Math.round(w) + ",'CASL' 0,'slnt' 0";
  });
  wrap.addEventListener('pointermove', (e) => {
    cancelAnimationFrame(raf);
    raf = requestAnimationFrame(() => set(e.clientX, e.clientY));
  });
  wrap.addEventListener('pointerleave', () => els.forEach((el) => {
    el.style.fontVariationSettings = "'wght' 380,'CASL' 0,'slnt' 0";
  }));
})();

/* No JavaScript — font-variation-settings tweens 'wght' 350→920, 'CASL' 0→1 and 'slnt' 0→-6 on hover; identical axis lists in both states make it interpolate. */
```

How this works

The proximity variant measures each letter's distance to the pointer and maps it to a weight, written straight into font-variation-settings; a short CSS transition smooths between frames so the swell feels liquid:

const w = Math.max(320, 900 - dist * 2.6);
el.style.fontVariationSettings = "'wght' " + w;

The pure-CSS variant relies on font-variation-settings being animatable when the axis list stays identical: each menu row declares 'wght' 400, 'CASL' 0, 'slnt' 0 at rest and 'wght' 900, 'CASL' 1, 'slnt' -6 on hover with a 0.35s transition — the font literally changes personality (upright → casual italic) with no layout swap.

Make it yours

  • Falloff: the ×2.6 distance multiplier sets the magnet radius — smaller = wider halo.
  • Clamp weights to your font's real axis range (Recursive: 300–1000).
  • Menu variant: transition only 'wght' for corporate restraint, add 'CASL' for playfulness.
  • Same pattern maps distance to 'wdth' or 'opsz' on fonts that ship those axes.

Gotchas — read before shipping

  • Weight changes reflow neighboring glyphs — display text only, never paragraphs.
  • Keep the axis list identical in both states or the transition snaps instead of tweening.
  • Load the variable font with a full axis range in the URL (wght@300..1000), not comma-separated static weights.

Browser support

ChromeSafariFirefoxEdge
62+11+62+62+

Variable fonts + animatable font-variation-settings are evergreen-universal. Fallback stacks render static weight — content unaffected.

Search CodeFronts

Loading…