No.166
CURSOR CSSJS

Lens Magnifier

マウスを乗せた場所を丸いルーペの中に拡大して見せる演出。画像だけでなく文字や図形もそのまま拡大でき、タップと矢印キーでも動かせるようにしています。

DEMO

MOTION
LAB

No.166 · LENS · 細かい文字もルーペで読めます · 2026

マウスを乗せる・タップする・矢印キーで拡大する場所を動かせます

CODE

HTML
<div class="demo-stage lns-stage">
  <div class="lns-frame" id="lns-frame" tabindex="0" role="group" aria-label="拡大鏡のデモ" aria-describedby="lns-hint">
    <div class="lns-art" id="lns-art">
      <p class="lns-big">MOTION<br>LAB</p>
      <p class="lns-small">No.166 · LENS · 細かい文字もルーペで読めます · 2026</p>
    </div>
    <div class="lns-lens" id="lns-lens" aria-hidden="true"></div>
  </div>
  <p class="lns-hint" id="lns-hint">マウスを乗せる・タップする・矢印キーで拡大する場所を動かせます</p>
</div>
CSS
.lns-stage { display:flex; flex-direction:column; align-items:center; justify-content:center; gap:.8rem; padding:1.5rem; background:#0B0B10; min-height:300px; }
.lns-frame { position:relative; width:100%; max-width:360px; aspect-ratio:16/10; border-radius:14px; overflow:hidden; cursor:crosshair; outline:none; }
.lns-frame:focus-visible { box-shadow:0 0 0 3px #fff; }
.lns-art { position:absolute; inset:0; box-sizing:border-box; display:flex; flex-direction:column; justify-content:space-between; padding:1rem 1.1rem;
  background: repeating-linear-gradient(0deg, rgba(255,255,255,.08) 0 1px, transparent 1px 12px), repeating-linear-gradient(90deg, rgba(255,255,255,.08) 0 1px, transparent 1px 12px), linear-gradient(135deg,#4C1D95,#9D174D 60%,#92400E); }
.lns-big { margin:0; font-family:'Oswald',sans-serif; font-size:2.4rem; line-height:1; letter-spacing:.06em; color:#fff; }
.lns-small { margin:0; font-size:.55rem; letter-spacing:.02em; color:#fff; }
.lns-lens { position:absolute; top:0; left:0; box-sizing:border-box; width:120px; height:120px; margin:-60px 0 0 -60px; border-radius:50%; overflow:hidden; pointer-events:none;
  border:2px solid rgba(255,255,255,.9); box-shadow:0 10px 30px rgba(0,0,0,.45); opacity:0; transform:scale(.6); transition: opacity .2s ease, transform .2s ease; }
.lns-lens.is-on { opacity:1; transform:scale(1); }
.lns-lens .lns-art { right:auto; bottom:auto; transform-origin:0 0; }
.lns-hint { margin:0; color:#A1A1AA; font-size:.74rem; text-align:center; }
@media (prefers-reduced-motion: reduce) {
  .lns-lens { transition-duration:.01ms; }
}
JS
(function(){
  var frame = document.getElementById('lns-frame');
  var art = document.getElementById('lns-art');
  var lens = document.getElementById('lns-lens');
  if (!frame || !art || !lens) return;
  var Z = 2.2, x = 0, y = 0;
  var copy = art.cloneNode(true);
  copy.removeAttribute('id');
  lens.appendChild(copy);
  function place(){
    var w = frame.clientWidth, h = frame.clientHeight, r = lens.clientWidth / 2;
    x = Math.max(0, Math.min(w, x)); y = Math.max(0, Math.min(h, y));
    lens.style.left = x + 'px'; lens.style.top = y + 'px';
    copy.style.width = w + 'px'; copy.style.height = h + 'px';
    copy.style.transform = 'translate(' + (r - x * Z) + 'px,' + (r - y * Z) + 'px) scale(' + Z + ')';
    lens.classList.add('is-on');
  }
  function fromEvent(e){ var b = frame.getBoundingClientRect(); x = e.clientX - b.left; y = e.clientY - b.top; place(); }
  frame.addEventListener('pointermove', function(e){ if (e.pointerType === 'mouse') fromEvent(e); });
  frame.addEventListener('pointerdown', fromEvent);
  frame.addEventListener('pointerleave', function(e){ if (e.pointerType === 'mouse') lens.classList.remove('is-on'); });
  frame.addEventListener('focus', function(){ if (!lens.classList.contains('is-on')) { x = frame.clientWidth / 2; y = frame.clientHeight / 2; place(); } });
  frame.addEventListener('blur', function(){ lens.classList.remove('is-on'); });
  frame.addEventListener('keydown', function(e){
    var d = e.shiftKey ? 40 : 12, k = e.key;
    if (k === 'ArrowLeft') x -= d; else if (k === 'ArrowRight') x += d;
    else if (k === 'ArrowUp') y -= d; else if (k === 'ArrowDown') y += d;
    else if (k === 'Escape') { lens.classList.remove('is-on'); return; }
    else return;
    e.preventDefault(); place();
  });
})();

AI PROMPT

Claude Code Cursor v0
PROMPT
マウスを乗せた場所を丸いルーペの中に拡大して見せる Lens(拡大鏡)を作って。拡大したい内容(文字や図形を含む要素)を position:relative・overflow:hidden の枠に入れ、同じ枠の中に直径120px・border-radius:50%・overflow:hidden・pointer-events:none のルーペを position:absolute で置く。ルーペの中には、元の内容を cloneNode(true) で複製して入れる(id は外す)。複製には元の枠と同じ幅と高さを px で指定し、transform-origin: 0 0 で transform: translate(r - x × 倍率, r - y × 倍率) scale(倍率) をかける(r はルーペの内側の半径、x・y は枠の中のマウス位置)。こうするとマウスの下の点がちょうどルーペの中心に来る。倍率は2.2倍。ルーペは表示時に scale(.6) から scale(1) へ .2秒で現れる。マウスは pointermove で追従させて枠から外れたら隠し、タッチはタップ(pointerdown)した位置へルーペを置く(タッチでのドラッグ追従はしない=ページのスクロールを妨げないため)。枠を tabindex=0 にして、フォーカスした時は中央にルーペを出し、矢印キーで12px(Shift で40px)ずつ動かし、Esc で隠す。位置は枠の範囲内に収める。複製側とルーペは aria-hidden="true" にして、同じ文が二重に読み上げられないようにする。@media (prefers-reduced-motion: reduce) で出現の transition を無効化する。

Acceptance criteria: works with prefers-reduced-motion (animation disabled), zero console errors, zero layout shift

AIエージェントでの実装ガイド → Claude Code でWebアニメーションを実装する

BOOKS · PR · AMAZON

📖 なかしまぁ先生のCSSアニメーション入門 📖 UIデザインの教科書 新版 マルチデバイス時代のインターフェース設計 📖 Fundamentals of Web Animation with GSAP

※当サイトはAmazonアソシエイトプログラムの参加者です。リンクから商品をご購入いただくと、当方に紹介料が発生します。

学びを止めない · AMAZONサブスク

📚 Kindle Unlimited — 技術書・デザイン書が読み放題。30日無料体験 → 🎧 Audible — 移動中に耳で学ぶオーディオブック。30日無料体験 → 📦 Amazon Prime — Prime Reading・配送特典つき。30日無料体験 → 🎵 Music Unlimited — 作業用BGMに。1億曲が聴き放題・30日無料体験 →

※当サイトはAmazonアソシエイトプログラムの参加者です。リンクから商品をご購入いただくと、当方に紹介料が発生します。

RELATED EFFECTS

↗ MORE
→ CURSOR SPOTLIGHT REVEAL → LIGHTBOX GALLERY → BEFORE AFTER SLIDER

Lens(ルーペで拡大する演出)とは

マウスを乗せた場所だけを丸いルーペの中に拡大して見せる演出です。Magic UI と Aceternity UI のどちらにも同じ名前のコンポーネント(Lens)があり、公式の説明ではどちらも「画像や動画などを拡大して見られる部品」とされています。商品写真の細部や、図面・地図の細かい文字を見せたい時に使われます。

当サイトの Cursor Spotlight Reveal はカーソルの周りだけ隠れた内容を見せる演出、Lightbox Gallery はサムネイルを押すと拡大表示が開き前後に送れるギャラリー、Before After Slider は2枚の画像を見比べる部品です。本ページは元の表示を保ったまま一部だけを拡大する別テーマです。

拡大した内容はマウスでしか見られない、という作りにしないことが大切です。キーボードやタップでもルーペを動かせるようにし、拡大しないと読めない重要な情報は、ルーペの外でも読める形で載せておいてください。

よくある質問

ルーペの中身はどうやって拡大していますか?

元の内容を丸ごと複製してルーペの中に入れ、複製に scale で倍率をかけたうえで、マウスの下の点がルーペの中心に来るよう translate でずらしています。transform-origin を左上(0 0)にしておくと、「マウス位置 × 倍率」だけずらせば計算が合います。画像1枚だけなら background-image と background-position で拡大する方法もありますが、複製する方法なら文字や図形が混ざった内容もそのまま拡大できます。

スマホでは指でなぞって動かせないのですか?

指でなぞる動きを受け取り続けるには、touch-action などで標準のスクロールを止める指定が必要になり、そうすると枠の上ではページをスクロールしにくくなります。本実装はスクロールを妨げないことを優先し、タップした位置にルーペを置く方式にしています。拡大を主な目的にするページでは、専用の拡大表示(ライトボックスなど)を併用するほうが使いやすくなります。

コードは無料で使えますか?

はい。Motion LabのコードスニペットとAIプロンプトは、クレジット表記なしで自由に使えます。コピーしてそのままプロジェクトに利用できます。