HTML / CSS / JS
アニメーション
2026/02/12
2026/2/6
JavaScriptなしで、操作できるスライダーを作りたい。
このサンプルは、ラジオボタンと label を使って、CSSだけで画像を切り替えるカード型スライダーです。
画像は重なった状態から前面へ移動し、めくれるような動きで切り替わります。
シンプルな構造ながら、視覚的に印象に残る演出を加えています。
<div class="kumonosu-slider-layout">
<div class="kumonosu-cards">
<!-- カード 1 -->
<input type="radio" id="kumonosu-radio-1" name="radio-card" checked>
<article class="kumonosu-card" style="--angle:4deg">
<img class="kumonosu-card-img" src="https://picsum.photos/id/10/400/400" alt="風景1">
<div class="kumonosu-card-data">
<span class="kumonosu-card-num">1 / 7</span>
<h2>静寂の湖畔</h2>
<p>朝霧に包まれた静かな湖。澄んだ空気と穏やかな水面が、訪れる人の心を落ち着かせてくれます。</p>
<div class="kumonosu-card-footer">
<label for="kumonosu-radio-7" aria-label="前へ">❮</label>
<label for="kumonosu-radio-2" aria-label="次へ">❯</label>
</div>
</div>
</article>
<!-- カード 2 -->
<input type="radio" id="kumonosu-radio-2" name="radio-card">
<article class="kumonosu-card" style="--angle:-8deg">
<img class="kumonosu-card-img" src="https://picsum.photos/id/28/400/400" alt="風景2">
<div class="kumonosu-card-data">
<span class="kumonosu-card-num">2 / 7</span>
<h2>深い森の小径</h2>
<p>木漏れ日が差し込む原生林。鳥のさえずりと葉の擦れ合う音が、自然の奏でる音楽のように響きます。</p>
<div class="kumonosu-card-footer">
<label for="kumonosu-radio-1" aria-label="前へ">❮</label>
<label for="kumonosu-radio-3" aria-label="次へ">❯</label>
</div>
</div>
</article>
<!-- カード 3 -->
<input type="radio" id="kumonosu-radio-3" name="radio-card">
<article class="kumonosu-card" style="--angle:-7deg">
<img class="kumonosu-card-img" src="https://picsum.photos/id/122/400/400" alt="風景3">
<div class="kumonosu-card-data">
<span class="kumonosu-card-num">3 / 7</span>
<h2>近代都市の灯り</h2>
<p>夜空にそびえ立つ摩天楼。都会のエネルギーが凝縮された夜景は、常に新しい物語を予感させます。</p>
<div class="kumonosu-card-footer">
<label for="kumonosu-radio-2" aria-label="前へ">❮</label>
<label for="kumonosu-radio-4" aria-label="次へ">❯</label>
</div>
</div>
</article>
<!-- カード 4 -->
<input type="radio" id="kumonosu-radio-4" name="radio-card">
<article class="kumonosu-card" style="--angle:11deg">
<img class="kumonosu-card-img" src="https://picsum.photos/id/124/400/400" alt="風景4">
<div class="kumonosu-card-data">
<span class="kumonosu-card-num">4 / 7</span>
<h2>波打ち際の休息</h2>
<p>どこまでも続く青い水平線。寄せては返す波の音が、日常の忙しさを忘れさせてくれるでしょう。</p>
<div class="kumonosu-card-footer">
<label for="kumonosu-radio-3" aria-label="前へ">❮</label>
<label for="kumonosu-radio-5" aria-label="次へ">❯</label>
</div>
</div>
</article>
<!-- カード 5 -->
<input type="radio" id="kumonosu-radio-5" name="radio-card" >
<article class="kumonosu-card" style="--angle:13deg">
<img class="kumonosu-card-img" src="https://picsum.photos/id/29/400/400" alt="風景5">
<div class="kumonosu-card-data">
<span class="kumonosu-card-num">5 / 7</span>
<h2>峻険な山脈</h2>
<p>雲を突き抜けるような高い山々。その雄大な姿は、自然の力強さと厳かさを教えてくれます。</p>
<div class="kumonosu-card-footer">
<label for="kumonosu-radio-4" aria-label="前へ">❮</label>
<label for="kumonosu-radio-6" aria-label="次へ">❯</label>
</div>
</div>
</article>
<!-- カード 6 -->
<input type="radio" id="kumonosu-radio-6" name="radio-card">
<article class="kumonosu-card" style="--angle:-17deg">
<img class="kumonosu-card-img" src="https://picsum.photos/id/234/400/400" alt="風景6">
<div class="kumonosu-card-data">
<span class="kumonosu-card-num">6 / 7</span>
<h2>冬の静寂</h2>
<p>一面が真っ白に染まる雪景色。冷たい空気が感覚を研ぎ澄ませ、世界を美しく際立たせます。</p>
<div class="kumonosu-card-footer">
<label for="kumonosu-radio-5" aria-label="前へ">❮</label>
<label for="kumonosu-radio-7" aria-label="次へ">❯</label>
</div>
</div>
</article>
<!-- カード 7 -->
<input type="radio" id="kumonosu-radio-7" name="radio-card" >
<article class="kumonosu-card" style="--angle:20deg">
<img class="kumonosu-card-img" src="https://picsum.photos/id/152/400/400" alt="風景7">
<div class="kumonosu-card-data">
<span class="kumonosu-card-num">7 / 7</span>
<h2>黄昏時の風景</h2>
<p>空が茜色に染まるマジックアワー。今日という一日の終わりを告げる、最も美しい瞬間の一つです。</p>
<div class="kumonosu-card-footer">
<label for="kumonosu-radio-6" aria-label="前へ">❮</label>
<label for="kumonosu-radio-1" aria-label="次へ">❯</label>
</div>
</div>
</article>
</div>
</div>
/* 1. 基本スタイル */
body {
font: 1rem "Helvetica Neue", Arial, "Hiragino Kaku Gothic ProN", "Hiragino Sans", Meiryo, sans-serif;
color-scheme: dark light;
background-color: #f0f0f0;
margin: 0;
line-height: 1.6;
}
/* 2. レイアウト用ラッパー */
.kumonosu-slider-layout {
min-height: 100svh;
display: grid;
place-content: center;
padding: 1rem;
overflow: hidden;
}
/* --- スタイル設定 --- */
*, ::before, ::after {
margin: 0;
box-sizing: border-box;
}
@property --angle {
syntax: "<angle>";
initial-value: 0deg;
inherits: true;
}
img {
max-width: 100%;
display: block;
}
input[type="radio"] {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
.kumonosu-cards {
--img-w: 220px;
--duration: 300ms;
--img-easing: cubic-bezier(0.34, 1.56, 0.64, 1);
width: min(100% - 4rem, 800px);
margin-inline: auto;
display: grid;
}
.kumonosu-card {
--cards-grid-cols: auto;
--cards-grid-rows: var(--img-w) auto;
--cards-grid-gap: 2rem;
--cards-footer-justify: center;
grid-area: 1/1;
display: grid;
place-items: center;
grid-template-columns: var(--cards-grid-cols);
grid-template-rows: var(--cards-grid-rows);
gap: var(--cards-grid-gap);
}
@media (min-width: 601px) {
.kumonosu-card {
--cards-grid-cols: var(--img-w) auto;
--cards-grid-rows: auto;
--cards-grid-gap: 4rem;
--cards-footer-justify: start;
}
}
.kumonosu-card-img {
width: 220px;
height: 220px;
aspect-ratio: 1 / 1;
rotate: var(--angle, 0deg);
border-radius: 12px;
border: 4px solid #FFF;
overflow: hidden;
transform-origin: center;
object-fit: cover;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
background-color: #ddd;
}
/* アニメーションロジック */
input:nth-of-type(1):checked+.kumonosu-card~.kumonosu-card>.kumonosu-card-img {
animation: straighten-img-1 calc(var(--duration) * 2) forwards;
animation-timing-function: var(--img-easing);
}
.kumonosu-card:has(~input:nth-of-type(2):checked)>.kumonosu-card-img, input:nth-of-type(2):checked+.kumonosu-card~.kumonosu-card>.kumonosu-card-img {
animation: straighten-img-2 calc(var(--duration) * 2) forwards;
animation-timing-function: var(--img-easing);
}
.kumonosu-card:has(~input:nth-of-type(3):checked)>.kumonosu-card-img, input:nth-of-type(3):checked+.kumonosu-card~.kumonosu-card>.kumonosu-card-img {
animation: straighten-img-3 calc(var(--duration) * 2) forwards;
animation-timing-function: var(--img-easing);
}
.kumonosu-card:has(~input:nth-of-type(4):checked)>.kumonosu-card-img, input:nth-of-type(4):checked+.kumonosu-card~.kumonosu-card>.kumonosu-card-img {
animation: straighten-img-4 calc(var(--duration) * 2) forwards;
animation-timing-function: var(--img-easing);
}
.kumonosu-card:has(~input:nth-of-type(5):checked)>.kumonosu-card-img, input:nth-of-type(5):checked+.kumonosu-card~.kumonosu-card>.kumonosu-card-img {
animation: straighten-img-5 calc(var(--duration) * 2) forwards;
animation-timing-function: var(--img-easing);
}
.kumonosu-card:has(~input:nth-of-type(6):checked)>.kumonosu-card-img, input:nth-of-type(6):checked+.kumonosu-card~.kumonosu-card>.kumonosu-card-img {
animation: straighten-img-6 calc(var(--duration) * 2) forwards;
animation-timing-function: var(--img-easing);
}
.kumonosu-card:has(~input:nth-of-type(7):checked)>.kumonosu-card-img, input:nth-of-type(7):checked+.kumonosu-card~.kumonosu-card>.kumonosu-card-img {
animation: straighten-img-7 calc(var(--duration) * 2) forwards;
animation-timing-function: var(--img-easing);
}
@keyframes straighten-img-1 {
50% {
--angle: 0deg;
}
}
@keyframes straighten-img-2 {
50% {
--angle: 0deg;
}
}
@keyframes straighten-img-3 {
50% {
--angle: 0deg;
}
}
@keyframes straighten-img-4 {
50% {
--angle: 0deg;
}
}
@keyframes straighten-img-5 {
50% {
--angle: 0deg;
}
}
@keyframes straighten-img-6 {
50% {
--angle: 0deg;
}
}
@keyframes straighten-img-7 {
50% {
--angle: 0deg;
}
}
/* 重なり順 */
.kumonosu-card {
z-index: -1;
}
input:checked+.kumonosu-card {
z-index: 10 !important;
}
.kumonosu-card:has(+input:checked) {
z-index: 9;
}
.kumonosu-card:has(+input + .kumonosu-card + input:checked) {
z-index: 8;
}
.kumonosu-card:has(+input + .kumonosu-card + input + .kumonosu-card + input:checked) {
z-index: 7;
}
.kumonosu-card:has(+input + .kumonosu-card + input + .kumonosu-card + input + .kumonosu-card + input:checked) {
z-index: 6;
}
.kumonosu-card:has(+input + .kumonosu-card + input + .kumonosu-card + input + .kumonosu-card + input + .kumonosu-card + input:checked) {
z-index: 5;
}
.kumonosu-card:has(+input + .kumonosu-card + input + .kumonosu-card + input + .kumonosu-card + input + .kumonosu-card + input + .kumonosu-card + input:checked) {
z-index: 4;
}
.kumonosu-card-data {
display: grid;
gap: 1rem;
}
.kumonosu-card-data>.kumonosu-card-num {
opacity: var(--data-opacity, 0);
font-size: .8rem;
color: #666;
}
.kumonosu-card-data>h2, .kumonosu-card-data>p {
transition: var(--duration) ease-in-out;
transition-delay: var(--data-delay, 0ms);
opacity: var(--data-opacity, 0);
translate: 0 var(--data-y, 20px);
}
.kumonosu-card-footer {
display: flex;
justify-content: var(--cards-footer-justify);
gap: 2rem;
}
.kumonosu-card-footer label {
margin-block-start: auto;
cursor: pointer;
pointer-events: var(--card-events, none);
opacity: var(--data-opacity, 0);
color: #000;
background-color: #EEE;
border-radius: 50%;
width: 32px;
height: 32px;
display: grid;
place-content: center;
transition: all 300ms ease-in-out;
}
/* 矢印ホバー時のスタイル:背景を黒に */
.kumonosu-card-footer label:hover {
color: #FFF;
background-color: #000;
}
input:checked+.kumonosu-card {
--data-opacity: 1;
--data-y: 0;
--data-delay: var(--duration);
--card-events: auto;
transition: z-index;
transition-delay: 300ms;
}
input:checked+.kumonosu-card>.kumonosu-card-img {
animation: reveal-img calc(var(--duration) * 2) forwards;
}
@keyframes reveal-img {
50% {
translate: -150% 0;
--angle: 0deg;
}
}
このスライダーは、CSSのみで状態管理と表示切り替えを行います。
ラジオボタンの checked 状態を利用し、選択中のカードだけを前面に表示します。
カードは同じ位置に重ねて配置され、z-index によって表示順を制御しています。
label for によるクリック操作grid と z-index による重なり構造画像は角度を持った状態から表示され、切り替え時に角度が整いながら横方向へ移動します。
テキストは少し遅れて表示され、視線が自然に流れるようになっています。
見た目や動きはCSS変数を調整するだけで変更できます。
構造を変えずにデザインを調整できる点が特徴です。
--img-w:画像サイズ--duration:アニメーション速度カードを追加する場合は、CSS側の定義もあわせて追加します。
この実装では、:has() や @property などの比較的新しいCSS機能を使用しています。
非対応ブラウザでは、重なりやアニメーションが簡略化される場合があります。
また、画像は object-fit: cover を使用しているため、被写体が切れることがあります。
:has() 非対応環境では重なり表現が弱くなる@property 非対応環境では回転アニメが滑らかでなくなる