# Variable Font Cursor Proximity

> A text component that animates the font variation settings of letters based on the cursor proximity. Works only with variable fonts.

## Table of Contents

- [Installation](#installation)
  - [Cli](#cli)
  - [Manual](#manual)
    - [use-mouse-position-ref](#use-mouse-position-ref)
    - [variable-font-cursor-proximity](#variable-font-cursor-proximity)
- [Understanding the component](#understanding-the-component)
  - [How it works](#how-it-works)
  - [Understanding Variable Fonts](#understanding-variable-fonts)
- [Examples](#examples)
  - [Falloff](#falloff)
- [Notes](#notes)
- [Credits](#credits)
- [Props](#props)

Example:

```tsx
"use client"

import { useRef } from "react"

import { cn } from "@/lib/utils"
import VariableFontCursorProximity from "@/components/fancy/text/variable-font-cursor-proximity"

const texts = ["Overstimulated", "Underutilized", "Familiar", "Extraordinary"]

export default function Preview() {
  const containerRef = useRef<HTMLDivElement>(null)

  return (
    <div
      className="w-dvw h-dvh rounded-lg items-center justify-center font-overused-grotesk bg-[#ff5941] cursor-pointer relative overflow-hidden"
      ref={containerRef}
    >
      <div className="w-full h-full flex flex-col items-center justify-center gap-4 text-white">
        {texts.map((text, i) => (
          <VariableFontCursorProximity
            key={i}
            className={cn("text-4xl md:text-6xl lg:text-7xl leading-none")}
            fromFontVariationSettings="'wght' 400, 'slnt' 0"
            toFontVariationSettings="'wght' 900, 'slnt' -10"
            radius={200}
            containerRef={containerRef}
          >
            {text}
          </VariableFontCursorProximity>
        ))}
      </div>
    </div>
  )
}

```

A generalized version of this component (where you can control any CSS property) is available in the [Text Cursor Proximity](https://fancycomponents.dev/docs/components/text/text-cursor-proximity.md) component.

## Installation 

### Cli

```bash
npx shadcn add @fancy/variable-font-cursor-proximity
```

### Manual

Create a hook for querying the cursor/mouse position.

#### use-mouse-position-ref

```tsx
import { RefObject, useEffect, useRef } from "react"

export const useMousePositionRef = (
  containerRef?: RefObject<HTMLElement | SVGElement | null>
) => {
  const positionRef = useRef({ x: 0, y: 0 })

  useEffect(() => {
    const updatePosition = (x: number, y: number) => {
      if (containerRef && containerRef.current) {
        const rect = containerRef.current.getBoundingClientRect()
        const relativeX = x - rect.left
        const relativeY = y - rect.top

        // Calculate relative position even when outside the container
        positionRef.current = { x: relativeX, y: relativeY }
      } else {
        positionRef.current = { x, y }
      }
    }

    const handleMouseMove = (ev: MouseEvent) => {
      updatePosition(ev.clientX, ev.clientY)
    }

    const handleTouchMove = (ev: TouchEvent) => {
      const touch = ev.touches[0]
      updatePosition(touch.clientX, touch.clientY)
    }

    // Listen for both mouse and touch events
    window.addEventListener("mousemove", handleMouseMove)
    window.addEventListener("touchmove", handleTouchMove)

    return () => {
      window.removeEventListener("mousemove", handleMouseMove)
      window.removeEventListener("touchmove", handleTouchMove)
    }
  }, [containerRef])

  return positionRef
}

```

This hook actually returns a ref to the position (instead of a state), so we can avoid re-renders when the cursor moves.

Then, copy and paste the component source code into your project:

#### variable-font-cursor-proximity

```tsx
"use client"

import React, { ElementType, forwardRef, useMemo, useRef } from "react"
import { motion, useAnimationFrame } from "motion/react"

import { cn } from "@/lib/utils"
import { useMousePositionRef } from "@/hooks/use-mouse-position-ref"

/**
 * Props for the VariableFontCursorProximity component.
 */
interface TextProps extends React.HTMLAttributes<HTMLElement> {
  /**
   * The text content to display and animate.
   * Each letter will respond individually to cursor proximity.
   * Required prop with no default value.
   */
  children: React.ReactNode

  /**
   * HTML Tag to render the component as.
   * @default "span"
   */
  as?: ElementType

  /**
   * Default font variation settings applied when cursor is outside the radius.
   * Should be a CSS font-variation-settings string (e.g., "'wght' 400, 'slnt' 0").
   * You should check the font variation settings of the font you are using to see the available axes.
   * Required prop with no default value.
   */
  fromFontVariationSettings: string

  /**
   * Target font variation settings applied when cursor is at the center of a letter.
   * Should be a CSS font-variation-settings string (e.g., "'wght' 900, 'slnt' 15").
   * Make sure to check the font variation settings of the font you are using to see the available axes.
   * Required prop with no default value.
   */
  toFontVariationSettings: string

  /**
   * Reference to the container element for mouse tracking.
   * The cursor position will be calculated relative to this container's bounds.
   * Required prop with no default value.
   */
  containerRef: React.RefObject<HTMLDivElement | null>

  /**
   * The radius in pixels within which letters respond to cursor proximity.
   * Letters outside this radius will use the default font variation settings.
   * @default 50
   */
  radius?: number

  /**
   * The falloff function that determines how the effect diminishes with distance.
   * - "linear": Linear interpolation (straight line falloff)
   * - "exponential": Quadratic falloff (more dramatic near cursor)
   * - "gaussian": Bell curve falloff (smooth, natural feeling)
   * @default "linear"
   */
  falloff?: "linear" | "exponential" | "gaussian"
}

const VariableFontCursorProximity = forwardRef<HTMLElement, TextProps>(
  (
    {
      children,
      as = "span",
      fromFontVariationSettings,
      toFontVariationSettings,
      containerRef,
      radius = 50,
      falloff = "linear",
      className,
      ...props
    },
    ref
  ) => {
    // Refs to store references to each individual letter element
    const letterRefs = useRef<(HTMLSpanElement | null)[]>([])

    // Cache for interpolated font settings to avoid recalculation
    const interpolatedSettingsRef = useRef<string[]>([])

    // Hook to track mouse position relative to the specified container
    const mousePositionRef = useMousePositionRef(containerRef)

    /**
     * Parse and prepare font variation settings for interpolation.
     *
     * Converts CSS font-variation-settings strings into structured data that can be
     * efficiently interpolated. Each axis is parsed with its from/to values for
     * smooth transitions during proximity-based animation.
     *
     * Expected format: "'wght' 400, 'slnt' 0" -> Map of axis names to values
     */
    const parsedSettings = useMemo(() => {
      // Parse the 'from' font variation settings string
      const fromSettings = new Map(
        fromFontVariationSettings
          .split(",")
          .map((s) => s.trim())
          .map((s) => {
            const [name, value] = s.split(" ")
            return [name.replace(/['"]/g, ""), parseFloat(value)]
          })
      )

      // Parse the 'to' font variation settings string
      const toSettings = new Map(
        toFontVariationSettings
          .split(",")
          .map((s) => s.trim())
          .map((s) => {
            const [name, value] = s.split(" ")
            return [name.replace(/['"]/g, ""), parseFloat(value)]
          })
      )

      // Create structured data for each axis with from/to values
      return Array.from(fromSettings.entries()).map(([axis, fromValue]) => ({
        axis,
        fromValue,
        toValue: toSettings.get(axis) ?? fromValue,
      }))
    }, [fromFontVariationSettings, toFontVariationSettings])

    /**
     * Calculate Euclidean distance between two points.
     *
     * Used to determine the distance between the cursor position and each letter's center.
     * This distance is then used to calculate the proximity effect strength.
     *
     * @param x1 - X coordinate of first point (cursor)
     * @param y1 - Y coordinate of first point (cursor)
     * @param x2 - X coordinate of second point (letter center)
     * @param y2 - Y coordinate of second point (letter center)
     * @returns Distance in pixels between the two points
     */
    const calculateDistance = (
      x1: number,
      y1: number,
      x2: number,
      y2: number
    ): number => {
      return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2))
    }

    /**
     * Calculate the falloff value based on distance and selected falloff type.
     *
     * This function determines how strongly the proximity effect affects each letter
     * based on its distance from the cursor. Different falloff types create different
     * visual effects and feelings of interaction.
     *
     * @param distance - Distance in pixels from cursor to letter center
     * @returns Falloff value between 0 (no effect) and 1 (full effect)
     */
    const calculateFalloff = (distance: number): number => {
      // Normalize distance to 0-1 range within the radius
      const normalizedDistance = Math.min(Math.max(1 - distance / radius, 0), 1)

      switch (falloff) {
        case "exponential":
          // Quadratic falloff - more dramatic effect near cursor
          return Math.pow(normalizedDistance, 2)
        case "gaussian":
          // Bell curve falloff - smooth, natural feeling
          return Math.exp(-Math.pow(distance / (radius / 2), 2) / 2)
        case "linear":
        default:
          // Linear falloff - consistent rate of change
          return normalizedDistance
      }
    }

    // Use animation frame to smoothly update font variations for all letters
    // This ensures smooth transitions as the cursor moves across the text
    useAnimationFrame(() => {
      if (!containerRef.current) return
      const containerRect = containerRef.current.getBoundingClientRect()

      // Process each letter individually for proximity-based animation
      letterRefs.current.forEach((letterRef, index) => {
        if (!letterRef) return

        // Calculate letter's center position relative to container
        const rect = letterRef.getBoundingClientRect()
        const letterCenterX = rect.left + rect.width / 2 - containerRect.left
        const letterCenterY = rect.top + rect.height / 2 - containerRect.top

        // Calculate distance from cursor to this letter's center
        const distance = calculateDistance(
          mousePositionRef.current.x,
          mousePositionRef.current.y,
          letterCenterX,
          letterCenterY
        )

        // If letter is outside the effect radius, reset to default settings
        if (distance >= radius) {
          if (
            letterRef.style.fontVariationSettings !== fromFontVariationSettings
          ) {
            letterRef.style.fontVariationSettings = fromFontVariationSettings
          }
          return
        }

        // Calculate falloff strength based on distance and falloff type
        const falloffValue = calculateFalloff(distance)

        // Interpolate between from and to settings for each axis
        const newSettings = parsedSettings
          .map(({ axis, fromValue, toValue }) => {
            const interpolatedValue =
              fromValue + (toValue - fromValue) * falloffValue
            return `'${axis}' ${interpolatedValue}`
          })
          .join(", ")

        // Cache and apply the interpolated settings
        interpolatedSettingsRef.current[index] = newSettings
        letterRef.style.fontVariationSettings = newSettings
      })
    })

    // Split text into words and track letter indices across all words
    const words = String(children).split(" ")
    let letterIndex = 0
    const ElementTag = as

    return (
      <ElementTag
        ref={ref}
        className={cn(
          className,
        )}
        {...props}
        data-text={children}
      >
        {words.map((word, wordIndex) => (
          <span
            key={wordIndex}
            className="inline-block whitespace-nowrap"
            aria-hidden
          >
            {word.split("").map((letter) => {
              const currentLetterIndex = letterIndex++
              return (
                <motion.span
                  key={currentLetterIndex}
                  ref={(el: HTMLSpanElement | null) => {
                    letterRefs.current[currentLetterIndex] = el
                  }}
                  className="inline-block"
                  aria-hidden="true"
                  style={{
                    fontVariationSettings:
                      interpolatedSettingsRef.current[currentLetterIndex],
                  }}
                >
                  {letter}
                </motion.span>
              )
            })}
            {wordIndex < words.length - 1 && (
              <span className="inline-block">&nbsp;</span>
            )}
          </span>
        ))}
        <span className="sr-only">{children}</span>
      </ElementTag>
    )
  }
)

VariableFontCursorProximity.displayName = "VariableFontCursorProximity"
export default VariableFontCursorProximity

```

## Understanding the component

The `VariableFontCursorProximity` splits its text into letters, that respond to cursor movement by adjusting their font variation settings, based on the distance between the letter and cursor distance. This component works only with variable fonts.

1. Splitting text into individual letters
2. Tracking cursor position relative to each letter
3. Smoothly transitioning font variations based on proximity
4. Supporting multiple falloff patterns for the effect

This component requires the use of variable fonts to function properly. Otherwise it will not work.

### How it works

The component calculates the distance between the cursor and each letter in real-time. When the cursor comes within the specified `radius` of a letter, that letter's font variations (like weight, slant, etc.) smoothly interpolate between two states:

- The default state (`fromFontVariationSettings`)
- The target state (`toFontVariationSettings`)

The closer the cursor gets to a letter, the closer that letter moves toward its target state.

### Understanding Variable Fonts

For more information about variable fonts and how they work, please refer to the [Variable Font Hover By Letter](https://fancycomponents.dev/docs/components/text/variable-font-hover-by-letter#understanding-variable-fonts.md) documentation.

## Examples

### Falloff

With the `falloff` prop, you can control the type of falloff. It can be either `linear`, `exponential`, or `gaussian`. The following demo showcases the `exponential` one. The effects are best observed on a larger block of text.

Example:

```tsx
"use client"

import { useRef } from "react"

import VariableFontCursorProximity from "@/components/fancy/text/variable-font-cursor-proximity"

export default function Preview() {
  const containerRef = useRef<HTMLDivElement>(null)

  return (
    <div
      className="w-dvw h-dvh rounded-lg items-center justify-center font-overused-grotesk p-8 sm:p-16 md:p-20 lg:p-24 bg-white cursor-pointer relative overflow-hidden"
      ref={containerRef}
    >
      <div className="w-full h-full items-center justify-center grid text-justify">
        <VariableFontCursorProximity
          className="leading-tight text-xs sm:text-sm md:text-base lg:text-lg text-[#ff5941] -m-4 p-2"
          fromFontVariationSettings="'wght' 400, 'slnt' 0"
          toFontVariationSettings="'wght' 900, 'slnt' -10"
          falloff="exponential"
          radius={70}
          containerRef={containerRef}
        >
          {`Modern typography is based primarily on the theories and principles of design evolved in the 20's and 30's of our century. It was Mallarmé and Rimbaud in the 19th century and Apollinaire in the early 20th century who paved the way to a new understanding of the possibilities inherent in typography and who, released from conventional prejudices and fetters, created through their experiments the basis for the pioneer achievements of the theoreticians and practitioners that followed. Walter Dexel, El Lissitzky, Kurt Schwitters, Jan Tschichold, Paul Renner, Moholy-Nagy, Joost Schmidt etc. breathed new life into an unduly rigid typography. In his book "Die neue Typografie" (1928) J. Tschichold formulated the rules of an up-to-date and objective typography which met the needs of the age.`}
        </VariableFontCursorProximity>
      </div>
    </div>
  )
}

```

## Notes

- Interpolating on large number of letters simultaneously can be a bit slow, even when we're avoiding re-renders with state updates. If you're experienceing performance issues, try to limit the length of the text you're animating.

- Make sure the main container has enough space to hold the text at its full weight to avoid layout shifts. For example, you can use negative margins like in the 2nd example.

## Credits

Ported to Framer by [Framer University](https://framer.university/)

## Props

| Prop | Type | Default | Description |
|----------|----------|----------|----------|
| label* | `string` | - | The text to be displayed and animated |
| fromFontVariationSettings* | `string` | - | Default font variation settings |
| toFontVariationSettings* | `string` | - | Target font variation settings |
| containerRef* | `React.RefObject` | - | Reference to the container for mouse tracking |
| radius | `number` | `50` | The radius of the proximity effect |
| falloff | `"linear" | "exponential" | "gaussian"` | `"linear"` | The falloff type of the proximity |
| className | `string` | - | Additional CSS classes for styling |

---

*This documentation is also available in [interactive format](https://fancycomponents.dev/docs/components/components/text/variable-font-cursor-proximity).*