No.179
TRANSITION CSSJS

Direction-aware Tabs

右のタブを選ぶと中身が右から、左のタブを選ぶと左からすべり込むタブ。下線も選んだタブへ移動し、矢印キー・Home・End での切り替えと aria-selected の更新まで含めています。

MY COLLECTION RANDOM ↗

DEMO

春の配色

桜色と若葉色で、やわらかく明るい印象にまとめます。

CODE

HTML
<div class="demo-stage dat-stage">
  <div class="dat-box">
    <div class="dat-tabs" role="tablist" aria-label="季節の配色">
      <button type="button" role="tab" class="dat-tab" id="dat-t1" aria-selected="true" aria-controls="dat-p1">春</button>
      <button type="button" role="tab" class="dat-tab" id="dat-t2" aria-selected="false" aria-controls="dat-p2" tabindex="-1">夏</button>
      <button type="button" role="tab" class="dat-tab" id="dat-t3" aria-selected="false" aria-controls="dat-p3" tabindex="-1">秋</button>
      <button type="button" role="tab" class="dat-tab" id="dat-t4" aria-selected="false" aria-controls="dat-p4" tabindex="-1">冬</button>
      <span class="dat-ink" id="dat-ink" aria-hidden="true"></span>
    </div>
    <div class="dat-panels">
      <div class="dat-panel" role="tabpanel" id="dat-p1" aria-labelledby="dat-t1" tabindex="0"><h3>春の配色</h3><p>桜色と若葉色で、やわらかく明るい印象にまとめます。</p><div class="dat-strip dat-s1" aria-hidden="true"></div></div>
      <div class="dat-panel" role="tabpanel" id="dat-p2" aria-labelledby="dat-t2" tabindex="0" hidden><h3>夏の配色</h3><p>空色と白を中心に、強い日差しに負けない爽やかさを出します。</p><div class="dat-strip dat-s2" aria-hidden="true"></div></div>
      <div class="dat-panel" role="tabpanel" id="dat-p3" aria-labelledby="dat-t3" tabindex="0" hidden><h3>秋の配色</h3><p>紅葉の橙と栗色で、落ち着いた温かみを足します。</p><div class="dat-strip dat-s3" aria-hidden="true"></div></div>
      <div class="dat-panel" role="tabpanel" id="dat-p4" aria-labelledby="dat-t4" tabindex="0" hidden><h3>冬の配色</h3><p>藍色と銀色で、澄んだ空気と静けさを表します。</p><div class="dat-strip dat-s4" aria-hidden="true"></div></div>
    </div>
  </div>
</div>
CSS
.dat-stage { display:flex; align-items:center; justify-content:center; padding:1.5rem; background:#0B0B10; min-height:300px; }
.dat-box { width:100%; max-width:360px; }
.dat-tabs { position:relative; display:grid; grid-template-columns:repeat(4, 1fr); border-bottom:1px solid rgba(255,255,255,.14); }
.dat-tab { min-height:44px; border:0; background:transparent; color:#A1A1AA; font-size:.9rem; font-weight:600; cursor:pointer; }
.dat-tab[aria-selected="true"] { color:#fff; }
.dat-tab:focus-visible { outline:2px solid #E91B89; outline-offset:-2px; border-radius:6px; }
.dat-ink { position:absolute; left:0; bottom:-1px; width:25%; height:2px; background:#E91B89; transform:translateX(calc(var(--i, 0) * 100%)); transition: transform .35s cubic-bezier(.22,1,.36,1); }
.dat-panels { overflow:hidden; padding-top:1rem; }
.dat-panel { padding:1rem; border-radius:12px; background:#15151B; color:#C9C9D1; font-size:.84rem; line-height:1.7; outline:none; }
.dat-panel:focus-visible { box-shadow:inset 0 0 0 2px #E91B89; }
.dat-panel h3 { margin:0 0 .3rem; color:#fff; font-size:1rem; }
.dat-panel p { margin:0; }
.dat-strip { height:28px; margin-top:.8rem; border-radius:8px; }
.dat-s1 { background:linear-gradient(90deg,#F9A8D4,#BBF7D0); }
.dat-s2 { background:linear-gradient(90deg,#38BDF8,#F0F9FF); }
.dat-s3 { background:linear-gradient(90deg,#EA580C,#78350F); }
.dat-s4 { background:linear-gradient(90deg,#1E3A8A,#CBD5E1); }
@media (prefers-reduced-motion: reduce) {
  .dat-ink { transition-duration:.01ms; }
}
JS
(function(){
  var tabs = Array.prototype.slice.call(document.querySelectorAll('.dat-tab'));
  var ink = document.getElementById('dat-ink');
  if (!tabs.length || !ink) return;
  var reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
  var cur = 0;
  function select(i, focus){
    if (focus) tabs[i].focus();
    if (i === cur) return;
    var dir = i > cur ? 1 : -1;
    var oldP = document.getElementById(tabs[cur].getAttribute('aria-controls'));
    var newP = document.getElementById(tabs[i].getAttribute('aria-controls'));
    tabs[cur].setAttribute('aria-selected', 'false'); tabs[cur].tabIndex = -1;
    tabs[i].setAttribute('aria-selected', 'true'); tabs[i].tabIndex = 0;
    oldP.hidden = true; newP.hidden = false;
    ink.style.setProperty('--i', i);
    if (!reduce.matches && newP.animate) {
      newP.animate([{ transform: 'translateX(' + (dir * 40) + 'px)', opacity: 0 }, { transform: 'none', opacity: 1 }],
        { duration: 320, easing: 'cubic-bezier(.22,1,.36,1)' });
    }
    cur = i;
  }
  tabs.forEach(function(t, i){
    t.addEventListener('click', function(){ select(i, false); });
    t.addEventListener('keydown', function(e){
      var n = tabs.length, k = e.key, to = null;
      if (k === 'ArrowRight') to = (i + 1) % n;
      else if (k === 'ArrowLeft') to = (i - 1 + n) % n;
      else if (k === 'Home') to = 0;
      else if (k === 'End') to = n - 1;
      if (to === null) return;
      e.preventDefault();
      select(to, true);
    });
  });
})();

AI PROMPT

Claude Code Cursor v0
PROMPT
右のタブを選ぶと中身が右から、左のタブを選ぶと左からすべり込むタブ(Direction-aware Tabs)を作って。WAI-ARIA のタブの形で、role="tablist"(aria-label 付き)の中に role="tab" のボタンを4つ置き、aria-selected と aria-controls を付け、選ばれていないタブは tabindex="-1" にする。パネルは role="tabpanel"・aria-labelledby・tabindex="0" で、選ばれていないパネルは hidden にする。切り替える時は、新しいタブの番号が今のタブより大きければ +1、小さければ −1 を向きとし、新しいパネルに element.animate で translateX(向き × 40px)・透明 から transform: none・不透明 へ320ミリ秒・cubic-bezier(.22,1,.36,1) の動きを付ける(古いパネルはすぐ hidden にして、高さの変化と重なりを避ける)。タブの下には幅25%の下線を置き、カスタムプロパティ --i(タブの番号)で transform: translateX(calc(var(--i) × 100%)) を指定して .35秒で移動させる(left や width は動かさない)。キーボードは左右の矢印キーで前後のタブへ移動して選択し(端では反対側へ回り込む)、Home で最初、End で最後のタブを選ぶ。どちらもタブにフォーカスを移し、既定のスクロールは preventDefault で止める。prefers-reduced-motion: reduce の時は、パネルのすべりと下線の移動を止めて即座に切り替える。

Acceptance criteria: works with prefers-reduced-motion (animation disabled), zero console errors, zero layout shift

AIエージェントでの実装ガイド → Claude Code でWebアニメーションを実装する

BOOKS · PR · AMAZON

📖 なかしまぁ先生のCSSアニメーション入門 📖 UIデザインの教科書 新版 マルチデバイス時代のインターフェース設計 📖 Fundamentals of Web Animation with GSAP

※当サイトはAmazonアソシエイトプログラムの参加者です。リンクから商品をご購入いただくと、当方に紹介料が発生します。

学びを止めない · AMAZONサブスク

📚 Kindle Unlimited — 技術書・デザイン書が読み放題。30日無料体験 → 🎧 Audible — 移動中に耳で学ぶオーディオブック。30日無料体験 → 📦 Amazon Prime — Prime Reading・配送特典つき。30日無料体験 → 🎵 Music Unlimited — 作業用BGMに。1億曲が聴き放題・30日無料体験 →

※当サイトはAmazonアソシエイトプログラムの参加者です。リンクから商品をご購入いただくと、当方に紹介料が発生します。

CREATOR TOOLS · スクール(PR)

🎬 Adobe Creative Cloud — After Effects / Photoshop 業界標準ツール → 💻 SkillHacks — プログラミング・動画編集オンラインスクール → 🚀 デイトラ — Web制作スクール・実案件レベルまで習得 →

※本サイトはアフィリエイトリンクを含みます。リンク経由のご購入・お申し込みで運営者が紹介料を受け取る場合があります。

RELATED EFFECTS

↗ MORE
→ SLIDE PANEL TRANSITION → ELASTIC TAB INDICATOR → SEGMENTED CONTROL

Direction-aware Tabs(向きを意識したタブの切り替え)とは

タブを切り替える時、選んだタブが今のタブより右なら中身を右から、左なら左からすべり込ませる演出です。タブの並びと中身の動く向きがそろうため、どのタブへ移ったのかが動きからも分かります。

当サイトの Slide Panel Transition はタブを押すとパネルが横スライドで切り替わるトランジション、Elastic Tab Indicator はインジケーターが一度伸びてから縮んで着地する弾性アニメーション、Segmented Control は iOS 風のセグメント切替です。本ページは切り替えの向きを番号から決め、WAI-ARIA のタブの形とキーボード操作までをまとめた別テーマです。

タブは見た目を作るだけでなく、role・aria-selected・キーボード操作をそろえることで、読み上げやキーボードの利用者にも同じように使えます。動きは中身が入ってくる一瞬だけにとどめ、切り替えの操作そのものを待たせないようにしてください。

よくある質問

すべり込む向きはどう決めていますか?

新しく選んだタブの番号と、それまで選ばれていたタブの番号を比べています。右側(番号が大きい)のタブなら中身を右から、左側なら左から入れると、タブの並びと中身の動きが一致し、「隣のページへ移った」ことが伝わります。矢印キーで端から反対側へ回り込んだ時も番号で比べるので、最後のタブから最初のタブへ移る時は左から入ります。

キーボードではどう操作しますか?

WAI-ARIA Authoring Practices Guide のタブのパターンに合わせ、左右の矢印キーで前後のタブへ移り、端では反対側へ回り込むようにしています。本実装はフォーカスしたタブをすぐに表示する「自動で切り替わる」方式で、Home と End で最初と最後のタブへ移れます。タブの並びの中では、選ばれている1つだけを Tab キーで止まる場所にし、ほかは tabindex="-1" にしています。

コードは無料で使えますか?

はい。Motion LabのコードスニペットとAIプロンプトは、クレジット表記なしで自由に使えます。コピーしてそのままプロジェクトに利用できます。