magnettype


npm ↗
GitHub ↗
TypeScriptZero dependenciesReact + Vanilla JS

CSS applies font-variation-settings to an entire element. Magnet Type applies them word by word or character by character — driven by cursor proximity. Words and characters inside the field attract toward a peak axis value; those outside hold at rest. Move your mouse through a paragraph and watch each word or letter respond independently.

Live demo — move your cursor through the text

Character mode active
Mode
Spread Radius80
Weight High900
Weight Low300

Character mode — per-character weight gradient across any block element. Works with mixed content (inline code, links, etc). Move your cursor through the paragraphs below.

spreadRadius controls how far from the cursor each character's weight fades to its minimum. Use proximityRadius to gate the effect to when the cursor is near the element edge, or omit it to always respond. Both props are independent and combinable.

How it works

The cursor field

Each word gets a span. On every animation frame, the distance from the cursor to each word’s center is measured. Words within the radius receive a font-variation-settings value interpolated between rest and peak — closer words get more of the peak value.

Attract and repel

In attract mode, nearby words approach the peak axis value and far words hold at rest. In repel mode, the logic inverts — words near the cursor stay at rest while words in the outer ring of the field approach peak. Each creates a different reading texture.

Field mode vs legibility mode

Field mode runs a requestAnimationFrame loop driven by cursor position. Legibility mode is static — it wraps visually confusable characters (il1I, rn, 0O) in spans with a boosted wdth axis, improving disambiguation at small sizes without cursor interaction.

Performance via rAF batching

The field loop reads word positions with getBoundingClientRect in a single batch pass, then writes all font-variation-settings in a second pass. No layout thrashing. The loop is cancelled on unmount and restarted when options change.

Usage

TypeScript + React · Vanilla JS

Drop-in component

import { MagnetTypeText } from '@liiift-studio/magnettype'

<MagnetTypeText mode="word" axes={{ wght: [300, 600] }} radius={120}>
  Your paragraph text here...
</MagnetTypeText>

Hook — attach to any element

import { useMagnetType } from '@liiift-studio/magnettype'

const ref = useMagnetType({ mode: 'word', axes: { wght: [300, 600] }, radius: 120 })
<p ref={ref}>{children}</p>

Per-character component

import { MagnetChar } from '@liiift-studio/magnettype'

<MagnetChar minWeight={300} maxWeight={900} spreadRadius={80}>
  Your paragraph text here...
</MagnetChar>

Vanilla JS

import { startMagnetType, getCleanHTML } from '@liiift-studio/magnettype'

const el = document.querySelector('p')
const original = getCleanHTML(el) // capture original HTML before injection
const stop = startMagnetType(el, original, { axes: { wght: [300, 600] }, radius: 120 })
// call stop() to cancel the rAF loop and restore markup

Options

MagnetType options reference
OptionDefaultDescription
mode'word''word' — cursor proximity drives per-word font-variation-settings. 'legibility' — cursor-proximity-driven wdth boost for confusable characters; on touch devices the boost is always active.
axes{ wght: [300, 500] }Map of axis tag → [restValue, peakValue]. restValue applies at full distance; peakValue when cursor is directly over the word.
radius120Pixel radius over which the field effect fades. Words beyond this distance receive restValue.
falloff'quadratic''linear' — strength decreases linearly with distance. 'quadratic' — decreases as distance², giving a tighter hot zone.
magnetMode'attract''attract' — words near cursor approach peakValue. 'repel' — words near cursor stay at restValue; far words approach peakValue.
scope'document''document' — listens for cursor events on the document (all instances share one field). 'element' — listens only within the element's own bounds.
cachePositionstrueCache word bounding rects after the first measurement pass. Eliminates repeated getBoundingClientRect calls during the active cursor loop; set to false if the layout shifts at runtime.
stabilizeLayouttrueApply compensating letter-spacing when axis values change to prevent line-reflow as font weight shifts.
transitionMs0CSS transition duration in milliseconds applied when the cursor leaves the field. Adds a smooth ease-out rather than a snap back to restValue.
propsAdditional proximity-driven effects via MagnetTypeProps. Accepts opacity: [rest, peak] to fade words by distance, and italic: true to italicise words as they enter the field.
wdthBoost6wdth axis units added to confusable characters in legibility mode. Risk-proportional — highest-risk characters receive the full boost.