Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
Type to search… it’s not that hard.
Bummer… nothing matched that.
Text Animations
Variable Font Weight Hover

Info

A smooth variable font hover effect where each character responds to the cursor, dynamically shifting weight based on distance. As you move closer, the text subtly becomes heavier or lighter, creating a soft, interactive feel.

Text Animations
May 2, 2026

Required

No items found.

Setup

00

Copy structure to Elementor

Right-click in Elementor, choose “Paste from another site,” and while the popup is open, press cmd/ctrl + v to insert the layout.

Copy To Elementor
Download JSON Template
00

Add External Scripts

Place the scripts before the body tag of your project. If you have added them before for another setup, skip this step.

<!-- GSAP Core -->
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15/dist/gsap.min.js"></script>

<!-- GSAP SplitText -->
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15/dist/SplitText.min.js"></script>
00

Add Custom Javascript

Paste the script through Elementor → Custom Code and set it to load after the closing body tag.

document.addEventListener("DOMContentLoaded", () => {
  if (document.body.classList.contains("elementor-editor-active")) return;
  if (!window.gsap || !window.SplitText) return;
  if (window.matchMedia("(hover: none), (pointer: coarse)").matches) return;
  if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;

  gsap.registerPlugin(SplitText);

  const defaults = {
    min: 300,
    max: 900,
    distance: 160,
    duration: 0.25,
    ease: "power3.out",
    mode: "gain"
  };

  const glyphs = [];
  const pointer = { x: 0, y: 0 };
  const clamp01 = gsap.utils.clamp(0, 1);
  let active = false;

  const getNum = (value, fallback) => {
    const num = parseFloat(value);
    return Number.isFinite(num) ? num : fallback;
  };

  const getMode = (value, fallback) => {
    return ["gain", "loss"].includes(value) ? value : fallback;
  };

  const getCurrentWeight = (element) => {
    const weight = window.getComputedStyle(element).fontWeight;
    if (weight === "normal") return 400;
    if (weight === "bold") return 700;
    const num = parseFloat(weight);
    return Number.isFinite(num) ? num : 400;
  };

  const measureAll = () => {
    glyphs.forEach((glyph) => {
      const rect = glyph.el.getBoundingClientRect();
      glyph.x = rect.left + window.scrollX + rect.width / 2;
      glyph.y = rect.top + window.scrollY + rect.height / 2;
    });
  };

  gsap.utils.toArray('[data-vfont-hover="text"]').forEach((item) => {
    const baseWeight = getCurrentWeight(item);

    const min      = getNum(item.dataset.vfontMin,      defaults.min);
    const max      = getNum(item.dataset.vfontMax,      defaults.max);
    const distance = getNum(item.dataset.vfontDistance, defaults.distance);
    const duration = getNum(item.dataset.vfontDuration, defaults.duration);
    const ease     = item.dataset.vfontEase             || defaults.ease;
    const mode     = getMode(item.dataset.vfontMode,    defaults.mode);

    new SplitText(item, { type: "words,chars" }).chars.forEach((char) => {
      gsap.set(char, {
        "--vfont-wght": baseWeight,
        fontVariationSettings: '"wght" var(--vfont-wght)'
      });

      glyphs.push({
        el: char,
        baseWeight,
        min,
        max,
        distance,
        mode,
        x: 0,
        y: 0,
        setWeight: gsap.quickTo(char, "--vfont-wght", { duration, ease, overwrite: "auto" })
      });
    });
  });

  if (!glyphs.length) return;

  let resizeTimer = null;
  const debouncedMeasure = () => {
    clearTimeout(resizeTimer);
    resizeTimer = setTimeout(measureAll, 100);
  };

  window.addEventListener("pointermove", (e) => {
    pointer.x = e.pageX;
    pointer.y = e.pageY;
    if (active) return;
    active = true;
    measureAll();
  }, { passive: true });

  window.addEventListener("resize", debouncedMeasure, { passive: true });

  document.fonts?.ready?.then(measureAll).catch(() => {});

  let resizeObserver = null;
  if (window.ResizeObserver) {
    resizeObserver = new ResizeObserver(debouncedMeasure);
    glyphs.forEach(({ el }) => resizeObserver.observe(el));
  }

  const tick = () => {
    if (!active) return;
    glyphs.forEach((glyph) => {
      const progress = clamp01(1 - Math.hypot(pointer.x - glyph.x, pointer.y - glyph.y) / glyph.distance);
      const weight = glyph.mode === "loss"
        ? glyph.baseWeight + (glyph.min - glyph.baseWeight) * progress
        : glyph.baseWeight + (glyph.max - glyph.baseWeight) * progress;
      glyph.setWeight(weight);
    });
  };

  gsap.ticker.add(tick);

  window.addEventListener("pagehide", () => {
    gsap.ticker.remove(tick);
    resizeObserver?.disconnect();
    clearTimeout(resizeTimer);
  }, { once: true });
});
00

Publish and preview live

Some solutions only work on the live site. Always publish and test after each change, as results may not appear in the editor.

Note

This effect only works properly with a variable font that supports the wght axis. If the selected font does not support variable weight, the script can still run, but the animation may not be visible because the font weight cannot smoothly change.

Required Attribute

Text

Add data-vfont-hover="text" to the text element you want to animate. This is the only required attribute. The script will split the text into words and characters, then animate each character’s font weight based on how close the cursor is to it.

Settings

Mode

Mode controls whether the characters become heavier or lighter when the cursor gets close to them. By default, the value is gain. You can override this by adding data-vfont-mode="VALUE" to the text element. Use gain if you want nearby characters to become bolder, or use loss if you want nearby characters to become lighter.

Minimum Weight

Minimum weight controls how light the characters can become when the effect is using loss mode. By default, the value is 300.

The script first reads the current font weight from your CSS styling, then animates from that original weight down toward the minimum value when the cursor gets close. You can override this by adding data-vfont-min="VALUE" to the text element.

Lower values make the characters thinner, but the result depends on the weight range supported by your variable font.

Maximum Weight

Maximum weight controls how bold the characters can become when the effect is using gain mode. By default, the value is 900.

The script first reads the current font weight from your CSS styling, then animates from that original weight up toward the maximum value when the cursor gets close. You can override this by adding data-vfont-max="VALUE" to the text element.

Higher values make the characters heavier, but your font must support that weight for the animation to show correctly.

Distance

Distance controls how close the cursor needs to be before the characters start reacting. By default, the value is 160 px. You can override this by adding data-vfont-distance="VALUE" to the text element. Higher values create a wider reaction area, while lower values make the effect feel tighter and more focused.

Duration

Duration controls how long the font weight animation takes to move from one value to another. By default, the value is 0.25 seconds. You can override this by adding data-vfont-duration="VALUE" to the text element. Lower values feel snappier, while higher values feel smoother and more relaxed.

Ease

Ease controls the motion curve used when the font weight changes. By default, the value is power3.out. You can override this by adding data-vfont-ease="VALUE" to the text element. This accepts any valid GSAP easing value, so you can make the animation feel sharper, softer, or more playful depending on the effect you want.