iOS アプリや Android アプリで見られるようなタブ コンポーネントを作成する方法の基礎的な概要。
この投稿では、レスポンシブで、複数のデバイス入力をサポートし、ブラウザ間で動作するウェブ用のタブ コンポーネントの構築に関する考えを共有します。デモをお試しください。
動画をご希望の場合は、この投稿の YouTube 版をご覧ください。
概要
タブはデザイン システムの一般的なコンポーネントですが、さまざまな形状や形式があります。最初は <frame> 要素をベースに構築されたデスクトップ タブがあり、今では物理プロパティに基づいてコンテンツをアニメーション化する、滑らかなモバイル コンポーネントがあります。これらはすべて、スペースを節約するという同じ目的を持っています。
現在、タブのユーザー エクスペリエンスに不可欠なのは、表示フレーム内のコンテンツの表示を切り替えるボタン ナビゲーション領域です。多くの異なるコンテンツ領域が同じスペースを共有していますが、ナビゲーションで選択されたボタンに基づいて条件付きで表示されます。
ウェブ戦術
全体として、このコンポーネントは、いくつかの重要なウェブ プラットフォーム機能のおかげで、非常に簡単に構築できました。
scroll-snap-points: 適切なスクロール停止位置で、スワイプとキーボード操作をエレガントに実現します。- ブラウザで処理されるページ内スクロール アンカーと共有サポートのための URL ハッシュによるディープリンク
<a>要素とid="#hash"要素のマークアップによるスクリーン リーダーのサポートprefers-reduced-motion: クロスフェード遷移とインスタント ページ内スクロールを有効にします。- 選択したタブに動的に下線を付け、色を変更する、ドラフト段階の
@scroll-timelineウェブ機能
HTML
基本的に、ここでの UX は、リンクをクリックし、URL でネストされたページの状態を表し、ブラウザが一致する要素にスクロールするとコンテンツ領域が更新されるというものです。
リンクや :target などの構造的なコンテンツ メンバーがあります。必要なのは、リンクのリスト(<nav> が適しています)と <article> 要素のリスト(<section> が適しています)です。各リンクハッシュはセクションと一致し、ブラウザはアンカーを介してスクロールできます。
たとえば、リンクをクリックすると、Chrome 89 で :target 記事に自動的にフォーカスが移動します。JS は必要ありません。ユーザーは、通常どおり入力デバイスを使用して記事のコンテンツをスクロールできます。マークアップで示されているように、補足コンテンツです。
タブを整理するために、次のマークアップを使用しました。
<snap-tabs>
<header>
<nav>
<a></a>
<a></a>
<a></a>
<a></a>
</nav>
</header>
<section>
<article></article>
<article></article>
<article></article>
<article></article>
</section>
</snap-tabs>
href プロパティと id プロパティを使用して、<a> 要素と <article> 要素の間の接続を確立できます。
<snap-tabs>
<header>
<nav>
<a href="#responsive"></a>
<a href="#accessible"></a>
<a href="#overscroll"></a>
<a href="#more"></a>
</nav>
</header>
<section>
<article id="responsive"></article>
<article id="accessible"></article>
<article id="overscroll"></article>
<article id="more"></article>
</section>
</snap-tabs>
次に、記事にさまざまな量の lorem を入力し、リンクにさまざまな長さと画像のタイトル セットを入力しました。コンテンツが用意できたら、レイアウトを開始できます。
スクロール レイアウト
このコンポーネントには、次の 3 種類のスクロール領域があります。
- ナビゲーション(ピンク)は水平方向にスクロール可能
- コンテンツ領域(青)は水平方向にスクロール可能
- 各記事アイテム(緑色)は、垂直方向にスクロールできます。
スクロールに関連する要素には、次の 2 種類があります。
- ウィンドウ
overflowプロパティ スタイルを持つ、定義されたディメンションを持つボックス。 - 特大のサーフェス
このレイアウトでは、ナビゲーション リンク、セクション記事、記事コンテンツなどのリストコンテナです。
レイアウト: <snap-tabs>
私が選択した最上位のレイアウトは flex(Flexbox)です。方向を column に設定したため、ヘッダーとセクションは垂直方向に並べられます。これは最初のスクロール ウィンドウで、overflow: hidden ですべてを非表示にします。ヘッダーとセクションは、個別のゾーンとしてまもなくオーバースクロールを採用する予定です。
<snap-tabs> <header></header> <section></section> </snap-tabs>
snap-tabs { display: flex; flex-direction: column; /* establish primary containing box */ overflow: hidden; position: relative; & > section { /* be pushy about consuming all space */ block-size: 100%; } & > header { /* defend againstneeding 100% */ flex-shrink: 0; /* fixes cross browser quarks */ min-block-size: fit-content; } }
カラフルな 3 スクロール図を再度表示:
<header>は、(ピンク)のスクロール コンテナになる準備が整いました。<section>は、(青色)のスクロール コンテナになるように準備されています。
VisBug でハイライト表示したフレームは、スクロール コンテナが作成したウィンドウを示しています。
タブ <header> レイアウト
次のレイアウトもほぼ同じです。flex を使用して縦方向の並びを作成します。
<snap-tabs>
<header>
<nav></nav>
<span class="snap-indicator"></span>
</header>
<section></section>
</snap-tabs>header { display: flex; flex-direction: column; }
.snap-indicator はリンクのグループとともに水平方向に移動する必要があります。このヘッダー レイアウトは、そのステージの設定に役立ちます。絶対位置に配置された要素はありません。
次に、スクロール スタイルです。2 つの水平スクロール領域(ヘッダーとセクション)間でスクロール スタイルを共有できることがわかったため、ユーティリティ クラス .scroll-snap-x を作成しました。
.scroll-snap-x {
/* browser decide if x is ok to scroll and show bars on, y hidden */
overflow: auto hidden;
/* prevent scroll chaining on x scroll */
overscroll-behavior-x: contain;
/* scrolling should snap children on x */
scroll-snap-type: x mandatory;
@media (hover: none) {
scrollbar-width: none;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
}
}
それぞれに、x 軸のオーバーフロー、オーバースクロールをトラップするスクロール コンテインメント、タッチデバイス用の非表示のスクロールバー、コンテンツ表示領域をロックするスクロール スナップが必要です。キーボードのタブ順序はアクセス可能で、操作によってフォーカスが自然に移動します。スクロール スナップ コンテナは、キーボードからカルーセル スタイルの操作も行えます。
タブヘッダー <nav> レイアウト
ナビゲーション リンクは、改行なしで 1 行に配置し、垂直方向に中央揃えにする必要があります。また、各リンクアイテムは、スクロール スナップ コンテナにスナップする必要があります。2021 年の CSS に向けて迅速に作業を進めてください。
<nav> <a></a> <a></a> <a></a> <a></a> </nav>
nav { display: flex; & a { scroll-snap-align: start; display: inline-flex; align-items: center; white-space: nowrap; } }
各リンクは独自のスタイルとサイズを持つため、ナビゲーション レイアウトでは方向とフローのみを指定する必要があります。ナビゲーション アイテムの幅がそれぞれ異なるため、インジケーターが新しいターゲットに合わせて幅を調整する際に、タブ間の切り替えが楽しくなります。ここにある要素の数に応じて、ブラウザはスクロールバーをレンダリングするかどうかを決定します。
タブ <section> レイアウト
このセクションはフレックス アイテムであり、スペースの大部分を占める必要があります。また、記事を配置する列を作成する必要もあります。CSS 2021 の迅速な対応に感謝いたします。block-size: 100% は、この要素を親に合わせて可能な限り引き伸ばし、独自のレイアウト用に、親の幅の 100% の一連の列を作成します。親に強力な制約を記述しているため、ここではパーセンテージが適切に機能します。
<section> <article></article> <article></article> <article></article> <article></article> </section>
section { block-size: 100%; display: grid; grid-auto-flow: column; grid-auto-columns: 100%; }
これは、「垂直方向にできるだけ拡張する」と言っているようなものです(flex-shrink: 0 に設定したヘッダーを思い出してください。これは、この拡張プッシュに対する防御です)。これにより、一連の全高列の行の高さが設定されます。auto-flow スタイルは、子を常に水平方向に配置し、折り返さないようにグリッドに指示します。これは、親ウィンドウをオーバーフローさせるために必要な動作です。
このような概念は、理解するのが難しい場合があります。このセクション要素はボックスに収まっていますが、一連のボックスも作成されています。図や説明がお役に立てば幸いです。
タブ <article> レイアウト
ユーザーは記事のコンテンツをスクロールでき、スクロールバーはオーバーフローがある場合にのみ表示されるようにします。これらの記事要素は適切な位置にあります。これらは、スクロールの親であると同時にスクロールの子でもあります。ブラウザは、タップ、マウス、キーボードによる複雑な操作を処理しています。
<article> <h2></h2> <p></p> <p></p> <h2></h2> <p></p> <p></p> ... </article>
article { scroll-snap-align: start; overflow-y: auto; overscroll-behavior-y: contain; }
記事が親スクローラー内でスナップされるように設定しました。ナビゲーション リンク アイテムと記事要素が、それぞれのスクロール コンテナのインライン開始位置にスナップされるのが気に入っています。調和のとれた関係のように見えます。
記事はグリッドの子であり、そのサイズは、スクロール UX を提供するビューポート領域として事前に決定されています。つまり、ここでは高さや幅のスタイルは必要なく、オーバーフローの処理方法を定義するだけで済みます。overflow-y を auto に設定し、便利な overscroll-behavior プロパティでスクロール操作をトラップします。
3 つのスクロール領域のまとめ
以下の例では、システム設定で [スクロールバーを常に表示] を選択しています。この設定をオンにした状態でレイアウトが機能することは、レイアウトとスクロールのオーケストレーションを確認するうえで、二重に重要であると考えています。
このコンポーネントでスクロールバーの溝を表示すると、スクロール領域の場所、サポートされている方向、相互作用が明確に示されます。これらのスクロール ウィンドウ フレームが、レイアウトの Flex または Grid の親でもあることを考慮してください。
DevTools を使用すると、これを可視化できます。
スクロール レイアウトは、スナップ、ディープリンク、キーボード アクセスに対応しています。UX の向上、スタイル、快適さのための強固な基盤。
注目の機能
スクロール スナップされた子は、サイズ変更中もロックされた位置を維持します。つまり、デバイスの回転やブラウザのサイズ変更時に、JavaScript でビューに何かを表示する必要はありません。Chromium DevTools のデバイスモードで、[Responsive] 以外のモードを選択し、デバイス フレームのサイズを変更して、この動作を試してみてください。要素がビューに表示されたまま、コンテンツとともにロックされていることに注目してください。これは、Chromium が仕様に合わせて実装を更新して以来、利用可能になっています。詳しくは、こちらのブログ投稿をご覧ください。
アニメーション
ここでのアニメーション作業の目標は、インタラクションを UI フィードバックに明確にリンクすることです。これにより、ユーザーはすべてのコンテンツをシームレスに検出できるようになります。モーションは目的を持って条件付きで追加します。ユーザーはオペレーティング システムでモーション設定を指定できるようになりました。私はインターフェースでユーザーの設定に応答することを心から楽しんでいます。
タブの下線を記事のスクロール位置にリンクします。スナップは、整列を美しくするだけでなく、アニメーションの開始と終了を固定する役割も果たします。これにより、ミニマップのように機能する <nav> がコンテンツに接続されたままになります。CSS と JS の両方からユーザーのモーション設定を確認します。配慮すべき点がいくつかあります。
スクロール動作
:target と element.scrollIntoView() の両方のモーション動作を改善する余地があります。デフォルトでは、即時です。ブラウザはスクロール位置を設定するだけです。では、そのスクロール位置に瞬時に移動するのではなく、遷移させたい場合はどうすればよいでしょうか。
@media (prefers-reduced-motion: no-preference) {
.scroll-snap-x {
scroll-behavior: smooth;
}
}
ここではモーションを導入しており、ユーザーが制御できないモーション(スクロールなど)であるため、このスタイルは、ユーザーがオペレーティング システムでモーションの軽減を希望していない場合にのみ適用されます。これにより、スクロール モーションを許可しているユーザーにのみスクロール モーションが導入されます。
タブ インジケーター
このアニメーションの目的は、インジケーターをコンテンツの状態に関連付けることです。動きを抑えたいユーザーには border-bottom スタイルをクロスフェードし、動きを気にしないユーザーにはスクロール連動のスライド + カラーフェード アニメーションを使用することにしました。
Chromium Devtools では、設定を切り替えて 2 つの異なる遷移スタイルをデモできます。このアプリの作成はとても楽しかったです。
@media (prefers-reduced-motion: reduce) {
snap-tabs > header a {
border-block-end: var(--indicator-size) solid hsl(var(--accent) / 0%);
transition: color .7s ease, border-color .5s ease;
&:is(:target,:active,[active]) {
color: var(--text-active-color);
border-block-end-color: hsl(var(--accent));
}
}
snap-tabs .snap-indicator {
visibility: hidden;
}
}
ユーザーがモーションを抑えることを希望している場合は、.snap-indicator を非表示にします。これは、もう必要ないためです。次に、border-block-end スタイルと transition に置き換えます。また、タブのインタラクションでは、アクティブなナビゲーション アイテムにブランドの下線ハイライトが表示されるだけでなく、テキストの色も暗くなっています。アクティブな要素は、テキストの色のコントラストが高く、明るいアンダーライトのアクセントが付いています。
CSS を数行追加するだけで、ユーザーは(動きに関する設定が配慮されているという意味で)自分が尊重されていると感じるでしょう。気に入ったわ
@scroll-timeline
上のセクションでは、モーションを抑えたクロスフェード スタイルを処理する方法を説明しました。このセクションでは、インジケーターとスクロール領域をリンクする方法を説明します。次は、楽しい試験運用版の機能です。皆さんも私と同じように ワクワクしていることでしょう
const { matches:motionOK } = window.matchMedia(
'(prefers-reduced-motion: no-preference)'
);
まず、JavaScript からユーザーのモーション設定を確認します。この結果が false の場合、ユーザーはモーションの軽減を希望しているため、スクロール連動モーション エフェクトは実行されません。
if (motionOK) {
// motion based animation code
}
このドキュメントの作成時点では、@scroll-timeline をサポートしているブラウザはありません。これは、試験運用版の実装のみを含むドラフト仕様です。ただし、このデモで使用するポリフィルがあります。
ScrollTimeline
CSS と JavaScript のどちらでもスクロール タイムラインを作成できますが、アニメーションでライブ要素の測定値を使用できるように、JavaScript を選択しました。
const sectionScrollTimeline = new ScrollTimeline({
scrollSource: tabsection, // snap-tabs > section
orientation: 'inline', // scroll in the direction letters flow
fill: 'both', // bi-directional linking
});
1 つの要素を別の要素のスクロール位置に追従させたい場合、ScrollTimeline を作成することで、スクロールリンクのドライバ(scrollSource)を定義します。通常、ウェブ上のアニメーションはグローバルな時間枠のティックに対して実行されますが、メモリ内にカスタムの sectionScrollTimeline を作成することで、すべてを変更できます。
tabindicator.animate({
transform: ...,
width: ...,
}, {
duration: 1000,
fill: 'both',
timeline: sectionScrollTimeline,
}
);
アニメーションのキーフレームに入る前に、スクロールのフォロワーである tabindicator が、カスタム タイムライン(セクションのスクロール)に基づいてアニメーション化されることを指摘しておくことが重要です。これでリンクは完了しましたが、最後の要素である、アニメーション化するステートフルなポイント(キーフレームとも呼ばれます)がありません。
ダイナミック キーフレーム
@scroll-timeline を使用してアニメーション化する、非常に強力な純粋な宣言型 CSS の方法がありますが、私が選択したアニメーションは動的すぎました。auto 幅の間で遷移する方法はなく、子要素の長さに応じて多数のキーフレームを動的に作成する方法もありません。
JavaScript はその情報を取得する方法を認識しているため、子を反復処理して、実行時に計算された値を取得します。
tabindicator.animate({
transform: [...tabnavitems].map(({offsetLeft}) =>
`translateX(${offsetLeft}px)`),
width: [...tabnavitems].map(({offsetWidth}) =>
`${offsetWidth}px`)
}, {
duration: 1000,
fill: 'both',
timeline: sectionScrollTimeline,
}
);
各 tabnavitem について、offsetLeft の位置を分解し、それを translateX 値として使用する文字列を返します。これにより、アニメーションに 4 つの変換キーフレームが作成されます。幅についても同様に、それぞれの動的な幅が確認され、キーフレーム値として使用されます。
フォントとブラウザの設定に基づく出力例を次に示します。
translateX キーフレーム:
[...tabnavitems].map(({offsetLeft}) =>
`translateX(${offsetLeft}px)`)
// results in 4 array items, which represent 4 keyframe states
// ["translateX(0px)", "translateX(121px)", "translateX(238px)", "translateX(464px)"]
幅のキーフレーム:
[...tabnavitems].map(({offsetWidth}) =>
`${offsetWidth}px`)
// results in 4 array items, which represent 4 keyframe states
// ["121px", "117px", "226px", "67px"]
この戦略をまとめると、タブ インジケーターは、セクション スクロールのスクロール スナップ位置に応じて 4 つのキーフレームにわたってアニメーション化されるようになります。スナップポイントにより、キーフレーム間の境界が明確になり、アニメーションの同期感がさらに高まります。
ユーザーは操作によってアニメーションを駆動し、インジケーターの幅と位置がセクションごとに変化し、スクロールに完全に追従します。
お気づきでないかもしれませんが、ハイライト表示されたナビゲーション アイテムが選択されたときの色が変化する様子は、とても気に入っています。
ハイライト表示されたアイテムのコントラストが高いと、選択されていない明るいグレーはさらに奥に押し込まれたように見えます。ホバー時や選択時など、テキストの色を遷移させることは一般的ですが、スクロール時にその色を遷移させ、下線インジケーターと同期させることは、次のレベルです。
手順は次のとおりです。
tabnavitems.forEach(navitem => {
navitem.animate({
color: [...tabnavitems].map(item =>
item === navitem
? `var(--text-active-color)`
: `var(--text-color)`)
}, {
duration: 1000,
fill: 'both',
timeline: sectionScrollTimeline,
}
);
});
各タブナビゲーション リンクには、この新しいカラー アニメーションが必要で、下線インジケーターと同じスクロール タイムラインをトラッキングします。以前と同じタイムラインを使用します。その役割はスクロール時にティックを出力することであるため、任意のタイプのアニメーションでそのティックを使用できます。先ほどと同様に、ループ内に 4 つのキーフレームを作成し、色を返します。
[...tabnavitems].map(item =>
item === navitem
? `var(--text-active-color)`
: `var(--text-color)`)
// results in 4 array items, which represent 4 keyframe states
// [
"var(--text-active-color)",
"var(--text-color)",
"var(--text-color)",
"var(--text-color)",
]
var(--text-active-color) の色のキーフレームはリンクをハイライト表示し、それ以外は標準のテキスト色です。外側のループが各ナビゲーション アイテム、内側のループが各ナビゲーション アイテムの個別のキーフレームであるため、ネストされたループにより比較的簡単になります。外側のループ要素が内側のループ要素と同じかどうかを確認し、それを使用して選択されたタイミングを把握します。
このブログ記事は、とても楽しく書くことができました。大好きさ
JavaScript のさらなる機能強化
ここで紹介しているコア機能は、JavaScript がなくても動作します。それでは、JS が利用可能な場合にどのように拡張できるかを見てみましょう。
ディープリンク
ディープリンクはモバイル用語に近いですが、ディープリンクの目的は、タブのコンテンツに直接 URL を共有できるタブで達成されていると思います。ブラウザは、URL ハッシュで一致する ID にページ内移動します。この onload ハンドラがプラットフォーム間で効果を発揮することがわかりました。
window.onload = () => {
if (location.hash) {
tabsection.scrollLeft = document
.querySelector(location.hash)
.offsetLeft;
}
}
スクロール終了の同期
ユーザーは常にクリックしたりキーボードを使用したりしているわけではなく、自由にスクロールしていることもあります。セクション スクロールが停止したときに、その位置が上部のナビゲーション バーに一致している必要があります。
スクロールの終了を待機する方法は次のとおりです。
js
tabsection.addEventListener('scroll', () => {
clearTimeout(tabsection.scrollEndTimer);
tabsection.scrollEndTimer = setTimeout(determineActiveTabSection, 100);
});
セクションがスクロールされるたびに、セクションのタイムアウトがあればクリアし、新しいタイムアウトを開始します。セクションのスクロールが停止したときに、タイムアウトをクリアせず、停止後 100 ミリ秒で起動します。このイベントが発生したら、ユーザーがどこで停止したかを特定する関数を呼び出します。
const determineActiveTabSection = () => {
const i = tabsection.scrollLeft / tabsection.clientWidth;
const matchingNavItem = tabnavitems[i];
matchingNavItem && setActiveTab(matchingNavItem);
};
スクロール スナップを想定すると、現在のスクロール位置をスクロール領域の幅で割ると、小数ではなく整数になります。次に、この計算されたインデックスを介してキャッシュから navitem を取得しようとします。一致するものが見つかった場合は、一致するものをアクティブに設定します。
const setActiveTab = tabbtn => {
tabnav
.querySelector(':scope a[active]')
.removeAttribute('active');
tabbtn.setAttribute('active', '');
tabbtn.scrollIntoView();
};
アクティブなタブを設定するには、まず現在アクティブなタブをクリアしてから、受信したナビゲーション アイテムにアクティブな状態属性を付与します。scrollIntoView() の呼び出しには、CSS との興味深いやり取りがあります。
.scroll-snap-x {
overflow: auto hidden;
overscroll-behavior-x: contain;
scroll-snap-type: x mandatory;
@media (prefers-reduced-motion: no-preference) {
scroll-behavior: smooth;
}
}
水平スクロール スナップ ユーティリティ CSS では、ユーザーがモーションを許容する場合に smooth スクロールを適用するメディアクエリをネストしています。JavaScript は、要素をスクロールして表示するための呼び出しを自由に行うことができ、CSS は UX を宣言的に管理できます。ときには、とても楽しい組み合わせになることもあります。
まとめ
私のやり方がわかったところで、あなたならどうしますか?これにより、コンポーネント アーキテクチャが興味深いものになります。お気に入りのフレームワークでスロットを使用した最初のバージョンを作成するのは誰でしょうか。🙂
アプローチを多様化し、ウェブで構築するすべての方法を学びましょう。Glitch を作成し、ツイートしていただければ、以下のコミュニティ リミックス セクションに追加します。
コミュニティ リミックス
- @devnook、@rob_dodson、@DasSurma とウェブ コンポーネント: 記事。
- @jhvanderschee(ボタン付き): Codepen。