斜めタブ(フェーズ切替)

様式名: コンストラクティビズム / タブ

タブ切替斜め赤黒白キーボード操作
⧉ 別タブで開く

「壊れた例」を押すと、この様式が成立しなくなる条件を実物で見られます。

特徴 — なぜ効くか

  • 平行四辺形のタブを黒帯の上に並べる。斜めが様式の成立条件なので、タブという「並ぶ部位」と相性がいい(同じ角度の平行四辺形は隣同士で隙間なく噛み合う)
  • 同じ様式でも 14_constructivist_cta は見せるだけの告知面。こちらは押される部位なので、斜めが当たり判定・フォーカスリング・状態表示に直接ぶつかる
  • 選択中は「赤の面・太字・下端の生成りバー」の3つで示す。赤と黒の差は 3.21:1 しかなく、色の差だけに載せると弱い
  • 先頭タブを左へ裁ち落として構図に角度を出す。中途半端に余白を残すと、切れ端に見えて事故に見える
  • 様式としての見せ場は角度だが、部位としての本体はキーボード操作。role="tablist" を名乗った時点で矢印キーの実装が義務になる

実装メモ — 踏んだ罠

  • 斜めは transform:skewX() で作る。面が傾くとラベルまで歪むので、中の <span> を逆向きに skewX して文字だけ立て直す(--skew を1か所で持ち、calc(var(--skew) * -1) で打ち消す)
  • 【clip-path で斜めを作ってはいけない】clip-path は outline も一緒に切り取る。実機(Chrome)で確認したところ、outline-offset が負ならフォーカスリングは斜めの辺で途切れてタブの形をなぞらず、正の値にすると要素の外側に出るのでリングが丸ごと消える。skewX なら outline も一緒に傾くのでリングは平行四辺形のまま残る
  • 【フォーカスリングは内側に引く】帯は overflow で切っている(左の裁ち落としと横スクロールのため必須)。この状態で outline-offset を正にすると、skewX 版でもリングが帯に切られて完全に見えなくなった(Chrome で確認)。offset は負にして内側に引く
  • 同じ角度の平行四辺形は隣同士で噛み合うので、区切りの罫線は要らない。gap:3px を空ければ下地の黒が線として見える
  • role="tablist" を名乗るなら roving tabindex まで実装する。選択中だけ tabindex=0、他は -1 にし、左右キー・Home・End で移動させる。宣言だけして矢印キーを付けないのが一番たちが悪い(支援技術には「タブだ」と伝わるのに、そのとおり操作できない)
  • タブは min-height:52px を確保する(タップ領域 WCAG 2.5.8)。斜めにすると押せる面が左右の端で細くなるので、高さで稼ぐ
  • パネル側にも tabindex="0" を置く。タブから Tab を1回押せば中身へ入れる

使いどころ / 使ってはいけない場面

壊れる条件

コントラスト実測

対象文字色背景実効背景判定
非選択タブの文字(黒帯)#f4f1ea#111111#11111116.74:1AA 合格
非選択タブの文字(ホバー地 #2a2a2a)#f4f1ea#2a2a2a#2a2a2a12.72:1AA 合格
選択タブの文字(赤の面)#ffffff#c8102e#c8102e5.88:1AA 合格
選択タブの面 vs 黒帯(非文字3:1基準)#c8102e#111111#1111113.21:1非文字3:1 合格だが余裕なし。面の色だけに頼らず、下端のバーと太字を併用する
選択インジケータの生成りバー(非文字3:1基準)#f4f1ea#c8102e#c8102e5.22:1AA 合格
パネル本文(生成り地)#111111#f4f1ea#f4f1ea16.74:1AA 合格
パネルの補助文(生成り地)#55504a#f4f1ea#f4f1ea7.07:1AA 合格
見出しの赤(生成り地)#c8102e#f4f1ea#f4f1ea5.22:1AA 合格
壊れた例:選択中を赤の文字色だけで示す#c8102e#111111#1111113.21:1不合格
壊れた例:非選択タブを灰に落とす#6b6b6b#111111#1111113.54:1不合格

アクセシビリティ

スマホへの落とし方

ブラウザ対応

関連するパーツ

コードを見る
<!doctype html>
<html lang="ja"><head><meta charset="utf-8"><title>コンストラクティビズム タブ デモ</title><style>
*{box-sizing:border-box;margin:0;padding:0}
:root{
  --red:#c8102e;    /* 白文字で 5.88:1。純赤#ff0000 は眩しく残像が出るので使わない */
  --ink:#111111;    /* 黒。タブ帯と活字の主色 */
  --paper:#f4f1ea;  /* 白より少し生成りにして、赤黒が浮きすぎないようにする */
  --sub:#55504a;    /* 生成り地の補助文字。7.07:1 */
  --skew:-12deg;
}
body{background:var(--paper);color:var(--ink);min-height:340px;overflow:hidden;
  font-family:"Helvetica Neue",system-ui,"Hiragino Kaku Gothic ProN",sans-serif}

/* 下に黒を8px残す。ここを詰めると選択タブの生成りバーが下のパネルと同色で溶けて消える。
   左は詰めて先頭タブを画面外へ裁ち落とす(構成主義の構図。中途半端に余白を残すと切れ端に見える) */
/* 狭い幅では折り返さず横スクロールにする。折り返すと平行四辺形の噛み合わせが段で崩れ、
   ラベルも「01 企/画」と割れる(375px で実際に割れたので入れた) */
.strip{background:var(--ink);padding:0 18px 8px 0;overflow-x:auto;overflow-y:hidden}
/* 斜めの平行四辺形は隣同士でそのまま噛み合う。gap を空けると黒地が線として見え、
   それがタブの区切りになる(区切りのために別の罫線を足さなくてよい) */
[role="tablist"]{display:flex;gap:3px;width:max-content;min-width:100%}

/* 斜めは skewX で作る。clip-path で作ると outline ごと切り取られ、
   フォーカスリングが消える(demo_bad.html の[1]で実演) */
[role="tab"]{transform:skewX(var(--skew));background:transparent;border:0;cursor:pointer;
  min-height:52px;padding:0 22px;color:var(--paper);font:inherit;font-size:13px;
  letter-spacing:.04em;position:relative;white-space:nowrap}
/* 面を傾けるとラベルまで歪む。中の塊を逆向きに傾けて文字だけ立て直す */
[role="tab"] span{display:inline-block;transform:skewX(calc(var(--skew) * -1))}
[role="tab"]:hover{background:#2a2a2a}
/* リングは内側に引く。帯が overflow:hidden なので、外側(正の offset)に出すと帯に切られる */
[role="tab"]:focus-visible{outline:3px solid var(--paper);outline-offset:-6px}

/* 選択中は「赤の面・太字・下端の生成りバー」の3つで示す。
   赤 on 黒 は 3.21:1 で非文字3:1をぎりぎり超えるだけなので、色の差だけには載せない */
[role="tab"][aria-selected="true"]{background:var(--red);font-weight:900}
[role="tab"][aria-selected="true"]::after{content:"";position:absolute;
  left:0;right:0;bottom:0;height:7px;background:var(--paper)}

.panels{position:relative;padding:26px 24px 30px}
/* 幾何の飾り。情報を持たないので操作からも読み上げからも外す */
.tri{position:absolute;pointer-events:none;top:0;right:-10px;width:0;height:0;
  border-right:120px solid transparent;border-top:120px solid var(--red);opacity:.16}
[role="tabpanel"]{position:relative}
[role="tabpanel"]:focus-visible{outline:3px solid var(--ink);outline-offset:6px}
[role="tabpanel"][hidden]{display:none}
.no{display:inline-block;background:var(--ink);color:var(--paper);font-size:11px;
  font-weight:900;letter-spacing:.18em;padding:5px 12px;transform:rotate(-3deg)}
h2{margin-top:14px;font-size:26px;font-weight:900;letter-spacing:-.01em;line-height:1.2}
h2 em{font-style:normal;color:var(--red)}
/* 和文の行長は em で取る。ch は半角基準なので全角では半分の文字数で折り返す */
p{margin-top:12px;font-size:13px;line-height:1.9;max-width:26em}
.sub{color:var(--sub);font-size:12px;margin-top:10px}
</style></head><body>
<div class="strip">
  <div role="tablist" aria-label="制作のフェーズ">
    <button role="tab" id="t1" aria-controls="p1" aria-selected="true" tabindex="0"><span>01 企画</span></button>
    <button role="tab" id="t2" aria-controls="p2" aria-selected="false" tabindex="-1"><span>02 設計</span></button>
    <button role="tab" id="t3" aria-controls="p3" aria-selected="false" tabindex="-1"><span>03 実装</span></button>
    <button role="tab" id="t4" aria-controls="p4" aria-selected="false" tabindex="-1"><span>04 検証</span></button>
  </div>
</div>
<div class="panels">
  <div class="tri"></div>
  <div role="tabpanel" id="p1" aria-labelledby="t1" tabindex="0">
    <span class="no">PHASE 01</span>
    <h2>なぜ作るかを、<em>先に</em>決める。</h2>
    <p>目的とターゲットが決まっていない画面は、あとから何色に塗っても決まらない。形より前に、伝えたい中身と順序を固める。</p>
    <p class="sub">成果物:コンセプトシート/伝える順序の一覧</p>
  </div>
  <div role="tabpanel" id="p2" aria-labelledby="t2" tabindex="0" hidden>
    <span class="no">PHASE 02</span>
    <h2>順序を、<em>構造</em>に変える。</h2>
    <p>載せる情報を並べ替え、どこで何を見せるかを決める。ここで決めた構造が、そのまま見出しと導線になる。</p>
    <p class="sub">成果物:情報構造/ワイヤーフレーム</p>
  </div>
  <div role="tabpanel" id="p3" aria-labelledby="t3" tabindex="0" hidden>
    <span class="no">PHASE 03</span>
    <h2>角度と色は、<em>最後</em>に足す。</h2>
    <p>斜めも赤も、構造が決まってから乗せる装いにすぎない。先に装いを決めると、中身のほうを装いに合わせて削ることになる。</p>
    <p class="sub">成果物:HTML/CSS/操作の実装</p>
  </div>
  <div role="tabpanel" id="p4" aria-labelledby="t4" tabindex="0" hidden>
    <span class="no">PHASE 04</span>
    <h2>目で見て、<em>測って</em>から出す。</h2>
    <p>キーボードだけで全部を操作し、コントラストを実際に計算する。通らなければ、様式のほうを直す。</p>
    <p class="sub">成果物:コントラスト実測値/キーボード操作の確認</p>
  </div>
</div>
<script>
/* タブは「1つの操作対象」として扱う。Tab キーは選択中のタブにだけ止まり、
   タブ間の移動は左右キーで行う(roving tabindex)。
   role="tablist" を名乗る以上ここまで実装する。名乗るだけで矢印キーが無いのが一番たちが悪い */
(function () {
  var list = document.querySelector('[role="tablist"]');
  var tabs = [].slice.call(list.querySelectorAll('[role="tab"]'));

  function select(tab, focus) {
    tabs.forEach(function (t) {
      var on = t === tab;
      t.setAttribute('aria-selected', on ? 'true' : 'false');
      t.tabIndex = on ? 0 : -1;
      document.getElementById(t.getAttribute('aria-controls')).hidden = !on;
    });
    if (focus) tab.focus();
  }
  list.addEventListener('click', function (ev) {
    var t = ev.target.closest('[role="tab"]');
    if (t) select(t, true);
  });
  list.addEventListener('keydown', function (ev) {
    var i = tabs.indexOf(document.activeElement);
    if (i < 0) return;
    var to = null;
    if (ev.key === 'ArrowRight') to = tabs[(i + 1) % tabs.length];
    else if (ev.key === 'ArrowLeft') to = tabs[(i - 1 + tabs.length) % tabs.length];
    else if (ev.key === 'Home') to = tabs[0];
    else if (ev.key === 'End') to = tabs[tabs.length - 1];
    if (!to) return;
    ev.preventDefault();
    select(to, true);
  });
})();
</script>
</body></html>
出典・参考: 自作の再現実装。タブのキーボード操作(roving tabindex・左右キー)は WAI-ARIA Authoring Practices の Tabs パターンに基づく。様式の成立条件は 14_constructivist_cta と同じ出典。コントラスト値と clip-path の挙動は当方で実測。