CSSだけで作る、円を描くように並ぶスライド画像ギャラリー

スクロール

CSSだけで作る、円を描くように並ぶスライド画像ギャラリー

投稿日2026/02/05

更新日2026/2/1

画像ギャラリーは、ただ並べるだけでも成立しますが、「どれが今見られているのか」を自然に伝えるのは意外と難しいものです。

本デモでは、cssだけを使い、画像が円を描くように並びながらスライドしていく体験型の画像ギャラリーを実装しました。

スクロールするだけで視点が移動し、その瞬間に注目される1枚が直感的に伝わる構成になっています。

Preview プレビュー

Code コード

<!-- class="wrapper" -> class="kumonosu-wrapper" -->
<section class="kumonosu-wrapper">
	<div><img src="https://picsum.photos/id/634/1200/1200"></div>
	<div><img src="https://picsum.photos/id/228/1200/1200"></div>
	<div><img src="https://picsum.photos/id/661/1200/1200"></div>
	<div><img src="https://picsum.photos/id/380/1200/1200"></div>
	<div><img src="https://picsum.photos/id/392/1200/1200"></div>
	<div><img src="https://picsum.photos/id/238/1200/1200"></div>
	<div><img src="https://picsum.photos/id/469/1200/1200"></div>
	<div><img src="https://picsum.photos/id/311/1200/1200"></div>
	<div><img src="https://picsum.photos/id/515/1200/1200"></div>
	<div><img src="https://picsum.photos/id/521/1200/1200"></div>
	<div><img src="https://picsum.photos/id/549/1200/1200"></div>
	<div><img src="https://picsum.photos/id/178/1200/1200"></div>
	<div><img src="https://picsum.photos/id/637/1200/1200"></div>
	<div><img src="https://picsum.photos/id/641/1200/1200"></div>
	<div><img src="https://picsum.photos/id/669/1200/1200"></div>
	<div><img src="https://picsum.photos/id/685/1200/1200"></div>
	<div><img src="https://picsum.photos/id/505/1200/1200"></div>
	<div><img src="https://picsum.photos/id/699/1200/1200"></div>
	<div><img src="https://picsum.photos/id/513/1200/1200"></div>
	<div><img src="https://picsum.photos/id/773/1200/1200"></div>
</section>
<!-- class="icon" -> class="kumonosu-icon" -->
<div class="kumonosu-icon">
	<!-- class="mouse" -> class="kumonosu-mouse" -->
	<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 24 40" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="kumonosu-mouse">
		<path stroke="none" d="M0 0h24v24H0z" fill="none" />
		<path d="M6 3m0 4a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v10a4 4 0 0 1 -4 4h-4a4 4 0 0 1 -4 -4z" />
		<path d="M12 7l0 4" />
		<path d="M8 26l4 4l4 -4">
			<animateTransform attributeType="XML" attributeName="transform" type="translate" values="0 0; 0 4; 0 0" dur="1s" repeatCount="indefinite" />
		</path>
	</svg>
</div>
@layer base, mouse, demo;
@layer demo {
	@property --rotate {
		syntax: "<number>";
		inherits: true;
		initial-value: 0;
	}
	body {
		height: 1200svh;
		margin: 0;
		animation: --page-rotate 1s linear;
		animation-timeline: scroll(nearest block);
		--cards: 20;
		animation-timing-function: steps(var(--cards));
	}
	@keyframes --page-rotate {
		to {
			--rotate: 1;
		}
	}
	.kumonosu-wrapper {
		--card-border-radius: 14px;
		--cards: sibling-count();
		--card-width: max(150px, 20vw);
		--card-height: calc(var(--card-width) * 6 / 4);
		/* 半径の設定 */
		--radius: calc(var(--card-width) * var(--cards) / (2 * 3.1416));
		position: fixed;
		width: calc(var(--radius) * 2);
		height: calc(var(--radius) * 2);
		/* 中央配置の調整 */
		top: calc(50% + var(--radius) + var(--card-height) * 2);
		left: 50%;
		transform-origin: center center;
		transform: translateX(-50%) rotate(calc(var(--rotate) * 360deg));
		transition: transform 300ms linear;
	}
	.kumonosu-wrapper>div {
		--card-i: sibling-index();
		/* 円形配置 */
		--card-offset-radius: circle(var(--radius) at 50% 50%);
		--card-offset-distance: calc((var(--card-i) - 1) / var(--cards) * 100%);
		/* 現在位置の計算 */
		--card-phase: calc((var(--card-i) - 1) / var(--cards) - 0.75);
		--card-pos: mod(calc(var(--card-phase) + var(--rotate) + 1), 1);
		--card-dist: min(var(--card-pos), calc(1 - var(--card-pos)));
		--card-grayscale: clamp(0, calc(var(--card-dist) * var(--cards)), 1);
		--card-opacity: calc(1 - (var(--card-dist) / 0.15));
		/* ぼかし */
		--card-focus-range: .1;
		--card-max-blur: 7px;
		--card-norm-dist: min(var(--card-dist), var(--card-focus-range));
		--card-blur-progress: calc(var(--card-norm-dist) / var(--card-focus-range));
		--card-blur: calc(var(--card-blur-progress) * var(--card-max-blur));
		filter: blur(var(--card-blur)) grayscale(var(--card-grayscale));
		opacity: var(--card-opacity);
		container: size;
		offset-path: var(--card-offset-radius);
		offset-distance: var(--card-offset-distance);
		offset-rotate: auto;
		offset-anchor: 50% 100%;
		position: absolute;
		width: var(--card-width);
		aspect-ratio: 4/6;
		object-fit: cover;
		border-radius: var(--card-border-radius);
		transition: all 300ms ease-in-out;
		transform-origin: center calc(var(--card-height) * 2 * -1);
	}
	.kumonosu-wrapper>div>img {
		width: 100%;
		height: 100%;
		object-fit: cover;
		border-radius: inherit;
	}
}
@layer mouse {
	.kumonosu-mouse {
		position: fixed;
		bottom: 1rem;
		left: 50%;
		translate: -50% 0;
		display: none;
		width: 50px;
		height: 50px;
		opacity: 1;
		color: var(--clr-txt);
		animation-name: mouse;
		animation-duration: 1s;
		animation-timing-function: linear;
		animation-fill-mode: forwards;
		animation-timeline: scroll(nearest block);
	}
	@supports (animation-timeline: scroll()) {
		.kumonosu-mouse {
			display: block;
		}
	}
	@keyframes mouse {
		75% {
			opacity: 1;
		}
		100% {
			opacity: 0;
		}
	}
}
@layer base {
	* {
		box-sizing: border-box;
	}
	:root {
		color-scheme: light dark;
		--bg-dark: rgb(16, 24, 40);
		--bg-light: rgb(248, 244, 238);
		--txt-light: rgb(10, 10, 10);
		--txt-dark: rgb(245, 245, 245);
		--clr-bg: light-dark(var(--bg-light), var(--bg-dark));
		--clr-txt: light-dark(var(--txt-light), var(--txt-dark));
	}
	body {
		background-color: var(--clr-bg);
		color: var(--clr-txt);
		min-height: 100svh;
		margin: 0;
		display: grid;
		place-items: center;
		overflow-x: hidden;
	}
}

Explanation 詳しい説明

仕様

このギャラリーは、JavaScriptを使用せず、HTMLとCSSのみで構成されています。

複数の画像は円形のパス上に配置されており、ページのスクロール量に応じて全体が回転することで、画像がスライドしていくように見える仕組みになっています。

スクロール位置はCSSアニメーションとして扱われ、レイアウト全体の回転量に変換されています。

スライドの挙動

スクロールすると、以下の変化が同時に起こります。

  • 画像全体が円を描くように回転し、順番にスライドする
  • 中央付近に来た画像は
    ・ぼかしが弱くなる
    ・彩度と不透明度が上がる
  • 周囲の画像は
    ・徐々にぼかされ
    ・控えめな存在感になる

これにより、「今どの画像が表示されているのか」が操作せずとも自然に理解できる設計になっています。

表現のポイント

本実装では、z-index の切り替えやJavaScriptによる状態管理を行わず、位置・ぼかし・不透明度の変化だけでスライド感を表現しています。

また、スクロールを段階的に区切ることで、スムーズさの中に「カチッ」とした切り替わり感を持たせ、ギャラリーとして心地よい操作感を演出しています。

カスタマイズ

CSSカスタムプロパティを調整することで、見た目や挙動を柔軟に変更できます。

  • 画像サイズ
    --card-width を変更すると、スライド画像の大きさが変わります。
  • 円の大きさ
    --radius を調整すると、画像が描く円のサイズが変わります。
  • スライドの切り替わり感
    スクロールのステップ数を変更すると、画像が切り替わる間隔を調整できます。
  • ぼかしの強さ
    最大ブラー量を変更することで、中央画像の強調度を調整できます。

注意点

本デモでは、以下の比較的新しいCSS仕様を使用しています。

  • スクロール連動アニメーション
  • @property
  • offset-path
  • sibling-index()

そのため、対応していないブラウザでは正しく表示されない場合があります。

実際のプロジェクトで使用する場合は、対応ブラウザの確認やフォールバック表現の検討をおすすめします。

まとめ

cssだけでも、レイアウトとアニメーションを工夫することで、動きのあるスライドギャラリーを実現できます。

本デモは、画像を「並べる」のではなく体験として見せるためのひとつの実装例です。