DEMO
CODE
<div class="demo-stage bas-stage">
<div class="bas-wrap" style="--bas-pos:50%">
<div class="bas-layer bas-after"><span class="bas-tag">AFTER</span></div>
<div class="bas-layer bas-before"><span class="bas-tag">BEFORE</span></div>
<span class="bas-handle" aria-hidden="true"><span class="bas-grip">⇔</span></span>
<input class="bas-range" id="bas-range" type="range" min="0" max="100" value="50" step="0.1" aria-label="比較位置">
</div>
</div>
.bas-stage { display:flex; align-items:center; justify-content:center; padding:2rem; background:#0B0B10; min-height:280px; }
.bas-wrap { position:relative; width:100%; max-width:420px; aspect-ratio:16/10; border-radius:14px; overflow:hidden; border:1px solid rgba(255,255,255,.1); }
.bas-layer { position:absolute; inset:0; display:flex; align-items:flex-end; padding:1rem; }
.bas-after { background:linear-gradient(135deg,#E91B89 0%,#7B37A8 55%,#5BAADE 100%); }
.bas-before { background:linear-gradient(135deg,#2A2A32 0%,#3E3E48 55%,#55555F 100%); clip-path:inset(0 calc(100% - var(--bas-pos)) 0 0); }
.bas-tag { font-family:'Oswald',sans-serif; font-size:.68rem; letter-spacing:.24em; color:#fff; background:rgba(0,0,0,.42); padding:.3rem .8rem; border-radius:999px; }
.bas-after .bas-tag { margin-left:auto; }
.bas-handle {
position:absolute; top:0; bottom:0; left:var(--bas-pos); width:2px; transform:translateX(-1px);
background:#fff; pointer-events:none;
}
.bas-grip {
position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
width:38px; height:38px; border-radius:50%; background:#fff; color:#1A1A1A;
display:flex; align-items:center; justify-content:center; font-size:.9rem;
box-shadow:0 4px 14px rgba(0,0,0,.35);
}
.bas-range {
position:absolute; inset:0; width:100%; height:100%; margin:0; opacity:0; cursor:ew-resize;
}
.bas-range:focus-visible ~ .bas-handle .bas-grip { outline:3px solid #E91B89; outline-offset:2px; }
@media (prefers-reduced-motion: reduce) {
.bas-handle { transition:none; }
}
(function(){
var input = document.getElementById('bas-range');
if (!input) return;
var wrap = input.closest('.bas-wrap');
function sync(){ wrap.style.setProperty('--bas-pos', input.value + '%'); }
input.addEventListener('input', sync);
sync();
})();
AI PROMPT
施工前後・加工前後を比べる画像比較スライダーを作って。同じ大きさの2枚を position: absolute で完全に重ね、上に載せる BEFORE 側に clip-path: inset(0 calc(100% - var(--pos)) 0 0) を掛ける。--pos が 0% なら全部隠れ、100% なら全部見える。操作はスライダー本体を <input type="range"> にして opacity: 0 で全面に敷き、input イベントで CSS変数 --pos を value + '%' に更新するだけ。この方式なら、ドラッグ・クリック・キーボードの矢印キー・スクリーンリーダーの操作が全部無料で付いてくる(pointerdown/pointermove を自前で書く必要がない)。境界線のつまみは pointer-events: none の絶対配置要素にして left: var(--pos) で追従させる。range に aria-label を付け、:focus-visible でつまみにアウトラインを出すこと。画像を使う場合は両方に同じ aspect-ratio を指定し、object-fit: cover で歪みを防ぐ。 Acceptance criteria: works with prefers-reduced-motion (animation disabled), zero console errors, zero layout shift
AIエージェントでの実装ガイド → Claude Code でWebアニメーションを実装する
BOOKS · PR · AMAZON
※当サイトはAmazonアソシエイトプログラムの参加者です。リンクから商品をご購入いただくと、当方に紹介料が発生します。
学びを止めない · AMAZONサブスク
※当サイトはAmazonアソシエイトプログラムの参加者です。リンクから商品をご購入いただくと、当方に紹介料が発生します。
2枚の画像を重ね、境界をドラッグして見える範囲を変えることで前後を比較するUIです。リフォーム・美容・写真編集・AI生成の品質比較など、変化そのものが商品価値である領域では定番のコンポーネントになります。
当サイトの Crossfade Gallery は2枚を時間で切り替える表現、Reveal Mask はマスクで一方向に開く表現です。本ページは「閲覧者が境界位置を自由に決められる」別テーマで、比較の主導権が読み手側にある点が本質的な違いです。
実装の要は、自前のドラッグ処理を書かず透明な range 入力を全面に敷くことです。これだけでキーボード操作と支援技術への対応が同時に片付き、コード量も大幅に減ります。
なぜ input type=range を使うのですか?
自前で pointerdown / pointermove / pointerup を実装すると、キーボード操作・タッチの慣性・フォーカス管理をすべて自分で書くことになります。range 入力を透明にして全面に敷けば、ドラッグもクリックも矢印キーもスクリーンリーダーも標準の挙動として手に入り、コードは値をCSS変数へ渡す数行で済みます。アクセシビリティ面でも有利です。
clip-path と width の切り替えはどちらが良いですか?
clip-path が推奨です。width を変える方式だと、上に重ねた画像の描画幅が変わるため中身が縮んで見えたり、レイアウトの再計算が毎フレーム走ったりします。clip-path は表示範囲を切り抜くだけなので画像は歪まず、コンポジタ側で処理されるため滑らかです。
コードは無料で使えますか?
はい。Motion LabのコードスニペットとAIプロンプトは、クレジット表記なしで自由に使えます。コピーしてそのままプロジェクトに利用できます。