ビジュアル
CSSだけで作る、サムネをクリックすると大きな画像が切り替わるギャラリー
2026/02/19
2026/2/11
サムネを押したときに、大きな画像が気持ちよく切り替わるギャラリーをCSSだけで作りたい。
このサンプルは、クリックしたサムネ(:target)の画像をanchor-positioningで大きな画像の表示エリアにぴったり合わせて配置し、別要素なのに同じ場所へ収まる“入れ替え”表現を作っています。
Preview プレビュー
Code コード
<div class="kumonosu-gallery">
<div class="kumonosu-main-img">
<h1>Image swap using <code>anchor-positioning</code></h1>
</div>
<a href="#kumonosu-item-1" id="kumonosu-item-1">
<img src="https://picsum.photos/400/400?random=1" alt="Title 1">
<h2>Title 1</h2>
</a>
<a href="#kumonosu-item-2" id="kumonosu-item-2">
<img src="https://picsum.photos/400/400?random=2" alt="Title 2">
<h2>Title 2</h2>
</a>
<a href="#kumonosu-item-3" id="kumonosu-item-3">
<img src="https://picsum.photos/400/400?random=3" alt="Title 3">
<h2>Title 3</h2>
</a>
<a href="#kumonosu-item-4" id="kumonosu-item-4">
<img src="https://picsum.photos/400/400?random=4" alt="Title 4">
<h2>Title 4</h2>
</a>
<a href="#kumonosu-item-5" id="kumonosu-item-5">
<img src="https://picsum.photos/400/400?random=5" alt="Title 5">
<h2>Title 5</h2>
</a>
<a href="#kumonosu-item-6" id="kumonosu-item-6">
<img src="https://picsum.photos/400/400?random=6" alt="Title 6">
<h2>Title 6</h2>
</a>
<a href="#kumonosu-item-7" id="kumonosu-item-7">
<img src="https://picsum.photos/400/400?random=7" alt="Title 7">
<h2>Title 7</h2>
</a>
<a href="#kumonosu-item-8" id="kumonosu-item-8">
<img src="https://picsum.photos/400/400?random=8" alt="Title 8">
<h2>Title 8</h2>
</a>
<a href="#kumonosu-item-9" id="kumonosu-item-9">
<img src="https://picsum.photos/400/400?random=9" alt="Title 9">
<h2>Title 9</h2>
</a>
</div>
@import url(https://fonts.bunny.net/css?family=jura:300,500);
/* --- 基本設定 --- */
* {
box-sizing: border-box;
}
:root {
color-scheme: light dark;
--bg-dark: rgb(12, 10, 9);
--bg-light: rgb(248, 244, 238);
--txt-light: rgb(10, 10, 10);
--txt-dark: rgb(245, 245, 245);
--line-light: rgba(0 0 0 / .25);
--line-dark: rgba(255 255 255 / .25);
--clr-bg: light-dark(var(--bg-light), var(--bg-dark));
--clr-txt: light-dark(var(--txt-light), var(--txt-dark));
--clr-lines: light-dark(var(--line-light), var(--line-dark));
}
body {
background-color: var(--clr-bg);
color: var(--clr-txt);
min-height: 100svh;
margin: 0;
padding: 2rem;
font-family: "Jura", sans-serif;
font-size: 1rem;
line-height: 1.5;
display: grid;
place-items: center;
gap: 2rem;
}
img {
max-width: 100%;
display: block;
}
@supports not (position-anchor: --test) {
body::before {
content: "Sorry, your browser doesn't support anchor-positioning. Please use Chrome 125+ or Edge 125+.";
position: fixed;
top: 0;
left: 0;
width: 100%;
background: #ff4757;
color: white;
text-align: center;
padding: 0.5rem;
font-size: 0.8rem;
z-index: 100;
}
}
/* --- ギャラリーのスタイル --- */
.kumonosu-gallery {
--size: 100%;
--grid-cols: repeat(3, 1fr);
--grid-rows: repeat(6, 1fr);
--gap: .5rem;
--main-image-columns: 1 / -1;
--main-image-rows: 1 / span 3;
--main-img-w: 100%;
--main-img-h: auto;
width: min(100%, 900px);
display: grid;
grid-template-columns: var(--grid-cols);
grid-template-rows: var(--grid-rows);
gap: var(--gap);
position: relative;
}
@media (width > 600px) {
body .kumonosu-gallery {
--size: 120px;
--grid-cols: repeat(6, 1fr);
--grid-rows: repeat(3, 1fr);
--main-image-columns: 4 / -1;
--main-image-rows: 1 / span 3;
--main-img-w: auto;
--main-img-h: 100%;
}
}
.kumonosu-gallery>.kumonosu-main-img {
position: relative;
grid-column: var(--main-image-columns);
grid-row: var(--main-image-rows);
display: grid;
place-content: center;
padding: 1em;
font-size: 2rem;
anchor-name: --main-img;
overflow: hidden;
border-image: fill 0 linear-gradient(transparent 50%, black);
}
.kumonosu-gallery>.kumonosu-main-img>h1 {
margin: 0;
font-size: clamp(1rem, 2.5vw + 0.45rem, 1.2rem);
text-align: center;
}
.kumonosu-gallery>.kumonosu-main-img:has(~:target) h1 {
display: none;
}
.kumonosu-gallery>a {
display: block;
aspect-ratio: 1;
overflow: hidden;
text-decoration: none;
}
.kumonosu-gallery>a:hover:not(:target) img {
opacity: .5;
}
.kumonosu-gallery>a>img {
width: 100%;
height: 100%;
object-fit: cover;
transition: opacity 300ms ease-in-out;
}
.kumonosu-gallery>a:target img {
position: absolute;
inset: anchor(--main-img top) anchor(--main-img right) anchor(--main-img bottom) anchor(--main-img left);
position-anchor: --main-img;
width: var(--main-img-w, 0);
height: var(--main-img-h, 0);
animation: --kumonosu-fade-in 750ms ease-in-out;
z-index: -1;
}
.kumonosu-gallery>a>h2 {
color: white;
font-size: 1.2rem;
font-weight: 300;
opacity: 0;
translate: 0 1lh;
transition: all 300ms ease-in-out;
margin: 0;
pointer-events: none;
}
.kumonosu-gallery>a:target h2 {
position: absolute;
inset: auto;
bottom: anchor(--main-img bottom);
left: calc(anchor(--main-img left) + 1rem);
position-anchor: --main-img;
opacity: 1;
translate: 0 -1rem;
}
@keyframes --kumonosu-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
Explanation 詳しい説明
仕様
全体はCSS Gridでレイアウトし、大きな画像の表示エリア(.main-img)にanchor-name: --main-imgを設定して基準にしています。
サムネはa要素で、クリックするとURLのハッシュが変わり、その要素が:targetになります。
:targetになったサムネのimgだけをposition: absoluteに切り替え、inset: anchor(...)で大きな画像の表示エリアの四辺に合わせて配置します。
見た目としては、サムネが大きな画像の場所に“切り替わった”ように見えます(実際はアンカー基準で再配置している仕組みです)。表示のつながりはフェードインアニメーションで補っています。
タイトル(h2)も同様に、:target時だけbottom: anchor(--main-img bottom)などで表示エリアに追従させています。
初期説明のh1は、どれかが:targetになると非表示にして、画像の切り替えに集中できるようにしています。
- 大きな画像の表示エリアに
anchor-nameを付けて基準化 - サムネは
:targetで選択状態を作る :target imgをアンカー位置へabsolute配置して大きな画像を切り替えるh2もアンカーに追従して表示する
カスタム
カスタムは「大きな画像の表示エリアの位置」と「切り替えの印象」を触るのが最優先です。レスポンシブはCSS変数で切り替えているので、レイアウト変更も破綻しにくい構成です。
- グリッド構成:
.galleryの--grid-cols/--grid-rows - 大きな画像の表示エリアの場所:
--main-image-columns/--main-image-rows - 大きな画像の収まり:
--main-img-w/--main-img-h - 余白と密度:
--gap、.main-imgのpadding - 切り替えの印象:
@keyframes --fade-inの時間・イージング
タイトルの位置は、:target h2のleft/bottom/translateを調整すると作りやすいです。
注意点
この実装はanchor-positioning(position-anchor / anchor())前提のため、未対応ブラウザでは同じ切り替えになりません。コード内では@supports not (position-anchor: --test)で注意表示を出していますが、実運用では対応ブラウザを明示するのが安全です。
また、切り替えは:target=URLハッシュ依存なので、クリックするとURL末尾が変わります。共有できるメリットがある一方、同一ページ内で他のハッシュ遷移を使っている場合はIDの衝突に注意が必要です。
anchor-positioning未対応環境では動作しない(ブラウザ要件が必要):targetなのでURLハッシュが変わる(他のアンカーと干渉注意)- 表示エリアの
overflowや背景(マスク)設定で見え方が変わる