CSSとJavaScriptを使用してカスタムのフルページスライダーを作成する

公開: 2022-03-11

私はカスタムのフルスクリーンレイアウトを実際に日常的に頻繁に使用しています。 通常、これらのレイアウトは、かなりの量のインタラクションとアニメーションを意味します。 時間によってトリガーされる複雑な遷移のタイムラインであろうと、スクロールベースのユーザー主導のイベントセットであろうと、ほとんどの場合、UIには、いくつかの調整と変更を加えた、すぐに使用できるプラグインソリューションを使用するだけでは不十分です。 。 一方、JavaScript開発者の多くは、特定のプラグインが提供するすべてのベルやホイッスルをタスクに必要としない場合でも、仕事を簡単にするためにお気に入りのJSプラグインに手を伸ばす傾向があります。

免責事項:もちろん、そこにある多くのプラグインの1つを使用することには利点があります。 多くのコーディングを行うことなく、ニーズに合わせて調整するために使用できるさまざまなオプションを利用できます。 また、ほとんどのプラグイン作成者は、コードを最適化し、クロスブラウザーおよびクロスプラットフォーム互換にするなどしています。 しかし、それでも、プロジェクトに含まれるフルサイズのライブラリは、1つまたは2つの異なるものだけで提供されます。 サードパーティのプラグインを使用することが当然悪いことだと言っているのではありません。プロジェクトでは毎日使用しています。一般的に、各アプローチの長所と短所をそのまま比較検討することをお勧めします。コーディングの良い習慣。 このように自分のことをすることになると、あなたが探しているものを知るためにもう少しコーディングの知識と経験が必要ですが、最終的には、1つのことと1つのことだけを行うコードを取得する必要がありますあなたはそれをしたいです。

この記事は、カスタムコンテンツアニメーションを使用してフルスクリーンのスクロールトリガースライダーレイアウトを開発する際の純粋なCSS/JSアプローチを示すことを目的としています。 この縮小されたアプローチでは、CMSバックエンドから提供されると予想される基本的なHTML構造、最新のCSS(SCSS)レイアウト手法、および完全な対話性のためのバニラJavaScriptコーディングについて説明します。 この概念は必要最低限​​のものであるため、大規模なプラグインに簡単に拡張したり、コアに依存関係がないさまざまなアプリケーションで使用したりできます。

私たちが作成しようとしているデザインは、各プロジェクトの注目の画像とタイトルを備えたミニマルな建築家ポートフォリオのショーケースです。 アニメーションを含む完全なスライダーは次のようになります。

建築家ポートフォリオのサンプルスライダー。

ここでデモを確認できます。詳細については、私のGithubリポジトリにアクセスできます。

HTMLの概要

これが私たちが扱う基本的なHTMLです:

 <div> <div class="mask"> <!-- Textual logo will go here --> </div> <div> <div class="slides"> <!-- Featured image slides will go here --> </div> <div class="slides mask"> <!-- Slide titles will go here --> </div> </div> <div> <!-- Static info on the right --> </div> <nav> <!-- Current slide indicator --> </nav> </div>

hero-sliderのIDを持つdivがメインホルダーです。 内部では、レイアウトはセクションに分割されています。

  • ロゴ(静的セクション)
  • 主に取り組むスライドショー
  • 情報(静的セクション)
  • 現在アクティブなスライドとスライドの総数を示すスライダーナビゲーション

スライドショーのセクションに焦点を当てましょう。それがこの記事の関心の対象です。 ここでは、 mainauxの2つの部分があります。 メインは注目の画像を含むdivで、auxは画像のタイトルを保持します。 これら2つのホルダーの内側の各スライドの構造はかなり基本的です。 ここに、メインホルダーの内側に画像スライドがあります。

 <div class="slide" data-index="0"> <div class="abs-mask"> <div class="slide-image"> </div> </div> </div>

インデックスデータ属性は、スライドショーのどこにいるかを追跡するために使用します。 興味深いトランジション効果を作成するために使用するabs-maskdivと、slide-imagedivには特定の注目画像が含まれています。 画像は、CMSから直接取得されたかのようにインラインでレンダリングされ、エンドユーザーによって設定されます。

同様に、タイトルはAuxホルダーの内側にスライドします。

 <h2 class="slide-title slide" data-index="0"><a href="#">#64 Paradigm</a></h2>

各スライドタイトルは、対応するデータ属性とそのプロジェクトの単一ページにつながることができるリンクを持つH2タグです。

HTMLの残りの部分も非常に単純です。 上部にロゴがあり、どのページを表示しているかをユーザーに知らせる静的な情報、説明、スライダーの現在/合計インジケーターがあります。

CSSの概要

ソースCSSコードは、CSSプリプロセッサであるSCSSで記述され、ブラウザが解釈できる通常のCSSにコンパイルされます。 SCSSには、変数、ネストされた選択、ミックスイン、およびその他の優れた機能を使用できるという利点がありますが、ブラウザーにコードを正しく読み取らせるには、CSSにコンパイルする必要があります。 このチュートリアルでは、最小限のツールが必要だったため、Scout-Appを使用してコンパイルを処理しました。

私はフレックスボックスを使用して、基本的なサイドバイサイドレイアウトを処理しました。 アイデアは、片側にスライドショーを、反対側に情報セクションを配置することです。

 #hero-slider { position: relative; height: 100vh; display: flex; background: $dark-color; } #slideshow { position: relative; flex: 1 1 $main-width; display: flex; align-items: flex-end; padding: $offset; } #info { position: relative; flex: 1 1 $side-width; padding: $offset; background-color: #fff; }

ポジショニングについて詳しく見ていきましょう。もう一度、スライドショーのセクションに注目してください。

 #slideshow { position: relative; flex: 1 1 $main-width; display: flex; align-items: flex-end; padding: $offset; } #slides-main { @extend %abs; &:after { content: ''; @extend %abs; background-color: rgba(0, 0, 0, .25); z-index: 100; } .slide-image { @extend %abs; background-position: center; background-size: cover; z-index: -1; } } #slides-aux { position: relative; top: 1.25rem; width: 100%; .slide-title { position: absolute; z-index: 300; font-size: 4vw; font-weight: 700; line-height: 1.3; @include outlined(#fff); } }

メインスライダーを絶対位置に設定し、 background-size: coverプロパティを使用して、背景画像が領域全体に広がるようにしました。 スライドのタイトルとのコントラストを高めるために、オーバーレイとして機能する絶対疑似要素を設定しました。 スライドのタイトルを含む補助スライダーは、画面の下部と画像の上部に配置されています。

一度に表示できるスライドは1つだけなので、各タイトルも絶対値に設定し、カットオフがないことを確認するためにJSでホルダーサイズを計算しますが、これについては次のセクションの1つで詳しく説明します。 ここでは、拡張と呼ばれるSCSS機能の使用法を確認できます。

 %abs { position: absolute; top: 0; left: 0; height: 100%; width: 100%; }

絶対測位を多用したので、このCSSを拡張可能にプルして、さまざまなセレクターで簡単に利用できるようにしました。 また、タイトルとメインスライダーのタイトルをスタイリングするときにDRYアプローチを提供するために、「アウトライン」と呼ばれるミックスインを作成しました。

 @mixin outlined($color: $dark-color, $size: 1px) { color: transparent; -webkit-text-stroke: $size $color; }

このレイアウトの静的な部分については、複雑なことは何もありませんが、ここでは、通常のフローではなくY軸上にある必要があるテキストを配置するときに興味深い方法を見ることができます。

 .slider-title-wrapper { position: absolute; top: $offset; left: calc(100% - #{$offset}); transform-origin: 0% 0%; transform: rotate(90deg); @include outlined; }

このタイプのレイアウトでは実際には十分に活用されていないことがわかったので、 transform-originプロパティに注意を向けたいと思います。 この要素の配置方法は、アンカーが要素の左上隅にとどまり、回転ポイントを設定し、さまざまな画面サイズに関して問題なく、テキストがそのポイントから下に向かって連続的に流れるようにすることです。

より興味深いCSS部分である初期読み込みアニメーションを見てみましょう。

スライダーのアニメーションをロードします。

通常、この種の同期されたアニメーションの動作は、ライブラリを使用して実現されます。たとえば、GSAPは、優れたレンダリング機能を提供し、使いやすく、開発者がプロ​​グラムで要素をチェーンできるタイムライン機能を備えています。お互いに移行します。

ただし、これは純粋なCSS / JSの例であるため、ここでは本当に基本的なものにすることにしました。 したがって、各要素はデフォルトで開始位置に設定されます。変換または不透明度によって非表示になり、JSによってトリガーされるスライダーの読み込み時に表示されます。 すべてのトランジションプロパティは手動で調整され、自然で興味深いフローを確保します。各トランジションは別のトランジションに続き、快適な視覚体験を提供します。

 #logo:after { transform: scaleY(0); transform-origin: 50% 0; transition: transform .35s $easing; } .logo-text { display: block; transform: translate3d(120%, 0, 0); opacity: 0; transition: transform .8s .2s, opacity .5s .2s; } .current, .sep:before { opacity: 0; transition: opacity .4s 1.3s; } #info { transform: translate3d(100%, 0, 0); transition: transform 1s $easing .6s; } .line { transform-origin: 0% 0; transform: scaleX(0); transition: transform .7s $easing 1s; } .slider-title { overflow: hidden; >span { display: block; transform: translate3d(0, -100%, 0); transition: transform .5s 1.5s; } }

ここで見てほしいことが1つあるとすれば、それはtransformプロパティの使用です。 HTML要素を移動するときは、トランジションであろうとアニメーションであろうと、 transformプロパティを使用することをお勧めします。 余白やパディング、さらにはオフセット(上、左など)を使用する傾向があり、レンダリングに関しては適切な結果が得られない人がたくさんいます。

インタラクティブな動作を追加するときにCSSを使用する方法をより深く理解するために、次の記事を十分に推奨することはできませんでした。

これはChromeエンジニアのPaulLewisによるもので、CSSであろうとJSであろうと、Webでのピクセルレンダリングについて知っておくべきほとんどすべてをカバーしています。

JavaScriptの概要とスライダーロジック

JavaScriptファイルは2つの異なる機能に分かれています。

ここで必要なすべての機能を処理するheroSlider関数と、再利用可能なユーティリティ関数をいくつか追加したutils関数です。 プロジェクトでそれらを再利用しようとしている場合にコンテキストを提供するために、これらのユーティリティ関数のそれぞれにコメントしました。

main関数は、 initresizeの2つのブランチを持つようにコーディングされています。 これらのブランチは、main関数のリターンを介して利用可能であり、必要に応じて呼び出されます。 initはメイン関数の初期化であり、ウィンドウロードイベントでトリガーされます。 同様に、サイズ変更ブランチはウィンドウのサイズ変更時にトリガーされます。 サイズ変更機能の唯一の目的は、タイトルのフォントサイズが異なる可能性があるため、ウィンドウのサイズ変更時にタイトルのスライダーサイズを再計算することです。

heroSlider関数では、必要なすべてのデータとセレクターを含むスライダーオブジェクトを提供しました。

 const slider = { hero: document.querySelector('#hero-slider'), main: document.querySelector('#slides-main'), aux: document.querySelector('#slides-aux'), current: document.querySelector('#slider-nav .current'), handle: null, idle: true, activeIndex: -1, interval: 3500 };

補足として、このアプローチは、たとえばReactを使用している場合、データを状態で保存したり、新しく追加されたフックを使用したりできるため、簡単に適応できます。 要点を維持するために、ここでのキーと値のペアのそれぞれが何を表しているかを見てみましょう。

  • 最初の4つのプロパティは、操作するDOM要素へのHTML参照です。
  • handleプロパティは、自動再生機能を開始および停止するために使用されます。
  • idleプロパティは、スライドの移行中にユーザーが強制的にスクロールするのを防ぐフラグです。
  • activeIndexを使用すると、現在アクティブなスライドを追跡できます
  • intervalはスライダーの自動再生間隔を示します

スライダーの初期化時に、2つの関数を呼び出します。

 setHeight(slider.aux, slider.aux.querySelectorAll('.slide-title')); loadingAnimation();

setHeight関数は、ユーティリティ関数にアクセスして、最大タイトルサイズに基づいて補助スライダーの高さを設定します。 このようにして、適切なサイズが提供され、コンテンツが2行に落ちてもスライドタイトルが途切れないようにします。

LoadingAnimation関数は、CSSクラスを要素に追加してイントロCSSトランジションを提供します。

 const loadingAnimation = function () { slider.hero.classList.add('ready'); slider.current.addEventListener('transitionend', start, { once: true }); }

スライダーインジケーターはCSS遷移タイムラインの最後の要素であるため、遷移が終了するのを待って、開始関数を呼び出します。 オブジェクトとして追加のパラメーターを提供することにより、これが1回だけトリガーされるようにします。

start関数を見てみましょう。

 const start = function () { autoplay(true); wheelControl(); window.innerWidth <= 1024 && touchControl(); slider.aux.addEventListener('transitionend', loaded, { once: true }); }

したがって、レイアウトが終了すると、最初の遷移はloadingAnimation関数によってトリガーされ、start関数が引き継ぎます。 次に、自動再生機能をトリガーし、ホイールコントロールを有効にし、タッチデバイスとデスクトップデバイスのどちらを使用しているかを判断し、タイトルスライドの最初の遷移を待って適切なCSSクラスを追加します。

自動再生

このレイアウトのコア機能の1つは、自動再生機能です。 対応する関数を見てみましょう:

 const autoplay = function (initial) { slider.autoplay = true; slider.items = slider.hero.querySelectorAll('[data-index]'); slider.total = slider.items.length / 2; const loop = () => changeSlide('next'); initial && requestAnimationFrame(loop); slider.handle = utils().requestInterval(loop, slider.interval); }

まず、自動再生フラグをtrueに設定して、スライダーが自動再生モードになっていることを示します。 このフラグは、ユーザーがスライダーを操作した後に自動再生を再トリガーするかどうかを決定するときに役立ちます。 次に、すべてのスライダーアイテム(スライド)を参照します。アクティブなクラスを変更し、2つの同期されたスライダーレイアウト(メインと補助)があるため、すべてのアイテムを合計して2で割ることにより、スライダーの合計反復回数を計算します。しかし、両方を同時に変更する「スライダー」自体は1つだけです。

ここでのコードの最も興味深い部分は、ループ関数です。 これはslideChangeを呼び出し、1分後に進むスライドの方向を提供しますが、ループ関数は2、3回呼び出されます。 理由を見てみましょう。

最初の引数がtrueと評価された場合、 requestAnimationFrameコールバックとしてループ関数を呼び出します。 これは、スライドの即時変更をトリガーする最初のスライダーのロード時にのみ発生します。 requestAnimationFrameを使用して、次のフレームの再描画の直前に、提供されたコールバックを実行します。

スライダーの作成に使用される手順の図。

ただし、自動再生モードでスライドを読み続けたいので、この同じ関数を繰り返し呼び出します。 これは通常、setIntervalで実現されます。 ただし、この場合、ユーティリティ関数の1つであるrequestIntervalを使用します。 setIntervalは問題なく機能しますが、 requestIntervalは、 requestAnimationFrameに依存し、よりパフォーマンスの高いアプローチを提供する高度な概念です。 これにより、ブラウザタブがアクティブな場合にのみ、関数が再トリガーされます。

この素晴らしい記事のこの概念の詳細については、CSSのトリックを参照してください。 この関数からの戻り値をslider.handleプロパティに割り当てることに注意してください。 関数が返すこの一意のIDは利用可能であり、後でcancelAnimationFrameを使用して自動再生をキャンセルするために使用します。

スライドチェンジ

slideChange関数は、コンセプト全体の主要な関数です。 自動再生によるか、ユーザートリガーによるかを問わず、スライドを変更します。 スライダーの方向を認識し、ループを提供するため、最後のスライドに到達したときに最初のスライドに進むことができます。 これが私がそれをコーディングした方法です:

 const changeSlide = function (direction) { slider.idle = false; slider.hero.classList.remove('prev', 'next'); if (direction == 'next') { slider.activeIndex = (slider.activeIndex + 1) % slider.total; slider.hero.classList.add('next'); } else { slider.activeIndex = (slider.activeIndex - 1 + slider.total) % slider.total; slider.hero.classList.add('prev'); } //reset classes utils().removeClasses(slider.items, ['prev', 'active']); //set prev const prevItems = [...slider.items] .filter(item => { let prevIndex; if (slider.hero.classList.contains('prev')) { prevIndex = slider.activeIndex == slider.total - 1 ? 0 : slider.activeIndex + 1; } else { prevIndex = slider.activeIndex == 0 ? slider.total - 1 : slider.activeIndex - 1; } return item.dataset.index == prevIndex; }); //set active const activeItems = [...slider.items] .filter(item => { return item.dataset.index == slider.activeIndex; }); utils().addClasses(prevItems, ['prev']); utils().addClasses(activeItems, ['active']); setCurrent(); const activeImageItem = slider.main.querySelector('.active'); activeImageItem.addEventListener('transitionend', waitForIdle, { once: true }); }

アイデアは、HTMLから取得したデータインデックスに基づいてアクティブなスライドを決定することです。 各ステップについて説明しましょう。

  1. スライダーアイドルフラグをfalseに設定します。 これは、スライドの変更が進行中であり、ホイールとタッチのジェスチャが無効になっていることを示しています。
  2. 以前のスライダー方向のCSSクラスがリセットされ、新しいクラスがチェックされます。 方向パラメーターは、自動再生関数から取得する場合はデフォルトで「next」として提供されるか、ユーザーが呼び出した関数であるwheelControlまたはtouchControlによって提供されます。
  3. 方向に基づいて、アクティブなスライドインデックスを計算し、現在の方向のCSSクラスをスライダーに提供します。 このCSSクラスは、使用される遷移効果を決定するために使用されます(たとえば、右から左または左から右)
  4. スライドは、CSSクラスを削除するが、単一のDOM要素ではなく、NodeListで呼び出すことができる別のユーティリティ関数を使用して、「状態」のCSSクラス(前、アクティブ)をリセットします。 その後、以前の現在アクティブなスライドのみがそれらのCSSクラスを追加します。 これにより、CSSはそれらのスライドのみを対象とし、適切なトランジションを提供できます。
  5. setCurrentは、activeIndexに基づいてスライダーインジケーターを更新するコールバックです。
  6. 最後に、アクティブな画像スライドの遷移が終了するのを待って、以前にユーザーによって中断された場合に自動再生を再開するwaitForIdleコールバックをトリガーします。

ユーザーコントロール

画面サイズに基づいて、ホイールとタッチの2種類のユーザーコントロールを追加しました。 ホイールコントロール:

 const wheelControl = function () { slider.hero.addEventListener('wheel', e => { if (slider.idle) { const direction = e.deltaY > 0 ? 'next' : 'prev'; stopAutoplay(); changeSlide(direction); } }); }

ここでは、ホイールをリッスンし、スライダーが現在アイドルモード(現在スライドの変更をアニメーション化していない)の場合、ホイールの方向を決定し、 stopAutoplayを呼び出して自動再生機能が進行中の場合は停止し、方向に基づいてスライドを変更します。 stopAutoplay関数は、autoplayフラグをfalse値に設定し、 cancelRequestIntervalユーティリティ関数を呼び出して適切なハンドルを渡すことにより、間隔をキャンセルする単純な関数にすぎません。

 const stopAutoplay = function () { slider.autoplay = false; utils().clearRequestInterval(slider.handle); }

wheelControlと同様に、タッチジェスチャを処理するtouchControlがあります。

 const touchControl = function () { const touchStart = function (e) { slider.ts = parseInt(e.changedTouches[0].clientX); window.scrollTop = 0; } const touchMove = function (e) { slider.tm = parseInt(e.changedTouches[0].clientX); const delta = slider.tm - slider.ts; window.scrollTop = 0; if (slider.idle) { const direction = delta < 0 ? 'next' : 'prev'; stopAutoplay(); changeSlide(direction); } } slider.hero.addEventListener('touchstart', touchStart); slider.hero.addEventListener('touchmove', touchMove); }

touchstarttouchmoveの2つのイベントをリッスンします。 次に、差を計算します。 負の値が返された場合、ユーザーが右から左にスワイプしたため、次のスライドに移動します。 一方、値が正の場合、つまりユーザーが左から右にスワイプした場合は、「前」として渡された方向でslideChangeをトリガーします。 どちらの場合も、自動再生機能は停止します。

これは非常に単純なユーザージェスチャの実装です。 これに基づいて、前/次のボタンを追加してクリック時にslideChangeをトリガーするか、箇条書きを追加してインデックスに基づいてスライドに直接移動することができます。

CSSのまとめと最終的な考え

これで、最新のトランジション効果を使用して非標準のスライダーレイアウトをコーディングする純粋なCSS/JSの方法になります。

このアプローチが考え方として役立ち、必ずしも従来の方法で設計されたとは限らないプロジェクトをコーディングするときに、フロントエンドプロジェクトで同様のものを使用できることを願っています。

画像遷移効果に興味のある方のために、次の数行でこれについて説明します。

イントロセクションで提供したスライドのHTML構造を再検討すると、各画像スライドの周囲にabs-maskのCSSクラスを持つdivがあることがわかります。 このdivは、 overflow:hiddenを使用し、画像とは異なる方向にオフセットすることで、表示されている画像の一部を一定量非表示にします。 たとえば、前のスライドのコーディング方法を見ると、次のようになります。

 &.prev { z-index: 5; transform: translate3d(-100%, 0, 0); transition: 1s $easing; .abs-mask { transform: translateX(80%); transition: 1s $easing; } }

前のスライドのX軸は-100%オフセットされており、現在のスライドの左側に移動していますが、内側のabs-mask divは右側に80%変換され、ビューポートが狭くなっています。 これは、アクティブなスライドのzインデックスを大きくすることと組み合わせて、一種のカバー効果をもたらします。アクティブな画像は前の画像をカバーすると同時に、マスクを動かして全景を表示することで表示領域を拡大します。