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>
次に、記事にはさまざまな量のロレム、リンクにはさまざまな長さと画像のタイトル セットを入力しました。コンテンツが揃ったので、レイアウトを開始できます。
スクロール レイアウト
このコンポーネントには、次の 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 の 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 つのスクロール領域のまとめ
以下のスクリーンショットは、システム設定で [常にスクロールバーを表示] を選択したものです。レイアウトとスクロール オーケストレーションを確認するうえで、この設定をオンにしてレイアウトが機能することは非常に重要です。
このコンポーネントでスクロールバー ガターが表示されると、スクロール領域の位置、サポートされている方向、相互作用が明確に示されます。これらのスクロール ウィンドウ フレームは、レイアウトの flex またはグリッドの親でもあることを考慮してください。
DevTools を使用すると、この状態を可視化できます。
スクロール レイアウトは、スナップ、ディープリンクの設定、キーボードでの操作が可能です。UX の向上、スタイル、魅力を実現するための強固な基盤。
注目の機能
スクロール スナップされた子は、サイズ変更中にロックされた位置を維持します。つまり、デバイスの回転やブラウザのサイズ変更で JavaScript が何も表示する必要がなくなります。Chromium DevTools のデバイスモードで試すには、[レスポンシブ] 以外のモードを選択し、デバイス フレームのサイズを変更します。要素はビュー内に残り、コンテンツとともにロックされます。これは、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。