GitHub

Components / Motion / Follow Frame

Follow Frame beta

A dashed frame that trails the cursor and names the action waiting there.

Preview
Approve & send
Move your cursor — the frame follows, and names the action.
<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 &amp; 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