magnettype
Per-word axis
variation.
CSS applies font-variation-settings to an entire element. Magnet Type applies them word by word — driven by cursor proximity. Words inside the field attract toward a peak axis value; words outside hold at rest. Move your mouse through a paragraph and watch each word 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="field" 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: 'field', axes: { wght: [300, 600] }, radius: 120 })
<p ref={ref}>{children}</p>Vanilla JS
import { startMagnetType, getCleanHTML } from '@liiift-studio/magnettype'
const el = document.querySelector('p')
const original = getCleanHTML(el)
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 | 'field' | 'field' — cursor proximity drives per-word font-variation-settings. 'legibility' — static per-character wdth boost for confusable characters. |
| 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. |
| wdthBoost | 6 | wdth axis units added to confusable characters in legibility mode. Risk-proportional — highest-risk characters receive the full boost. |