【プロンプト有】CSSとJSで作る、マウスに追従する画像プレビューリスト

ホバー

【プロンプト有】CSSとJSで作る、マウスに追従する画像プレビューリスト

投稿日2026/04/20

更新日2026/4/18

単なるリスト表示でも、少しのインタラクションを加えることで印象は大きく変わります。
このサンプルでは、リスト項目にホバーすると対応する画像がマウスに追従して表示されるUIを実装しています。

画像はふわっと浮かび上がるように表示され、カーソルの動きに滑らかに追従することで、視覚的な楽しさと直感的な操作感を両立しています。ポートフォリオや作品一覧、記事リストなどに最適な表現です。

Preview プレビュー

Code コード

<main class="min-h-screen flex flex-col items-center justify-center">
	<div class="w-full max-w-4xl border-t border-slate-200">
		<div class="project-item group flex flex-col md:flex-row md:items-center justify-between py-10 px-6 border-b border-slate-200 cursor-pointer transition-colors duration-500 hover:bg-slate-50" data-image="https://images.unsplash.com/photo-1514924013411-cbf25faa35bb?auto=format&fit=crop&q=80&w=800">
			<div class="flex-1">
				<h2 class="text-2xl md:text-4xl font-light underline-animate inline-block">雨の日の街角</h2>
				<p class="text-slate-500 mt-2 text-sm md:text-base font-medium">濡れたアスファルトにネオンが反射する夜の都市風景</p>
			</div>
			<div class="mt-4 md:mt-0 flex items-center transform translate-x-[-10px] opacity-0 transition-all duration-300 group-hover:translate-x-0 group-hover:opacity-100">
				<span class="mr-4 text-sm font-semibold uppercase tracking-widest">View View</span>
				<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
					<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" />
				</svg>
			</div>
		</div>
		<div class="project-item group flex flex-col md:flex-row md:items-center justify-between py-10 px-6 border-b border-slate-200 cursor-pointer transition-colors duration-500 hover:bg-slate-50" data-image="https://images.unsplash.com/photo-1439066615861-d1af74d74000?auto=format&fit=crop&q=80&w=800">
			<div class="flex-1">
				<h2 class="text-2xl md:text-4xl font-light underline-animate inline-block">静かな湖畔</h2>
				<p class="text-slate-500 mt-2 text-sm md:text-base font-medium">風のない水面に空が映り込む穏やかな自然の情景</p>
			</div>
			<div class="mt-4 md:mt-0 flex items-center transform translate-x-[-10px] opacity-0 transition-all duration-300 group-hover:translate-x-0 group-hover:opacity-100">
				<span class="mr-4 text-sm font-semibold uppercase tracking-widest">View View</span>
				<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
					<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" />
				</svg>
			</div>
		</div>
		<div class="project-item group flex flex-col md:flex-row md:items-center justify-between py-10 px-6 border-b border-slate-200 cursor-pointer transition-colors duration-500 hover:bg-slate-50" data-image="https://images.unsplash.com/photo-1473580044384-7ba9967e16a0?auto=format&fit=crop&q=80&w=800">
			<div class="flex-1">
				<h2 class="text-2xl md:text-4xl font-light underline-animate inline-block">砂漠の夕暮れ</h2>
				<p class="text-slate-500 mt-2 text-sm md:text-base font-medium">広がる砂丘とオレンジ色に染まる地平線のコントラスト</p>
			</div>
			<div class="mt-4 md:mt-0 flex items-center transform translate-x-[-10px] opacity-0 transition-all duration-300 group-hover:translate-x-0 group-hover:opacity-100">
				<span class="mr-4 text-sm font-semibold uppercase tracking-widest">View View</span>
				<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
					<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" />
				</svg>
			</div>
		</div>
	</div>
</main>
<div id="preview-container">
	<img id="preview-image" src="" alt="Preview">
</div>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;500;700&display=swap');
body {
	font-family: 'Inter', sans-serif;
	background-color: #ffffff;
	overflow-x: hidden;
}
/* ホバー時の下線アニメーション */
.underline-animate {
	position: relative;
}
.underline-animate::after {
	content: '';
	position: absolute;
	width: 0;
	height: 2px;
	bottom: -2px;
	left: 0;
	background-color: currentColor;
	transition: width 0.4s cubic-bezier(0.25, 1, 0.5, 1);
}
.project-item:hover .underline-animate::after {
	width: 100%;
}
/* プレビューコンテナの初期状態 */
#preview-container {
	position: fixed;
	top: 0;
	left: 0;
	width: 320px;
	height: 200px;
	pointer-events: none;
	z-index: 50;
	transform: translate(-50%, -50%) scale(0.8);
	opacity: 0;
	filter: blur(20px);
	transition: transform 0.5s cubic-bezier(0.23, 1, 0.32, 1),
		opacity 0.4s ease,
		filter 0.4s ease;
	overflow: hidden;
	border-radius: 12px;
	box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
}
#preview-container.active {
	opacity: 1;
	filter: blur(0px);
	transform: translate(-50%, -50%) scale(1);
}
/* 画像のふわっとした切り替え */
#preview-image {
	width: 100%;
	height: 100%;
	object-fit: cover;
	transition: opacity 0.3s ease;
}
/* モバイル対応:マウス追従を無効化する代わりにリスト表示を調整 */
@media (max-pointer: fine) {
	#preview-container {
		display: none;
	}
}
const previewContainer = document.getElementById('preview-container');
const previewImage = document.getElementById('preview-image');
const projectItems = document.querySelectorAll('.project-item');
let targetX = 0;
let targetY = 0;
let currentX = 0;
let currentY = 0;
// マウス位置の更新
document.addEventListener('mousemove', (e) => {
	targetX = e.clientX;
	targetY = e.clientY;
});
// 慣性(スムースな追従)アニメーション
function animate() {
	currentX += (targetX - currentX) * 0.15;
	currentY += (targetY - currentY) * 0.15;
	previewContainer.style.left = `${currentX}px`;
	previewContainer.style.top = `${currentY}px`;
	requestAnimationFrame(animate);
}
animate();
// 各項目へのイベント登録
projectItems.forEach(item => {
	item.addEventListener('mouseenter', () => {
		const imageUrl = item.getAttribute('data-image');
		// 画像をふわっと切り替える
		previewImage.style.opacity = '0';
		setTimeout(() => {
			previewImage.src = imageUrl;
			previewImage.style.opacity = '1';
		}, 50);
		previewContainer.classList.add('active');
	});
	item.addEventListener('mouseleave', () => {
		previewContainer.classList.remove('active');
	});
});
// タッチデバイスでの配慮
document.addEventListener('touchstart', () => {
	previewContainer.style.display = 'none';
}, {
	passive: true
});
https://cdn.tailwindcss.com

Explanation 詳しい説明

基本構造

このUIは「リスト要素」と「プレビュー画像コンテナ」で構成されています。リスト項目にはそれぞれ表示する画像URLを data 属性として持たせ、JavaScriptで対応する画像要素を事前に生成してコンテナ内に配置しています。

画像は position: fixed のコンテナ内に配置されており、通常は非表示状態にしておき、ホバー時のみ表示を切り替えます。

仕様

マウスの位置は mousemove イベントで取得し、直接位置を反映するのではなく、現在位置から目標位置へ少しずつ近づける補間(lerp)処理を行っています。これにより、遅延のある滑らかな追従アニメーションが実現されています。

また、ホバーしたリスト項目ごとに対応する画像を切り替え、opacity のトランジションでフェード表示しています。コンテナ自体には scale と blur を組み合わせたトランジションを適用し、浮かび上がるような演出を加えています。

カスタム

画像サイズはプレビューコンテナの width と height を変更することで調整可能です。追従の滑らかさは補間処理の speed の値を変更することで調整できます。値を大きくすると追従が速くなり、小さくするとゆったりした動きになります。

ホバー時の演出は、scale や blur の値を変更することで印象を変えられます。より強い演出にしたい場合は影(box-shadow)や回転(rotate)を追加するのも効果的です。

また、リスト項目にリンクを設定することで、そのままナビゲーションUIとしても利用できます。

プロンプトについて

本記事のコードはGoogle AI Studioでプロンプトから生成しています。

一度でうまくいかない場合でも、何度か試したりプロンプトを調整することで、よりきれいなUIを作る可能性があります。

注意点

mousemove を常時監視しているため、パフォーマンスに配慮して requestAnimationFrame 内で処理を行っています。この構造は削除しないようにしてください。

画像をすべて事前に生成しているため、枚数が多い場合は読み込み負荷が高くなる可能性があります。必要に応じて遅延読み込みや枚数制限を検討してください。

position: fixed を使用しているため、モバイル環境では挙動が異なる場合があります。タッチデバイスでは hover が使えないため、クリックやタップに置き換える実装が必要です。

また、ポインター位置に依存するUIのため、アクセシビリティ対応としてキーボード操作時の代替表示も検討するとより実用的になります。

Prompt プロンプト

あなたはHTML生成用に最適化するAIです。

以下の仕様をもとに、見た目と動きが再現されたUIをHTML / CSS / JavaScriptで作成してください。

UI仕様:
・プロジェクト一覧(タイトル・説明)を縦に並べる
・各項目にホバーすると背景がハイライトされる
・ホバー中の項目に応じて、画像プレビューがマウス位置に追従して表示される
・画像はふわっと切り替わり、非表示時はぼかしがかかる
・タイトルにはホバー時に下線アニメーションが入る
・矢印アイコンがホバー時にスライドして表示される
・スマートフォン・タブレット・PCでレイアウトが崩れないレスポンシブ対応を行う

データ内容:
・朝焼けの風景:山の稜線から昇る太陽とやわらかな光のグラデーション
・雨の日の街角:濡れたアスファルトにネオンが反射する夜の都市風景
・静かな湖畔:風のない水面に空が映り込む穏やかな自然の情景
・砂漠の夕暮れ:広がる砂丘とオレンジ色に染まる地平線のコントラスト

実装要件:
・HTML / CSS / JavaScriptのみで実装すること
・Tailwind CSSはCDNで読み込むこと
・アイコンはSVGで直接記述すること
・マウスに追従する画像プレビューを実装すること
・ホバー時のアニメーション(フェード・スライド・下線)を再現すること
・状態管理やアニメーションはすべてJavaScriptで実装すること
・レスポンシブ対応を行い、画面サイズに応じてレイアウトやUIを調整すること
・画像はUnsplashのURLを使用すること

禁止事項:
・フレームワークの使用
・npmやパッケージ導入の説明
・複数ファイルへの分割
・外部JavaScriptライブラリに依存すること

出力形式:
・index.html のコードのみを出力すること
・説明や補足は出力しないこと