横スクロールセクション
縦スクロール量をtransformで横移動に変換するピン留めセクション。ポートフォリオや工程紹介に映えます。
ライブデモ
使用例(お題: アイドルグループ Sakura)
この技法を「アイドルグループ Sakura」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- Sakura メンバー紹介。縦スクロールが横移動になり、メンバーカードが流れる -->
<div class="skh-scroller" id="hsScroller">
<div class="skh-intro">
<span class="skh-logo">Sakura</span>
<h1>MEMBER</h1>
<p>↓ スクロールでメンバーを横に紹介</p>
</div>
<!-- stickyで固定された高い領域。縦スクロール量を横移動に変換 -->
<section class="skh-pin" id="hsPin">
<div class="skh-track" id="hsTrack">
<article class="skh-card" style="--seed:31;--accent:#ff8fb3;">
<span class="skh-no">01</span>
<span class="skh-photo"></span>
<h2>ひなた</h2>
<p class="skh-role">センター / リーダー</p>
<p class="skh-msg">いつも笑顔をいちばん前で。</p>
</article>
<article class="skh-card" style="--seed:32;--accent:#ffa6c4;">
<span class="skh-no">02</span>
<span class="skh-photo"></span>
<h2>みお</h2>
<p class="skh-role">メインボーカル</p>
<p class="skh-msg">歌で気持ちを届けたい。</p>
</article>
<article class="skh-card" style="--seed:33;--accent:#ffb6cd;">
<span class="skh-no">03</span>
<span class="skh-photo"></span>
<h2>こはる</h2>
<p class="skh-role">ダンスリーダー</p>
<p class="skh-msg">キレッキレでいくよ!</p>
</article>
<article class="skh-card" style="--seed:34;--accent:#ff9fbd;">
<span class="skh-no">04</span>
<span class="skh-photo"></span>
<h2>あおい</h2>
<p class="skh-role">作詞担当</p>
<p class="skh-msg">言葉に想いをのせて。</p>
</article>
<article class="skh-card" style="--seed:35;--accent:#ffc2d6;">
<span class="skh-no">05</span>
<span class="skh-photo"></span>
<h2>ゆい</h2>
<p class="skh-role">最年少 / ムードメーカー</p>
<p class="skh-msg">みんなを元気にしちゃう!</p>
</article>
</div>
</section>
<div class="skh-outro">
<h2>5人で、ひとつの春を。</h2>
<p>個性あふれるメンバーが集まって、Sakura はできています。ライブ会場で会えるのを楽しみにしています。</p>
</div>
</div>
CSS
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--pink: #ffd1e0;
--pink-deep: #ff8fb3;
--gray: #eef0f3;
--ink: #5a4853;
}
body {
font-family: "Hiragino Kaku Gothic ProN", "Segoe UI", sans-serif;
background: #fff5f9;
color: var(--ink);
-webkit-font-smoothing: antialiased;
}
/* 自前スクロール領域 */
.skh-scroller {
width: 100%;
height: 100vh;
max-height: 100%;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
scrollbar-color: var(--pink-deep) transparent;
}
.skh-intro, .skh-outro {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 0 24px;
}
.skh-intro {
height: 220px;
gap: 4px;
background: linear-gradient(180deg, #fff6fa, var(--pink));
}
.skh-logo { font-size: 1rem; letter-spacing: .2em; color: var(--pink-deep); font-weight: 700; }
.skh-intro h1 { font-size: 2rem; font-weight: 800; letter-spacing: .14em; color: #c4396a; }
.skh-intro p { font-size: .8rem; color: #9a7080; margin-top: 4px; }
.skh-outro {
min-height: 220px;
gap: 12px;
padding: 50px 24px 80px;
background: #fff;
}
.skh-outro h2 { font-size: 1.4rem; font-weight: 800; color: #c4396a; }
.skh-outro p { max-width: 460px; color: #7a6470; line-height: 1.85; font-size: .9rem; }
/* スクロール距離を稼ぐ高い領域 */
.skh-pin {
position: relative;
height: 360vh; /* この高さの間だけ横移動 */
background: linear-gradient(180deg, var(--pink) 0%, #fff5f9 12%);
}
.skh-track {
position: sticky;
top: 0;
height: 100vh;
min-height: 360px;
display: flex;
align-items: center;
will-change: transform;
}
.skh-card {
position: relative;
flex: 0 0 70%;
height: 78%;
margin: 0 10px;
padding: 22px 18px;
border-radius: 22px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 6px;
background: #fff;
border: 1px solid var(--pink);
box-shadow: 0 22px 44px -24px rgba(255,143,179,.7);
}
.skh-card:first-child { margin-left: 24px; }
.skh-card:last-child { margin-right: 24px; }
.skh-no {
font-size: .72rem;
letter-spacing: .3em;
color: var(--accent);
font-weight: 800;
}
.skh-photo {
width: 110px; height: 110px;
border-radius: 50%;
background-size: cover;
background-position: center;
background-color: var(--pink); /* 読み込み前のフォールバック */
border: 3px solid var(--accent);
margin: 4px 0;
}
.skh-card h2 { font-size: 1.5rem; font-weight: 800; color: #c4396a; }
.skh-role {
font-size: .72rem;
letter-spacing: .06em;
color: #fff;
background: var(--accent);
padding: 3px 12px;
border-radius: 999px;
}
.skh-msg { font-size: .84rem; color: #7a6470; margin-top: 4px; }
/* 動きを減らす設定:横移動をやめ縦並びに */
@media (prefers-reduced-motion: reduce) {
.skh-pin { height: auto; }
.skh-track {
position: static;
height: auto;
flex-direction: column;
transform: none !important;
}
.skh-card { flex-basis: auto; width: 86%; height: auto; margin: 12px auto; }
}
JavaScript
// Sakura メンバー紹介:縦スクロール量を、メンバートラックの横移動へ変換
(() => {
const scroller = document.getElementById('hsScroller');
const pin = document.getElementById('hsPin');
const track = document.getElementById('hsTrack');
if (!scroller || !pin || !track) return; // null安全
// 各メンバー写真に picsum を設定(--seedを利用)
Array.from(track.querySelectorAll('.skh-photo')).forEach(photo => {
const card = photo.closest('.skh-card');
const seed = card ? getComputedStyle(card).getPropertyValue('--seed').trim() : '';
photo.style.backgroundImage = `url("https://picsum.photos/160/160?random=${seed || '1'}")`;
});
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduce) return; // 動きを減らす設定では縦並び(CSS側)に任せる
// 固定領域の進捗(0〜1)を計算し、トラックを横へ動かす
function render() {
const passed = scroller.scrollTop - pin.offsetTop;
const total = pin.offsetHeight - scroller.clientHeight;
const progress = total > 0
? Math.min(Math.max(passed / total, 0), 1)
: 0;
// トラックがはみ出す幅だけ横移動
const maxShift = track.scrollWidth - track.clientWidth;
track.style.transform = `translate3d(${-progress * maxShift}px,0,0)`;
}
let ticking = false;
scroller.addEventListener('scroll', () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => { render(); ticking = false; });
}, { passive: true });
window.addEventListener('resize', render);
render(); // 初期化
// 操作がなくても横移動が見えるよう、ゆっくり往復スクロール
let auto = true;
let dir = 1;
const stop = () => { auto = false; };
['wheel', 'touchstart', 'pointerdown'].forEach(ev =>
scroller.addEventListener(ev, stop, { passive: true }));
setTimeout(function step() {
if (!auto) return;
const max = scroller.scrollHeight - scroller.clientHeight;
if (max <= 0) return;
scroller.scrollTop += dir * 3;
if (scroller.scrollTop >= max - 1) dir = -1;
else if (scroller.scrollTop <= 1) dir = 1;
requestAnimationFrame(step);
}, 700);
})();
コード
HTML
<!-- 自前のスクロール領域(プレビュー枠内で完結) -->
<div class="hs-scroller" id="hsScroller">
<div class="hs-intro">
<span>↓ 縦スクロールが横移動に変わる</span>
</div>
<!-- stickyで固定された高い領域。縦スクロール量を横移動に変換 -->
<section class="hs-pin" id="hsPin">
<div class="hs-track" id="hsTrack">
<article class="hs-panel" style="--c1:#ff6a3d;--c2:#f9484a;">
<span class="hs-idx">01</span><h2>Discover</h2>
<p>横に流れるパネル</p>
</article>
<article class="hs-panel" style="--c1:#7a5cff;--c2:#4a2fd6;">
<span class="hs-idx">02</span><h2>Design</h2>
<p>縦スクロール量に連動</p>
</article>
<article class="hs-panel" style="--c1:#11b3a3;--c2:#0a8f7e;">
<span class="hs-idx">03</span><h2>Develop</h2>
<p>transformで横移動</p>
</article>
<article class="hs-panel" style="--c1:#f7b500;--c2:#f78f00;">
<span class="hs-idx">04</span><h2>Deliver</h2>
<p>ポートフォリオに最適</p>
</article>
</div>
</section>
<div class="hs-outro">
<h2>縦→横の変換テクニック</h2>
<p>固定領域の高さでスクロール距離を確保し、その進捗をパネルの横移動に割り当てています。</p>
</div>
</div>
CSS
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: "Segoe UI", system-ui, sans-serif;
background: #101018;
color: #fff;
}
/* プレビュー枠を埋める自前スクロール領域 */
.hs-scroller {
width: 100%;
height: 100vh;
max-height: 100%;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
}
.hs-intro, .hs-outro {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 0 24px;
}
.hs-intro {
height: 220px;
color: #9aa0b5;
font-size: .9rem;
letter-spacing: .06em;
}
.hs-outro {
min-height: 240px;
gap: 12px;
padding: 56px 24px 90px;
}
.hs-outro h2 { font-size: 1.5rem; }
.hs-outro p { max-width: 480px; color: #b6bbcd; line-height: 1.7; }
/* スクロール距離を稼ぐための高い領域 */
.hs-pin {
position: relative;
height: 320vh; /* この高さの間だけ横移動 */
}
.hs-track {
position: sticky;
top: 0;
height: 100vh;
min-height: 360px;
display: flex;
align-items: center;
will-change: transform;
}
.hs-panel {
position: relative;
flex: 0 0 78%;
height: 76%;
margin: 0 11px;
border-radius: 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
background: linear-gradient(140deg, var(--c1), var(--c2));
box-shadow: 0 20px 50px rgba(0,0,0,.35);
}
.hs-panel:first-child { margin-left: 24px; }
.hs-panel:last-child { margin-right: 24px; }
.hs-idx {
font-size: .85rem;
letter-spacing: .4em;
opacity: .85;
}
.hs-panel h2 {
font-size: 2.2rem;
font-weight: 800;
text-shadow: 0 4px 18px rgba(0,0,0,.25);
}
.hs-panel p { font-size: .9rem; opacity: .92; }
/* 動きを減らす設定:横移動をやめ縦並び風に */
@media (prefers-reduced-motion: reduce) {
.hs-pin { height: auto; }
.hs-track {
position: static;
height: auto;
flex-direction: column;
transform: none !important;
}
.hs-panel { flex-basis: auto; width: 90%; height: 220px; margin: 12px auto; }
}
JavaScript
// 自前スクロール領域の縦スクロール量を、トラックの横移動へ変換
(() => {
const scroller = document.getElementById('hsScroller');
const pin = document.getElementById('hsPin');
const track = document.getElementById('hsTrack');
if (!scroller || !pin || !track) return; // null安全
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduce) return; // 動きを減らす設定では縦並び(CSS側)に任せる
// 固定領域の進捗(0〜1)を計算し、トラックを横へ動かす
function render() {
// pin の上端がスクロール領域の上端を超えた量 ÷ 動かせる距離 = 進捗
const passed = scroller.scrollTop - pin.offsetTop;
const total = pin.offsetHeight - scroller.clientHeight;
const progress = total > 0
? Math.min(Math.max(passed / total, 0), 1)
: 0;
// トラックがはみ出す幅だけ横移動
const maxShift = track.scrollWidth - track.clientWidth;
track.style.transform = `translate3d(${-progress * maxShift}px,0,0)`;
}
let ticking = false;
const onScroll = () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => { render(); ticking = false; });
};
scroller.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', render);
render(); // 初期化
// 操作がなくても横移動が見えるよう、ゆっくり往復スクロール
let auto = true;
let dir = 1;
const stop = () => { auto = false; };
['wheel', 'touchstart', 'pointerdown'].forEach(ev =>
scroller.addEventListener(ev, stop, { passive: true }));
setTimeout(function step() {
if (!auto) return;
const max = scroller.scrollHeight - scroller.clientHeight;
if (max <= 0) return;
scroller.scrollTop += dir * 3;
if (scroller.scrollTop >= max - 1) dir = -1;
else if (scroller.scrollTop <= 1) dir = 1;
requestAnimationFrame(step);
}, 700);
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「横スクロールセクション」の効果を追加してください。
# 追加してほしい効果
横スクロールセクション(スクロール演出)
縦スクロール量をtransformで横移動に変換するピン留めセクション。ポートフォリオや工程紹介に映えます。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 自前のスクロール領域(プレビュー枠内で完結) -->
<div class="hs-scroller" id="hsScroller">
<div class="hs-intro">
<span>↓ 縦スクロールが横移動に変わる</span>
</div>
<!-- stickyで固定された高い領域。縦スクロール量を横移動に変換 -->
<section class="hs-pin" id="hsPin">
<div class="hs-track" id="hsTrack">
<article class="hs-panel" style="--c1:#ff6a3d;--c2:#f9484a;">
<span class="hs-idx">01</span><h2>Discover</h2>
<p>横に流れるパネル</p>
</article>
<article class="hs-panel" style="--c1:#7a5cff;--c2:#4a2fd6;">
<span class="hs-idx">02</span><h2>Design</h2>
<p>縦スクロール量に連動</p>
</article>
<article class="hs-panel" style="--c1:#11b3a3;--c2:#0a8f7e;">
<span class="hs-idx">03</span><h2>Develop</h2>
<p>transformで横移動</p>
</article>
<article class="hs-panel" style="--c1:#f7b500;--c2:#f78f00;">
<span class="hs-idx">04</span><h2>Deliver</h2>
<p>ポートフォリオに最適</p>
</article>
</div>
</section>
<div class="hs-outro">
<h2>縦→横の変換テクニック</h2>
<p>固定領域の高さでスクロール距離を確保し、その進捗をパネルの横移動に割り当てています。</p>
</div>
</div>
【CSS】
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: "Segoe UI", system-ui, sans-serif;
background: #101018;
color: #fff;
}
/* プレビュー枠を埋める自前スクロール領域 */
.hs-scroller {
width: 100%;
height: 100vh;
max-height: 100%;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
}
.hs-intro, .hs-outro {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 0 24px;
}
.hs-intro {
height: 220px;
color: #9aa0b5;
font-size: .9rem;
letter-spacing: .06em;
}
.hs-outro {
min-height: 240px;
gap: 12px;
padding: 56px 24px 90px;
}
.hs-outro h2 { font-size: 1.5rem; }
.hs-outro p { max-width: 480px; color: #b6bbcd; line-height: 1.7; }
/* スクロール距離を稼ぐための高い領域 */
.hs-pin {
position: relative;
height: 320vh; /* この高さの間だけ横移動 */
}
.hs-track {
position: sticky;
top: 0;
height: 100vh;
min-height: 360px;
display: flex;
align-items: center;
will-change: transform;
}
.hs-panel {
position: relative;
flex: 0 0 78%;
height: 76%;
margin: 0 11px;
border-radius: 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
background: linear-gradient(140deg, var(--c1), var(--c2));
box-shadow: 0 20px 50px rgba(0,0,0,.35);
}
.hs-panel:first-child { margin-left: 24px; }
.hs-panel:last-child { margin-right: 24px; }
.hs-idx {
font-size: .85rem;
letter-spacing: .4em;
opacity: .85;
}
.hs-panel h2 {
font-size: 2.2rem;
font-weight: 800;
text-shadow: 0 4px 18px rgba(0,0,0,.25);
}
.hs-panel p { font-size: .9rem; opacity: .92; }
/* 動きを減らす設定:横移動をやめ縦並び風に */
@media (prefers-reduced-motion: reduce) {
.hs-pin { height: auto; }
.hs-track {
position: static;
height: auto;
flex-direction: column;
transform: none !important;
}
.hs-panel { flex-basis: auto; width: 90%; height: 220px; margin: 12px auto; }
}
【JavaScript】
// 自前スクロール領域の縦スクロール量を、トラックの横移動へ変換
(() => {
const scroller = document.getElementById('hsScroller');
const pin = document.getElementById('hsPin');
const track = document.getElementById('hsTrack');
if (!scroller || !pin || !track) return; // null安全
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduce) return; // 動きを減らす設定では縦並び(CSS側)に任せる
// 固定領域の進捗(0〜1)を計算し、トラックを横へ動かす
function render() {
// pin の上端がスクロール領域の上端を超えた量 ÷ 動かせる距離 = 進捗
const passed = scroller.scrollTop - pin.offsetTop;
const total = pin.offsetHeight - scroller.clientHeight;
const progress = total > 0
? Math.min(Math.max(passed / total, 0), 1)
: 0;
// トラックがはみ出す幅だけ横移動
const maxShift = track.scrollWidth - track.clientWidth;
track.style.transform = `translate3d(${-progress * maxShift}px,0,0)`;
}
let ticking = false;
const onScroll = () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => { render(); ticking = false; });
};
scroller.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', render);
render(); // 初期化
// 操作がなくても横移動が見えるよう、ゆっくり往復スクロール
let auto = true;
let dir = 1;
const stop = () => { auto = false; };
['wheel', 'touchstart', 'pointerdown'].forEach(ev =>
scroller.addEventListener(ev, stop, { passive: true }));
setTimeout(function step() {
if (!auto) return;
const max = scroller.scrollHeight - scroller.clientHeight;
if (max <= 0) return;
scroller.scrollTop += dir * 3;
if (scroller.scrollTop >= max - 1) dir = -1;
else if (scroller.scrollTop <= 1) dir = 1;
requestAnimationFrame(step);
}, 700);
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。