ドキュメントのコード例ブロックをコンテナクエリ・Web Animations API・アクセシビリティで刷新
ドキュメントサイトのコード例表示コンポーネントが大幅に改善され、ナローレイアウト対応・アニメーション制御・アクセシビリティの3つの軸で品質が向上しました。変更は code-examples.css・code-examples.js・code-examples.js(トランスフォーマー)の3ファイルに集中しており、設計の一貫性とユーザー体験の両立を図っています。
背景
ドキュメントのコード例ブロックは、プレビュー領域・ソースパネルの開閉・リサイズグリップなど複数のインタラクションを持つ複合コンポーネントです。これまでの実装はビューポート幅への依存・ハードコードされたスペーシング値・アニメーションなしの即時表示切替という課題を抱えていました。
このPRはそれらの問題を、コンテナクエリ・デザイントークン・Web Animations APIという現代的な技術で解消します。変更されるのはドキュメント資産のみであり、コンポーネントライブラリ本体のAPIには影響しません。
技術的な変更
CSSのコンテナクエリ化とデザイントークン統合
.code-example がコンテナとして宣言され、ナローレイアウトの判定がビューポート幅ではなくコンテナ幅に基づくようになりました。これにより、ページレイアウトの変化に追従した柔軟なスタイリングが可能になります。
変更前:
.code-example {
background: var(--wa-color-surface-lowered);
border: var(--wa-border-style) var(--wa-panel-border-width) var(--wa-color-neutral-border-quiet);
border-radius: var(--wa-panel-border-radius);
color: var(--wa-color-text-normal);
isolation: isolate;
}
.code-example-preview {
border-block-end: var(--wa-border-style) var(--wa-panel-border-width) var(--wa-color-neutral-border-quiet);
padding: 2rem 3.25rem 2rem 2rem;
min-width: 20rem;
}
変更後:
.code-example {
--code-example-panel-radius: calc(var(--wa-panel-border-radius) - var(--wa-panel-border-width));
--code-example-resizer-inline-size: calc(var(--wa-space-l) + var(--wa-space-2xs));
--code-example-preview-min-inline-size: calc(4 * var(--wa-space-5xl));
--code-example-border: var(--wa-border-style) var(--wa-panel-border-width) var(--wa-color-neutral-border-quiet);
--code-example-show-duration: var(--wa-transition-normal);
--code-example-hide-duration: var(--wa-transition-normal);
container-type: inline-size;
container-name: code-example;
@container code-example (max-inline-size: 30rem) {
.code-example-preview {
padding-block: var(--wa-space-m);
padding-inline: var(--wa-space-m);
}
.code-example-resizer {
display: none;
}
}
}
.code-example-preview {
border-block-end: var(--code-example-border);
padding-block: var(--code-example-preview-padding-block);
padding-inline: var(--code-example-preview-padding-inline-start) var(--code-example-preview-padding-inline-end);
}
ハードコードされていた 2rem・3.25rem などの値が --code-example-* スコープのカスタムプロパティに置き換えられ、--wa-space-* トークンで構成されるようになりました。ボーダー定義も --code-example-border に集約され、同じ値を複数箇所で繰り返す必要がなくなっています。
Web Animations APIによるソースパネルアニメーション
ソースパネルの表示・非表示がアニメーション化され、wa-details コンポーネントと整合した実装になりました。Turboナビゲーションによる再初期化と高速トグル時の競合制御が明示的に組み込まれています。
const codeExampleAnimations = new WeakMap();
function parseDuration(duration) {
duration = String(duration).toLowerCase();
if (duration.includes('ms')) return parseFloat(duration) || 0;
if (duration.includes('s')) return (parseFloat(duration) || 0) * 1000;
return parseFloat(duration) || 0;
}
async function animate(el, keyframes, options) {
return el.animate(keyframes, options).finished.catch(() => {
/* suppress errors in Safari */
});
}
function prefersReducedMotion() {
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
function getAnimationGeneration(codeExample) {
return codeExampleAnimations.get(codeExample) || 0;
}
function bumpAnimationGeneration(codeExample) {
const generation = getAnimationGeneration(codeExample) + 1;
codeExampleAnimations.set(codeExample, generation);
return generation;
}
function cancelSourceAnimations(source) {
source.getAnimations().forEach(animation => animation.cancel());
}
function getCodeExampleDurations(source) {
const style = getComputedStyle(source);
const showDuration = parseDuration(style.getPropertyValue('--code-example-show-duration').trim() || '200ms');
const hideDuration = parseDuration(style.getPropertyValue('--code-example-hide-duration').trim() || '200ms');
return { showDuration, hideDuration };
}
codeExampleAnimations は WeakMap でコンポーネントインスタンスごとのアニメーション世代番号を管理します。トグル操作のたびに世代番号をインクリメントし、古い世代のアニメーションを cancel() することで、高速な連続トグルによる表示状態の不整合を防ぎます。アニメーション時間は getComputedStyle で --code-example-show-duration / --code-example-hide-duration を読み取るため、CSSレイヤーから動的に制御できます。
アクセシビリティのマークアップ追加
HTMLトランスフォーマー側では、ソースパネルに role="region"・aria-label・aria-hidden が付与されるようになりました。
変更前:
<div class="code-example-source" id="${id}">
変更後:
<div class="code-example-source" id="${id}" role="region" aria-label="Example source code"${isOpen ? '' : ' aria-hidden="true"'}>
role="region" によってランドマーク領域として識別可能になり、aria-label で目的が明示されます。折りたたまれた状態では aria-hidden="true" が付与されるため、スクリーンリーダーが非表示のソースコードをスキャンすることを防ぎます。
設計判断
スコープ付きカスタムプロパティによるカプセル化
--code-example-* プレフィックスを持つカスタムプロパティをコンポーネントのルート要素で定義することで、--wa-* トークンへの依存を一点に集約しています。消費箇所では --code-example-border のように意味のある名前を参照するため、デザイントークンの変更がコンポーネント全体に自動的に反映される構造です。アニメーション時間を --code-example-show-duration としてCSSに持たせ、JavaScriptが getComputedStyle で読み取る設計は、wa-details との整合を取りつつ、ネストされた wa-details のタイミングを上書きしないための明示的な選択です。
WeakMapによる状態管理
アニメーション世代の管理に WeakMap を採用したことで、コード例要素がDOMから削除された際に自動的にエントリがGCされます。Turboナビゲーションのようなシングルページ遷移では要素の生成・破棄が繰り返されるため、メモリリークを防ぐうえで適切な選択です。
ビューポートではなくコンテナに対する応答
ナローレイアウトの閾値をコンテナクエリ(max-inline-size: 30rem)で定義することで、ページレイアウトのどの位置にコード例ブロックが置かれても正しく機能します。サイドバーが展開された状態や埋め込みコンテキストなど、ビューポード幅だけでは捕捉できない状況にも対応できます。
まとめ
このPRは、ドキュメントコンポーネントの内部実装を「ハードコード値とビューポード依存」から「デザイントークン・コンテナクエリ・Web Animations API」へと移行する変更です。CSSとJavaScriptの責務分離・状態管理の堅牢化・アクセシビリティの明示的な付与を一度に整理することで、ドキュメント基盤としての保守性と品質が引き上げられました。