Components / Motion / Follow Frame
Follow Frame beta
A dashed frame that trails the cursor and names the action waiting there.
<div class="k-fzone" data-follow style="max-width:430px"> <div class="k-frame" data-frame></div> <div class="k-frame-tag" data-frame-tag><i></i>Approve & send</div> <span class="hint">Move your cursor — the frame follows, and names the action.</span> </div>
.k-fzone{position:relative;width:100%;min-height:190px;border:1px solid var(--k-line);border-radius:14px;background:var(--k-bg);overflow:hidden;font-family:var(--k-font)}
.k-fzone .hint{position:absolute;left:16px;bottom:12px;font-size:12px;color:var(--k-ink-3)}
.k-frame{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:118px;height:74px;border:1.5px dashed var(--k-border-strong);border-radius:10px;pointer-events:none;will-change:transform}
.k-frame-tag{position:absolute;left:50%;top:50%;transform:translate(-50%,-168%);display:flex;gap:6px;align-items:center;background:var(--k-ink);color:var(--k-bg);font-size:11px;font-weight:550;padding:4px 9px;border-radius:99px;pointer-events:none;white-space:nowrap;will-change:transform}
.k-frame-tag i{width:6px;height:6px;border-radius:50%;background:var(--k-core);display:block;flex:none}
const fz = document.querySelector('[data-follow]');
const fFrame = fz.querySelector('[data-frame]');
const fTag = fz.querySelector('[data-frame-tag]');
const fReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let fTx = 0, fTy = 0, fX = 0, fY = 0, fMoved = false;
function fCenter() { fTx = fz.clientWidth / 2; fTy = fz.clientHeight / 2; }
function fPlace() {
fFrame.style.left = fX + 'px'; fFrame.style.top = fY + 'px';
fTag.style.left = fX + 'px'; fTag.style.top = fY + 'px';
}
fCenter(); fX = fTx; fY = fTy; fPlace();
fz.addEventListener('pointermove', e => {
const r = fz.getBoundingClientRect();
fMoved = true;
fTx = Math.max(64, Math.min(r.width - 64, e.clientX - r.left));
fTy = Math.max(48, Math.min(r.height - 48, e.clientY - r.top));
});
fz.addEventListener('pointerleave', () => { fMoved = false; fCenter(); });
window.addEventListener('resize', () => { if (!fMoved) { fCenter(); fX = fTx; fY = fTy; fPlace(); } });
if (!fReduced) {
(function fLoop() {
fX += (fTx - fX) * 0.16;
fY += (fTy - fY) * 0.16;
fPlace();
requestAnimationFrame(fLoop);
})();
}
Usage
For playful surfaces — onboarding, product tours, marketing panels. One frame per surface, and the label must name a real action.
Accessibility
Pointer-only by design; touch and keyboard users lose nothing, since the frame carries no information the panel itself does not. It parks, centered, under prefers-reduced-motion.
Related parts
Aurora beta
A slow wash of color behind a hero — the accent and the core, drifting under glass.
Beam border beta
A single point of light travels the edge of a card. The beam is the core color — the one accent, in motion.
Converge beta
Light streams gather into one tile — many inputs, one place where they land.
Counter beta
Numbers that count up once, when they arrive on screen — machined, not bouncy.