デザインの様式を、動くデモ・効く理由・実装で踏んだ罠つきで解剖する図鑑。
様式名: コンストラクティビズム / タブ
「壊れた例」を押すと、この様式が成立しなくなる条件を実物で見られます。
| 対象 | 文字色 | 背景 | 実効背景 | 比 | 判定 |
|---|---|---|---|---|---|
| 非選択タブの文字(黒帯) | #f4f1ea | #111111 | #111111 | 16.74:1 | AA 合格 |
| 非選択タブの文字(ホバー地 #2a2a2a) | #f4f1ea | #2a2a2a | #2a2a2a | 12.72:1 | AA 合格 |
| 選択タブの文字(赤の面) | #ffffff | #c8102e | #c8102e | 5.88:1 | AA 合格 |
| 選択タブの面 vs 黒帯(非文字3:1基準) | #c8102e | #111111 | #111111 | 3.21:1 | 非文字3:1 合格だが余裕なし。面の色だけに頼らず、下端のバーと太字を併用する |
| 選択インジケータの生成りバー(非文字3:1基準) | #f4f1ea | #c8102e | #c8102e | 5.22:1 | AA 合格 |
| パネル本文(生成り地) | #111111 | #f4f1ea | #f4f1ea | 16.74:1 | AA 合格 |
| パネルの補助文(生成り地) | #55504a | #f4f1ea | #f4f1ea | 7.07:1 | AA 合格 |
| 見出しの赤(生成り地) | #c8102e | #f4f1ea | #f4f1ea | 5.22:1 | AA 合格 |
| 壊れた例:選択中を赤の文字色だけで示す | #c8102e | #111111 | #111111 | 3.21:1 | 不合格 |
| 壊れた例:非選択タブを灰に落とす | #6b6b6b | #111111 | #111111 | 3.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>