Optical Type Strain

Type that shears and slips under the cursor

Move the pointer across the paragraph. Glyphs inside a 120px radius translate, skew, and scaleY, then ease back.

One span per glyph

transform runs on the element box. Skewing a whole word shears the bounding box, not each letter. Each character is a span. Distance to the pointer is Math.hypot; outside 120px the glyph resets.

Smoothstep

A hard radius makes a visible circle. Smoothstep eases force from 1 at the pointer to 0 at 120px. Push is 14px, skew is 12deg, Y is scaled by 0.65 so the line does not jump. Values go to CSS variables on translate3d, skewX, and scaleY. Transforms only — layout would reflow while you move.

const t = 1 - dist / 120;
const force = t * t * (3 - 2 * t);
const nx = dx / dist;
const ny = dy / dist;

el.style.setProperty("--ots-x", `${nx * 14 * force}px`);
el.style.setProperty("--ots-y", `${ny * 14 * force * 0.65}px`);
el.style.setProperty("--ots-skew-x", `${-nx * 12 * force}deg`);
el.style.setProperty("--ots-scale-y", `${1 + force * 0.18}`);

Timing

Pointer move sets data-active: 80ms. Pointer leave clears it: 420ms back to identity. Same duration both ways feels like a toggle. Attack slower than 80ms lags the pointer.

.ots-char {
  transition: transform 420ms cubic-bezier(0.22, 1, 0.36, 1);
}

.ots-root[data-active="true"] .ots-char {
  transition-duration: 80ms;
  transition-timing-function: cubic-bezier(0.2, 0, 0, 1);
}

Wrap

inline-block letters wrap mid-word. Group them in a word span with white-space: nowrap. Spaces stay their own spans so wrap points stay on word boundaries.