アニメーション
CSSとJSで作る、写真が斜めに切り替わるスライダー
2026/10/13
2026/10/12
写真が斜めにスライドしながら切り替わり、選択した写真が中央に大きく表示されるフォトスライダーです。
一般的な横方向のスライダーとは異なり、写真が斜め方向へ流れるように移動するのが特徴です。CSSのtransformとtransitionを組み合わせ、位置の移動や拡大縮小を滑らかにアニメーションさせています。
左右の矢印ボタンやドットナビゲーションに加え、写真を直接クリックして切り替えることも可能です。ポートフォリオやフォトギャラリーなど、写真を印象的に見せたい場面に活用できます。
Code コード
<div class="kumonosu-wrapper">
<div id="kumonosu-app" class="kumonosu-carousel">
<div class="kumonosu-window">
<div class="kumonosu-track">
<div class="kumonosu-slide" style="transform: translateY(-150%) rotate(-90deg) scale(0.6)"><img class="kumonosu-image" src="https://kumonosu.net/wp-content/uploads/2026/01/260114a.jpeg.webp" alt="画像1" data-index="0" tabindex="0" role="button" aria-label="画像1を表示" draggable="false"></div>
<div class="kumonosu-slide" style="transform: translateY(-100%) rotate(-60deg) scale(0.6)"><img class="kumonosu-image" src="https://kumonosu.net/wp-content/uploads/2026/01/260114b.jpeg.webp" alt="画像2" data-index="1" tabindex="0" role="button" aria-label="画像2を表示" draggable="false"></div>
<div class="kumonosu-slide" style="transform: translateY(-50%) rotate(-30deg) scale(0.6)"><img class="kumonosu-image" src="https://kumonosu.net/wp-content/uploads/2026/01/260114c.jpeg.webp" alt="画像3" data-index="2" tabindex="0" role="button" aria-label="画像3を表示" draggable="false"></div>
<div class="kumonosu-slide" style="transform: translateY(0%) rotate(0deg) scale(1)"><img class="kumonosu-image" src="https://kumonosu.net/wp-content/uploads/2026/01/176a.jpg.webp" alt="画像4" data-index="3" tabindex="0" role="button" aria-label="画像4を表示" draggable="false"></div>
<div class="kumonosu-slide" style="transform: translateY(50%) rotate(30deg) scale(0.6)"><img class="kumonosu-image" src="https://kumonosu.net/wp-content/uploads/2026/01/176b.jpg.webp" alt="画像5" data-index="4" tabindex="0" role="button" aria-label="画像5を表示" draggable="false"></div>
<div class="kumonosu-slide" style="transform: translateY(100%) rotate(60deg) scale(0.6)"><img class="kumonosu-image" src="https://kumonosu.net/wp-content/uploads/2026/01/176d.jpg.webp" alt="画像6" data-index="5" tabindex="0" role="button" aria-label="画像6を表示" draggable="false"></div>
<div class="kumonosu-slide" style="transform: translateY(150%) rotate(90deg) scale(0.6)"><img class="kumonosu-image" src="https://kumonosu.net/wp-content/uploads/2026/01/176c.jpg.webp" alt="画像7" data-index="6" tabindex="0" role="button" aria-label="画像7を表示" draggable="false"></div>
<div class="kumonosu-slide" style="transform: translateY(200%) rotate(120deg) scale(0.6)"><img class="kumonosu-image" src="https://kumonosu.net/wp-content/uploads/2026/01/176e.jpg.webp" alt="画像8" data-index="7" tabindex="0" role="button" aria-label="画像8を表示" draggable="false"></div>
<div class="kumonosu-slide" style="transform: translateY(250%) rotate(150deg) scale(0.6)"><img class="kumonosu-image" src="https://kumonosu.net/wp-content/uploads/2026/01/176f.jpg.webp" alt="画像9" data-index="8" tabindex="0" role="button" aria-label="画像9を表示" draggable="false"></div>
<div class="kumonosu-slide" style="transform: translateY(300%) rotate(180deg) scale(0.6)"><img class="kumonosu-image" src="https://kumonosu.net/wp-content/uploads/2026/01/176g.jpg.webp" alt="画像10" data-index="9" tabindex="0" role="button" aria-label="画像10を表示" draggable="false"></div>
</div>
</div>
<div class="kumonosu-controls"><button class="kumonosu-arrow" type="button" data-direction="prev" aria-label="前の画像">‹</button>
<div class="kumonosu-dots"><button class="kumonosu-dot" type="button" data-index="0" aria-label="画像1を表示" aria-current="false"></button><button class="kumonosu-dot" type="button" data-index="1" aria-label="画像2を表示" aria-current="false"></button><button class="kumonosu-dot" type="button" data-index="2" aria-label="画像3を表示" aria-current="false"></button><button class="kumonosu-dot kumonosu-active" type="button" data-index="3" aria-label="画像4を表示" aria-current="true"></button><button class="kumonosu-dot" type="button" data-index="4" aria-label="画像5を表示" aria-current="false"></button><button class="kumonosu-dot" type="button" data-index="5" aria-label="画像6を表示" aria-current="false"></button><button class="kumonosu-dot" type="button" data-index="6" aria-label="画像7を表示" aria-current="false"></button><button class="kumonosu-dot" type="button" data-index="7" aria-label="画像8を表示" aria-current="false"></button><button class="kumonosu-dot" type="button" data-index="8" aria-label="画像9を表示" aria-current="false"></button><button class="kumonosu-dot" type="button" data-index="9" aria-label="画像10を表示" aria-current="false"></button></div><button class="kumonosu-arrow" type="button" data-direction="next" aria-label="次の画像">›</button>
</div>
</div>
</div>
.kumonosu-wrapper, .kumonosu-wrapper * {
box-sizing: border-box;
}
.kumonosu-wrapper {
position: fixed;
inset: 0;
min-height: 100vh;
background: #ececec;
display: grid;
place-content: center;
overflow: hidden;
}
.kumonosu-carousel {
color: #262626;
user-select: none;
}
.kumonosu-window {
width: clamp(120px, 80vmin, 300px);
margin-top: 2rem;
}
.kumonosu-track {
display: flex;
width: max-content;
}
.kumonosu-slide {
width: clamp(120px, 80vmin, 300px);
aspect-ratio: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: .5rem;
will-change: transform;
}
.kumonosu-image {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 1rem;
cursor: pointer;
}
.kumonosu-controls {
position: fixed;
bottom: max(1rem, env(safe-area-inset-bottom));
left: 0;
right: 0;
width: max-content;
max-width: 100%;
padding: 0 .5rem;
margin: auto;
display: flex;
align-items: center;
gap: 1rem;
justify-content: center;
color: #404040;
border-radius: 999px;
background: #e5e5e580;
backdrop-filter: blur(4px);
border: 1px solid #e5e5e5cc;
box-shadow: 0 1px 3px #0000001a;
}
.kumonosu-arrow {
padding: .5rem;
cursor: pointer;
border: 0;
background: transparent;
color: inherit;
display: grid;
place-items: center;
}
.kumonosu-dots {
width: 180px;
display: flex;
justify-content: center;
align-items: center;
gap: .5rem;
}
.kumonosu-dot {
border-radius: 999px;
cursor: pointer;
height: .5rem;
width: .5rem;
background: #4040404d;
transition: width .3s, background-color .3s;
}
.kumonosu-dot.kumonosu-active {
width: 1.75rem;
background: currentColor;
}
.kumonosu-track {
transform: translateX(-30%);
transition: transform .8s cubic-bezier(.22, 1, .36, 1);
}
.kumonosu-slide {
flex-shrink: 0;
transition: transform .8s cubic-bezier(.22, 1.15, .36, 1);
}
.kumonosu-image {
flex-shrink: 0;
background: #d4d4d4;
}
.kumonosu-dot {
border: 0;
padding: 0;
flex-shrink: 0;
}
.kumonosu-arrow {
font-size: 24px;
width: 40px;
height: 40px;
}
.kumonosu-arrow:disabled {
opacity: .3;
cursor: default;
}
.kumonosu-controls {
z-index: 10;
}
@media (prefers-reduced-motion: reduce) {
.kumonosu-track, .kumonosu-slide, .kumonosu-dot {
transition: none;
}
}
(() => {
const app = document.getElementById('kumonosu-app');
const track = app.querySelector('.kumonosu-track');
const slides = [...app.querySelectorAll('.kumonosu-slide')];
const dots = [...app.querySelectorAll('.kumonosu-dot')];
const prev = app.querySelector('[data-direction="prev"]');
const next = app.querySelector('[data-direction="next"]');
let activeIndex = 3;
function show(index) {
activeIndex = Math.max(0, Math.min(slides.length - 1, index));
track.style.transform = `translateX(${-activeIndex * 100 / slides.length}%)`;
slides.forEach((slide, i) => {
const offset = i - activeIndex;
slide.style.transform = `translateY(${offset * 50}%) rotate(${offset * 30}deg) scale(${offset === 0 ? 1 : .6})`;
dots[i].classList.toggle('kumonosu-active', offset === 0);
dots[i].setAttribute('aria-current', String(offset === 0));
});
prev.disabled = activeIndex === 0;
next.disabled = activeIndex === slides.length - 1;
}
app.addEventListener('click', event => {
const target = event.target.closest('[data-index]');
if (target) show(Number(target.dataset.index));
});
app.addEventListener('keydown', event => {
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
event.preventDefault();
show(activeIndex + (event.key === 'ArrowLeft' ? -1 : 1));
} else if ((event.key === 'Enter' || event.key === ' ') && event.target.matches('.kumonosu-image')) {
event.preventDefault();
show(Number(event.target.dataset.index));
}
});
prev.addEventListener('click', () => show(activeIndex - 1));
next.addEventListener('click', () => show(activeIndex + 1));
show(activeIndex);
})();
Explanation 詳しい説明
基本構造
複数の写真を横並びに配置したトラックを、JavaScriptで移動させるスライダーです。
HTMLでは.kumonosu-trackの中に10枚の写真を配置し、それぞれを.kumonosu-slideで囲んでいます。
スライドの切り替え時には、トラック全体を横方向へ移動させると同時に、各写真の上下位置やサイズを変更しています。
これにより、写真が斜め方向へスライドしながら切り替わり、選択した写真が中央に大きく表示されるアニメーションを実現しています。
外部ライブラリは使用せず、CSSとJavaScriptのみで実装しています。
スライダーの動き
スライダーの中心となるのは、JavaScriptのshow()関数です。
現在選択されている写真の番号をactiveIndexで管理し、その値をもとに各スライドの位置やサイズを計算しています。
選択中の写真は中央に大きく表示され、それ以外の写真は上下にずれた位置へ配置されます。
スライドを切り替えると、トラック全体が横方向へ移動し、各写真も上下方向へ移動します。
この2つの動きを組み合わせることで、写真が斜め方向へ流れるような切り替えを実現しています。
さらに、選択中の写真は元のサイズで表示され、それ以外の写真は60%に縮小されます。
CSSのtransitionによって位置とサイズの変化を滑らかにつなぎ、自然な切り替えアニメーションに仕上げています。
操作方法
スライダーは以下の操作に対応しています。
・左右の矢印ボタンで前後の写真へ移動
・ドットナビゲーションで任意の写真へ移動
・写真をクリックして選択
・キーボードの左右矢印キーで切り替え
・フォーカスした写真をEnterキーまたはスペースキーで選択
最初と最後の写真では、それ以上移動できない方向の矢印ボタンが無効になります。
また、現在選択されている写真に対応するドットは横長に変化し、どの写真が表示されているかを確認できます。
カスタム方法
写真の切り替え方は、JavaScriptのshow()関数から変更できます。
translateY(${offset * 50}%)の数値を変更すると、写真が上下に移動する距離を調整できます。
上下の移動距離を大きくすると斜め方向への動きが強調され、小さくすると一般的な横スライダーに近い動きになります。
rotate(${offset * 30}deg)では写真の傾きを設定しています。現在は選択中の写真を0度とし、ほかの写真は位置に応じて傾きを加えています。
scale(${offset === 0 ? 1 : .6})では、選択中の写真とそれ以外の写真の大きさを設定できます。
アニメーションの速度は、CSSの.kumonosu-trackと.kumonosu-slideに設定されたtransitionから変更できます。
現在は0.8秒に設定されているため、短くすると素早く切り替わり、長くするとゆっくりとした動きになります。
写真のサイズはclamp(120px, 80vmin, 300px)で指定されています。最小・最大サイズを変更することで、表示する写真の大きさを調整できます。
初期表示する写真は、JavaScriptのactiveIndexから変更できます。現在は3が設定されているため、4枚目の写真が最初に表示されます。
注意点
このスライダーは、一般的な横方向へ流れるスライダーとは異なり、トラックの横移動と各写真の上下移動を組み合わせています。
そのため、写真の枚数を大幅に増やす場合は、上下方向の移動距離によって写真が画面外へ大きくはみ出す可能性があります。
また、写真の枚数を変更する場合は、HTMLのスライドだけでなく、対応するドットナビゲーションも追加・削除する必要があります。
スライダーの幅は画面サイズに応じて調整されますが、スワイプやドラッグによる切り替えには対応していません。スマートフォンでは写真のタップや矢印ボタン、ドットナビゲーションで操作する仕様です。
なお、prefers-reduced-motionにも対応しており、動きを抑える設定をしている環境では切り替えアニメーションが無効になります。