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 つのスクロール領域のまとめ
システム設定で [スクロールバーを常に表示する] を選択しました。レイアウトとスクロール オーケストレーションを確認するうえでも、この設定をオンにしてレイアウトが機能することは重要です。
このコンポーネントでスクロールバー ガターが表示されると、スクロール領域の位置、サポートされている方向、相互作用が明確に示されます。これらのスクロール ウィンドウ フレームが、レイアウトのフレックスまたはグリッドの親でもあることを検討してください。
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
});
あるものを別のスクロール位置に追従させ、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)
のキーフレームはリンクをハイライト表示します。それ以外の場合は標準のテキスト色になります。ネストループは、外側のループが各ナビゲーション アイテムで、内側のループが各 navitem の個人用キーフレームであるため、比較的単純です。外側のループ要素が内側のループ要素と同じかどうかを確認し、選択されたタイミングを把握します。
記事を書くのはとても楽しかったです。大好きさ
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。