GU公式サイト風:縦横に動くフルスクリーンスライダー
本記事では、GU公式サイトのビジュアル切替のように、上下にスクロールしながら
カテゴリ単位で横に切り替えられるスライダーUIを紹介します。
各スライドはフルスクリーン表示で、ホイール・スワイプ・矢印キーに対応。
縦移動は無限ループ、横移動はカテゴリタブやスワイプ操作でスムーズに切替できます。
誤操作を防ぐロック制御や、今どこにいるかを示すドットインジケーターも実装済み。
WebデザインやUI演出の参考として、そのままコピペで動く完全コードを掲載しています。
コードについて
本記事のコードは AI(ChatGPT)による生成をベースに作成・調整しています。ご利用の環境でテストの上ご使用ください。
免責 本コードの利用に伴う不具合・損害について、当サイトは責任を負いません。自己責任にてご利用ください。
免責 本コードの利用に伴う不具合・損害について、当サイトは責任を負いません。自己責任にてご利用ください。
デモ
PC表示デモ
※ 実コードは「コードをコピーして使おう!」をコピーしてご自身のHTMLとして開いてください。
スマホ表示イメージ
※スマホでの見え方イメージ(実機の比率は端末により異なります)
コードをコピーして使おう!
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>GU風:フルスクリーン縦+横スライド(2025最新版)</title>
<style>
/* ===========================================
Base / Reset
=========================================== */
:root{
--ink:#0f172a;
--bg:#0b0b0c;
--muted:#a7b0bd;
--accent:#ffffff;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{
background:var(--bg);
color:var(--accent);
font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans JP","Yu Gothic UI",sans-serif;
-webkit-font-smoothing:antialiased;
-moz-osx-font-smoothing:grayscale;
overflow:hidden;
}
/* ===========================================
Header(透過固定)
=========================================== */
.header{
position:fixed; inset:0 0 auto 0; height:72px;
display:flex; align-items:center; gap:12px; padding:0 20px;
z-index:50;
background:linear-gradient(180deg, rgba(0,0,0,.45), rgba(0,0,0,0));
color:#fff;
pointer-events:none;
}
.header *{ pointer-events:auto; }
.logo{font-weight:800; letter-spacing:.02em}
.badge{
display:inline-flex; align-items:center; justify-content:center;
height:32px; padding:0 14px; border-radius:999px;
border:1px solid rgba(255,255,255,.12);
background:rgba(255,255,255,.06);
color:#fff; font-size:13px; cursor:pointer;
transition:.25s;
}
.badge[aria-pressed="true"]{
background:#fff; color:#111; border-color:transparent; font-weight:700;
}
.tabset{display:flex; gap:8px; margin-left:14px}
.note{margin-left:auto; color:var(--muted); font-size:13px}
/* ===========================================
ステージ構造(横スワイプ=カテゴリ)
各カテゴリを横に並べる
=========================================== */
.stage{
position:absolute; inset:0;
height:100dvh; width:100%;
overflow:hidden;
}
.carousel{
position:absolute; top:0; left:0;
display:flex;
height:100%;
will-change:transform;
}
.stack{
position:relative;
flex:0 0 100vw;
height:100%;
overflow:hidden;
}
.inner{
position:absolute; left:0; top:0; width:100%;
will-change:transform;
}
/* ===========================================
スライド(縦方向)
=========================================== */
.slide{
position:relative;
width:100vw; height:100dvh;
display:flex; align-items:flex-end; justify-content:flex-start;
overflow:hidden;
}
.media{
position:absolute; inset:0;
width:100%; height:100%;
object-fit:cover;
display:block;
filter:contrast(.98) saturate(.96);
}
.caption{
position:relative; z-index:5;
padding:28px; max-width:56%;
color:#fff; text-shadow:0 8px 28px rgba(0,0,0,.55);
}
.title{
font-weight:800; letter-spacing:-.02em;
font-size:clamp(26px,4.2vw,48px); line-height:1.1;
margin:0 0 10px;
}
.desc{font-size:clamp(14px,1.6vw,18px); opacity:.9}
.vignette{
position:absolute; inset:auto 0 0 0; height:40%;
background:linear-gradient(180deg, rgba(0,0,0,0), rgba(0,0,0,.28));
z-index:1; pointer-events:none;
}
/* ===========================================
ドット(GU風:右中央配置・縦並び)
=========================================== */
.dots{
position:fixed;
right:22px; /* 右端に配置 */
top:50%; /* 縦中央 */
transform:translateY(-50%);
display:flex;
flex-direction:column; /* 縦方向に並べる */
align-items:center;
gap:10px;
z-index:60;
}
.dots button{
width:6px;
height:6px;
border:none;
border-radius:50%;
background:rgba(255,255,255,.4); /* 非アクティブは淡い白 */
cursor:pointer;
transition:all .25s ease;
}
.dots button.active{
background:#fff;
border-radius:3px;
height:22px; /* アクティブは縦長の棒に */
width:6px;
}
/* SP調整 */
@media (max-width:768px){
.header{height:64px}
.caption{max-width:90%; padding:18px}
.title{font-size:clamp(20px,7vw,34px)}
.note{display:none}
}
</style>
</head>
<body>
<header class="header" role="banner" aria-label="固定ヘッダー">
<div class="logo">GU-Like UI</div>
<nav class="tabset" role="tablist" aria-label="カテゴリ">
<button class="badge" role="tab" aria-pressed="true" data-go="0">WOMEN</button>
<button class="badge" role="tab" aria-pressed="false" data-go="1">MEN</button>
<button class="badge" role="tab" aria-pressed="false" data-go="2">KIDS・TEEN</button>
</nav>
</header>
<main class="stage" id="stage" role="main" aria-live="polite">
<div class="carousel" id="carousel">
<!-- ========== WOMEN ========== -->
<section class="stack" data-cat="WOMEN">
<div class="inner">
<div class="slide">
<img class="media" src="https://picsum.photos/id/1018/1600/900" alt="花柄スカート">
<div class="vignette"></div>
<div class="caption">
<p class="title">フラワープリント・スカート</p>
<p class="desc">軽やかに揺れる春の装い。</p>
<p class="price">¥2,490(税込)</p>
</div>
</div>
<div class="slide">
<img class="media" src="https://picsum.photos/id/1011/1600/900" alt="カフェスタイル">
<div class="vignette"></div>
<div class="caption">
<p class="title">カフェで過ごす休日</p>
<p class="desc">ナチュラルカラーのリラックスコーデ。</p>
<p class="price">¥1,990(税込)</p>
</div>
</div>
<div class="slide">
<img class="media" src="https://picsum.photos/id/1025/1600/900" alt="公園のピクニック">
<div class="vignette"></div>
<div class="caption">
<p class="title">ピクニック・コーデ</p>
<p class="desc">ナチュラル素材で過ごす休日スタイル。</p>
<p class="price">¥2,990(税込)</p>
</div>
</div>
<div class="slide">
<img class="media" src="https://picsum.photos/id/1032/1600/900" alt="街角ファッション">
<div class="vignette"></div>
<div class="caption">
<p class="title">シティ・フェミニン</p>
<p class="desc">都会で映える洗練ワントーンコーデ。</p>
<p class="price">¥3,490(税込)</p>
</div>
</div>
<div class="slide">
<img class="media" src="https://picsum.photos/id/1043/1600/900" alt="オフィススタイル">
<div class="vignette"></div>
<div class="caption">
<p class="title">オフィス・スタイル</p>
<p class="desc">ジャケットで魅せる上品カジュアル。</p>
<p class="price">¥4,990(税込)</p>
</div>
</div>
</div>
</section>
<!-- ========== MEN ========== -->
<section class="stack" data-cat="MEN">
<div class="inner">
<div class="slide">
<img class="media" src="https://picsum.photos/id/1005/1600/900" alt="メンズジャケット">
<div class="vignette"></div>
<div class="caption">
<p class="title">テーラードジャケット</p>
<p class="desc">クラシックとモダンの融合。</p>
<p class="price">¥5,990(税込)</p>
</div>
</div>
<div class="slide">
<img class="media" src="https://picsum.photos/id/1003/1600/900" alt="週末カジュアル">
<div class="vignette"></div>
<div class="caption">
<p class="title">ウィークエンド・カジュアル</p>
<p class="desc">休日の軽やかスタイル。</p>
<p class="price">¥2,990(税込)</p>
</div>
</div>
<div class="slide">
<img class="media" src="https://picsum.photos/id/1012/1600/900" alt="ストリートファッション">
<div class="vignette"></div>
<div class="caption">
<p class="title">ストリート・エッセンス</p>
<p class="desc">スニーカー×パーカーで作る街のスタイル。</p>
<p class="price">¥3,490(税込)</p>
</div>
</div>
<div class="slide">
<img class="media" src="https://picsum.photos/id/1020/1600/900" alt="オフィスカジュアル">
<div class="vignette"></div>
<div class="caption">
<p class="title">オフィス・カジュアル</p>
<p class="desc">クリーンな印象を与える着こなし。</p>
<p class="price">¥4,490(税込)</p>
</div>
</div>
<div class="slide">
<img class="media" src="https://picsum.photos/id/1036/1600/900" alt="スポーツミックス">
<div class="vignette"></div>
<div class="caption">
<p class="title">スポーツ・ミックス</p>
<p class="desc">アクティブに動ける都会的スタイル。</p>
<p class="price">¥3,990(税込)</p>
</div>
</div>
</div>
</section>
<!-- ========== KIDS ========== -->
<section class="stack" data-cat="KIDS">
<div class="inner">
<div class="slide">
<img class="media" src="https://picsum.photos/id/1027/1600/900" alt="ルームウェア">
<div class="vignette"></div>
<div class="caption">
<p class="title">ルームウェア・コレクション</p>
<p class="desc">おうち時間もかわいく快適に。</p>
<p class="price">¥1,490(税込)</p>
</div>
</div>
<div class="slide">
<img class="media" src="https://picsum.photos/id/1035/1600/900" alt="外遊び">
<div class="vignette"></div>
<div class="caption">
<p class="title">外遊びスタイル</p>
<p class="desc">動きやすさ重視のアクティブコーデ。</p>
<p class="price">¥1,990(税込)</p>
</div>
</div>
<div class="slide">
<img class="media" src="https://picsum.photos/id/1050/1600/900" alt="お出かけファッション">
<div class="vignette"></div>
<div class="caption">
<p class="title">お出かけコーデ</p>
<p class="desc">家族でのおでかけにもぴったり。</p>
<p class="price">¥2,490(税込)</p>
</div>
</div>
<div class="slide">
<img class="media" src="https://picsum.photos/id/1053/1600/900" alt="スクールスタイル">
<div class="vignette"></div>
<div class="caption">
<p class="title">スクール・スタイル</p>
<p class="desc">元気いっぱいの通学コーデ。</p>
<p class="price">¥1,690(税込)</p>
</div>
</div>
<div class="slide">
<img class="media" src="https://picsum.photos/id/1062/1600/900" alt="冬のおしゃれ">
<div class="vignette"></div>
<div class="caption">
<p class="title">ウィンタースタイル</p>
<p class="desc">暖かくてかわいい冬の着こなし。</p>
<p class="price">¥2,990(税込)</p>
</div>
</div>
</div>
</section>
</section>
</div>
</main>
<div class="dots" id="dots"></div>
<script>
(()=>{
const carousel = document.getElementById('carousel');
const stacks = Array.from(document.querySelectorAll('.stack'));
const dotsWrap = document.getElementById('dots');
const catBtns = Array.from(document.querySelectorAll('.badge'));
const VH = ()=>window.innerHeight;
const VW = ()=>window.innerWidth;
const duration = 800;
let catIndex = 0;
let animatingX = false; // 横アニメロック
let vLock = false; // ★縦アニメロック(2コマ進み防止)
let lastDirection = 1;
// 各カテゴリの状態
const states = stacks.map((stack)=>{
const inner = stack.querySelector('.inner');
const originals = Array.from(inner.children);
const total = originals.length;
const before = originals.map(el=>el.cloneNode(true));
const after = originals.map(el=>el.cloneNode(true));
inner.innerHTML = '';
after.forEach(n=>inner.appendChild(n));
originals.forEach(n=>inner.appendChild(n));
before.forEach(n=>inner.appendChild(n));
const state = { stack, inner, total, index: total }; // 中央セット先頭
applyY(state, true);
// ★transform の transition 終了でロック解除
inner.addEventListener('transitionend',(e)=>{
if(e.propertyName!=='transform') return;
// 端を越えたら中央へ瞬間戻し(transition無しなので transitionend は発火しない)
if(state.index < state.total){
state.index += state.total;
applyY(state, true);
}else if(state.index >= state.total*2){
state.index -= state.total;
applyY(state, true);
}
vLock = false; // ★ここで縦ロック解除
refreshDotsOnly();
});
return state;
});
function logicalIndex(s){ return ((s.index - s.total) % s.total + s.total) % s.total; }
function updateDots(){
const s = states[catIndex];
dotsWrap.innerHTML='';
for(let i=0;i<s.total;i++){
const b=document.createElement('button');
b.type='button';
if(i===logicalIndex(s)) b.classList.add('active');
b.addEventListener('click',()=>{
if(vLock) return;
const s = states[catIndex];
s.index = s.total + i; // 中央セット上の論理位置へ
vLock = true;
applyY(s, false);
});
dotsWrap.appendChild(b);
}
}
function refreshDotsOnly(){
const s = states[catIndex];
[...dotsWrap.children].forEach((b,i)=>b.classList.toggle('active', i===logicalIndex(s)));
}
// ★縦移動はロックを厳密管理
function goToSlide(dir){
if(vLock) return;
const s = states[catIndex];
s.index += (dir===1?1:-1);
vLock = true; // ここでロック
applyY(s, false);
}
function goToCat(i){
const next = (i + stacks.length) % stacks.length;
if(next===catIndex || animatingX) return;
// 現在カテゴリの縦位置を保持
const currentState = states[catIndex];
const currentY = parseFloat(currentState.inner.style.transform.match(/-?\d+(\.\d+)?/g)?.[0]||0);
currentState.savedY = currentY; // ★保存(中央セットの絶対位置を記憶)
currentState.savedIndex = currentState.index;
animateX(-next * VW(), ()=>{
catIndex = next;
const s = states[catIndex];
// ★前回の位置が記録されていれば復元、それ以外は初期位置
if(s.savedIndex !== undefined){
s.index = s.savedIndex;
applyY(s, true);
} else {
s.index = s.total;
applyY(s, true);
}
updateDots();
updateCatBtns();
});
}
function updateCatBtns(){
catBtns.forEach((b,i)=>b.setAttribute('aria-pressed', String(i===catIndex)));
}
function applyY(s, noAnim=false){
s.inner.style.transition = noAnim ? 'none' : `transform ${duration}ms ease`;
s.inner.style.transform = `translateY(${-s.index*VH()}px)`;
if(noAnim){
void s.inner.offsetWidth;
s.inner.style.transition = `transform ${duration}ms ease`;
}
}
function animateX(targetX, cb){
animatingX = true;
const startX = parseFloat(carousel.style.transform.match(/-?\d+(\.\d+)?(?=px,?)/)?.[0]||0);
const start = performance.now();
const ease = t=>t<.5?4*t*t*t:1-Math.pow(-2*t+2,3)/2;
const tick = (now)=>{
const p=Math.min(1,(now-start)/duration);
const e=ease(p);
const curX=startX+(targetX-startX)*e;
carousel.style.transform=`translateX(${curX}px)`;
if(p<1) requestAnimationFrame(tick);
else { animatingX=false; cb&&cb(); }
};
requestAnimationFrame(tick);
}
// タブ
catBtns.forEach(b=>b.addEventListener('click',()=>goToCat(Number(b.dataset.go))));
// ★ホイール:ロックのみで制御(タイマー禁止)
window.addEventListener('wheel', (e)=>{
if(animatingX) return;
const ax=Math.abs(e.deltaX), ay=Math.abs(e.deltaY);
if(ax<10 && ay<10) return;
if(ax>ay){
e.deltaX>0 ? goToCat(catIndex+1) : goToCat(catIndex-1);
}else{
goToSlide(e.deltaY>0 ? 1 : -1);
}
}, { passive: true });
// タッチ
let sx=0, sy=0, ex=0, ey=0;
window.addEventListener('touchstart',e=>{
const t=e.touches[0]; sx=t.clientX; sy=t.clientY;
},{passive:true});
window.addEventListener('touchmove',e=>{
const t=e.touches[0]; ex=t.clientX; ey=t.clientY;
},{passive:true});
window.addEventListener('touchend',()=>{
const dx=ex-sx, dy=ey-sy;
const ax=Math.abs(dx), ay=Math.abs(dy);
const th=40;
if(ax>ay && ax>th){
dx<0 ? goToCat(catIndex+1) : goToCat(catIndex-1);
}else if(ay>th){
goToSlide(dy<0 ? 1 : -1);
}
sx=sy=ex=ey=0;
},{passive:true});
// ★キーボード:長押しの連続発火を無視
window.addEventListener('keydown',e=>{
if(e.repeat) return;
if(e.key==='ArrowDown'||e.key==='PageDown') goToSlide(1);
if(e.key==='ArrowUp'||e.key==='PageUp') goToSlide(-1);
if(e.key==='ArrowRight') goToCat(catIndex+1);
if(e.key==='ArrowLeft') goToCat(catIndex-1);
});
// リサイズ
let rT;
window.addEventListener('resize',()=>{
clearTimeout(rT);
rT=setTimeout(()=>{ states.forEach(s=>applyY(s, true)); },120);
});
// 初期化
updateDots();
updateCatBtns();
})();
</script>
</body>
</html>
コメント