iOS アプリや Android アプリに見られるようなタブ コンポーネントを作成する方法の基本的な概要です。
この投稿では、レスポンシブで、複数のデバイス入力をサポートし、複数のブラウザで機能するウェブ用の Tabs コンポーネントの作成に関する考えを共有したいと思います。デモをお試しください。
動画をご覧になる場合は、この投稿の YouTube バージョンをご覧ください。
概要
タブはデザイン システムの一般的なコンポーネントですが、さまざまな形態があります。まず、デスクトップ タブは <frame>
要素で構築されていましたが、現在は物理特性に基づいてコンテンツをアニメーション化するモバイル コンポーネントが用意されています。皆同じこととして、スペースの節約を目指しています。
現在、タブのユーザー エクスペリエンスに不可欠なのは、ディスプレイ フレーム内のコンテンツの表示を切り替えるボタン ナビゲーション領域です。多くの異なるコンテンツ領域は同じスペースを共有しますが、ナビゲーションで選択したボタンに基づいて条件付きで表示されます。
ウェブ戦術
全体的に、このコンポーネントはいくつかの重要なウェブ プラットフォーム機能のおかげで、非常に簡単に構築できることがわかりました。
scroll-snap-points
- 適切なスクロール停止位置でのスワイプとキーボードの操作- ブラウザでの URL ハッシュを介したディープリンクで、ページ内スクロールのアンカーや共有のサポートを処理
<a>
要素とid="#hash"
要素のマークアップでのスクリーン リーダーのサポートprefers-reduced-motion
: クロスフェード遷移とインスタント インページ スクロールを有効にする- 選択されたタブを動的に下線したり色を変更したりするドラフト内
@scroll-timeline
ウェブ機能
HTML
基本的に、ここでの UX とは、リンクをクリックして、URL がネストされたページの状態を表すようにし、ブラウザが一致する要素までスクロールしたときにコンテンツ領域が更新されることを確認することです。
そこには、リンクと :target
という構造的なコンテンツ メンバーがあります。それには、<nav>
が適しているリンクのリストと、<section>
に適している <article>
要素のリストが必要です。各リンクハッシュは 1 つのセクションと照合されるため、ブラウザはアンカーによってスクロールできます。
たとえば、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>
次に、記事にはローレムを混ぜたもので、リンクにはタイトルのまとまった長さと画像のセットを入れました。作業するコンテンツを使用して レイアウトを開始します
レイアウトのスクロール
このコンポーネントには、次の 3 種類のスクロール領域があります。
- ナビゲーション(ピンク)は水平方向にスクロールできます。
- コンテンツ エリア(青色)は水平方向にスクロール可能です。
- 各記事アイテム(緑色)は縦方向にスクロールできます。
スクロールに関連する要素には、次の 2 種類があります。
- ウィンドウ
overflow
プロパティ スタイルでディメンションが定義されたボックス。 - 特大のサーフェス
このレイアウトでは、ナビゲーション リンク、セクション記事、記事コンテンツのリストコンテナです。
レイアウト: <snap-tabs>
トップレベルのレイアウトは Flex(Flexbox)でした。方向を column
に設定したため、ヘッダーとセクションは垂直方向に並べられます。これが最初のスクロール ウィンドウです。非表示のオーバーフローですべてが非表示になります。まもなく、ヘッダーとセクションで個々のゾーンとしてオーバースクロールが使用されるようになります。
<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>
レイアウト
ナビゲーション リンクは、改行なしで縦方向の中央揃えで配置し、各リンクアイテムはスクロール スナップ コンテナにスナップする必要があります。2021 CSS の Swift 作業にお役立てください。
<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 つのスクロール領域のまとめ
下では、システム設定で [スクロールバーを常に表示] を選択しました。この設定をオンにしてレイアウトが機能することは、私にとってはレイアウトとスクロール オーケストレーションを確認することになるため、二重に重要になると思います。
このコンポーネントのスクロールバーのガターを見ることで、スクロール領域の場所、サポートする方向、相互作用を明確に示すことができると思います。これらのスクロール ウィンドウ フレームのそれぞれが、レイアウトのフレックスまたはグリッドの親であることを考慮してください。
DevTools はこれを可視化するのに役立ちます。
スクロール レイアウトは完成しており、スナップ、ディープリンク可能、キーボード アクセスが可能です。UX の強化、スタイル、満足度の向上のための強固な基盤
注目の機能
スクロールでスナップされた子は、サイズ変更時にロックされた位置を維持します。つまり、デバイスの回転やブラウザのサイズ変更時に、JavaScript で何も表示する必要がありません。Chromium DevTools の [Device Mode] で [レスポンシブ] 以外のモードを選択してから、デバイス フレームのサイズを変更してみてください。要素はビュー内に留まり、そのコンテンツとともにロックされます。この機能は、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: article
- @jhvanderschee(ボタン付き): Codepen