magnettype
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
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 markupOptions
| Option | Default | Description |
|---|---|---|
| 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. |
| radius | 120 | Pixel 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. |
| cachePositions | true | Cache 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. |
| stabilizeLayout | true | Apply compensating letter-spacing when axis values change to prevent line-reflow as font weight shifts. |
| transitionMs | 0 | CSS transition duration in milliseconds applied when the cursor leaves the field. Adds a smooth ease-out rather than a snap back to restValue. |
| props | — | Additional 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. |
| wdthBoost | 6 | wdth axis units added to confusable characters in legibility mode. Risk-proportional — highest-risk characters receive the full boost. |