使用 CSS 和 JavaScript 構建自定義整頁滑塊

已發表: 2022-03-11

我幾乎每天都使用自定義全屏佈局。 通常,這些佈局意味著大量的交互和動畫。 無論是時間觸發的複雜過渡時間線還是基於滾動的用戶驅動的事件集,在大多數情況下,UI 需要的不僅僅是使用開箱即用的插件解決方案,只需進行一些調整和更改. 另一方面,我看到許多 JavaScript 開發人員傾向於使用他們最喜歡的 JS 插件來簡化他們的工作,即使這項任務可能不需要某個插件提供的所有花里胡哨。

免責聲明:當然,使用可用的眾多插件之一有其好處。 您將獲得多種選項來調整您的需求,而無需進行大量編碼。 此外,大多數插件作者優化他們的代碼,使其跨瀏覽器和跨平台兼容,等等。 但是,您仍然會在項目中包含一個全尺寸的庫,它可能只提供一兩個不同的東西。 我並不是說使用任何類型的第三方插件自然是一件壞事,我在我的項目中每天都這樣做,只是權衡每種方法的利弊通常是個好主意編碼的好習慣。 當以這種方式做你自己的事情時,它需要更多的編碼知識和經驗才能知道你在尋找什麼,但最後,你應該得到一段代碼,它只做一件事,只做一件事你想要它。

本文旨在展示一種純 CSS/JS 方法來開發具有自定義內容動畫的全屏滾動觸發滑塊佈局。 在這個按比例縮小的方法中,我將介紹您期望從 CMS 後端交付的基本 HTML 結構、現代 CSS (SCSS) 佈局技術以及用於完全交互的 vanilla 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>

id 為hero-slider的 div 是我們的主要持有者。 在內部,佈局分為幾個部分:

  • 徽標(靜態部分)
  • 我們將主要處理的幻燈片
  • 信息(靜態部分)
  • 滑塊導航將指示當前活動的幻燈片以及幻燈片的總數

讓我們關注幻燈片部分,因為這是我們在本文中的興趣點。 這裡我們有兩個部分—— mainaux 。 Main 是包含特色圖像的 div,而 aux 包含圖像標題。 這兩個支架內的每張幻燈片的結構都非常基本。 在這裡,我們在主支架內有一張圖片幻燈片:

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

我們將使用索引數據屬性來跟踪我們在幻燈片中的位置。 我們將使用 abs-mask div 來創建有趣的過渡效果,而 slide-image div 包含特定的特色圖像。 圖像被內聯渲染,就好像它們直接來自 CMS 並由最終用戶設置。

同樣,標題在輔助支架內滑動:

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

每個幻燈片標題都是一個帶有相應數據屬性的 H2 標記和一個能夠指向該項目的單個頁面的鏈接。

我們的 HTML 的其餘部分也非常簡單。 我們在頂部有一個徽標,靜態信息告訴用戶他們在哪個頁面上,一些描述和滑塊當前/總指示器。

CSS 概述

源 CSS 代碼是用 SCSS 編寫的,這是一個 CSS 預處理器,然後被編譯成瀏覽器可以解釋的常規 CSS。 SCSS 為您提供了使用變量、嵌套選擇、mixin 和其他很酷的東西的優勢,但它需要編譯成 CSS 才能讓瀏覽器按應有的方式讀取代碼。 出於本教程的目的,我使用 Scout-App 來處理編譯,因為我希望將工具保持在最低限度。

我使用 flexbox 來處理基本的並排佈局。 這個想法是讓幻燈片在一側,信息部分在另一側。

 #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屬性讓背景圖像拉伸整個區域。 為了提供與幻燈片標題的更多對比,我設置了一個絕對偽元素,它充當疊加層。 包含幻燈片標題的輔助滑塊位於屏幕底部和圖像頂部。

由於一次只能看到一張幻燈片,我將每個標題也設置為絕對標題,並通過 JS 計算持有人大小以確保沒有截斷,但更多關於這一點的內容將在我們即將發布的部分中介紹。 在這裡,您可以看到名為擴展的 SCSS 功能的使用:

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

因為我經常使用絕對定位,所以我把這個 CSS 拉到一個可擴展的中,以便在各種選擇器中輕鬆使用它。 此外,我創建了一個名為“outlined”的 mixin,以在設置標題和主滑塊標題的樣式時提供 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; } }

如果我希望您在這裡看到一件事,那就是使用transform屬性。 移動 HTML 元素時,無論是過渡還是動畫,都建議使用transform屬性。 我看到很多人傾向於使用邊距或填充,甚至是偏移量——頂部、左側等,這在渲染時不會產生足夠的結果。

為了更深入地了解如何在添加交互行為時使用 CSS,我推薦以下文章再合適不過了。

它由 Chrome 工程師 Paul Lewis 撰寫,幾乎涵蓋了人們應該知道的關於 Web 中像素渲染的所有內容,無論是 CSS 還是 JS。

JavaScript 概述和滑塊邏輯

JavaScript 文件分為兩個不同的函數。

heroSlider函數處理了我們在這裡需要的所有功能,而utils函數我添加了幾個可重用的實用程序函數。 如果您希望在項目中重用它們,我已經對這些實用函數中的每一個進行了註釋以提供上下文。

main 函數的編碼方式有兩個分支: initresize 。 這些分支可通過主函數的返回獲得,並在必要時被調用。 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,則可以輕鬆調整此方法,因為您可以將數據存儲在狀態中或使用新添加的鉤子。 為了保持重點,讓我們來看看這裡的每個鍵值對代表什麼:

  • 前四個屬性是對我們將要操作的 DOM 元素的 HTML 引用。
  • handle屬性將用於啟動和停止自動播放功能。
  • idle屬性是一個標誌,它將阻止用戶在幻燈片過渡時強制滾動。
  • activeIndex將允許我們跟踪當前活動的幻燈片
  • interval表示滑塊的自動播放間隔

在滑塊初始化時,我們調用兩個函數:

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

setHeight函數使用實用函數來根據最大標題大小設置輔助滑塊的高度。 通過這種方式,我們可以確保提供足夠的大小,並且即使內容分成兩行,幻燈片標題也不會被截斷。

loadingAnimation 函數向提供介紹 CSS 過渡的元素添加一個 CSS 類:

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

由於我們的滑塊指示器是 CSS 過渡時間線中的最後一個元素,我們等待它的過渡結束並調用 start 函數。 通過提供附加參數作為對象,我們確保僅觸發一次。

讓我們看一下啟動函數:

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

因此,當佈局完成後,它的初始轉換由loadingAnimation函數觸發,並由 start 函數接管。 然後它觸發自動播放功能,啟用滾輪控制,確定我們是在觸摸設備還是桌面設備上,並等待標題幻燈片第一次轉換以添加適當的 CSS 類。

自動播放

此佈局的核心功能之一是自動播放功能。 讓我們回顧一下相應的函數:

 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,表示滑塊處於自動播放模式。 在確定用戶與滑塊交互後是否重新觸發自動播放時,此標誌很有用。 然後我們引用所有滑塊項目(幻燈片),因為我們將更改它們的活動類並通過將所有項目相加並除以二來計算滑塊將具有的總迭代次數,因為我們有兩個同步的滑塊佈局(主和輔助)但只有一個“滑塊”本身可以同時改變它們。

這裡代碼中最有趣的部分是循環函數。 它調用slideChange ,提供了我們將在稍後討論的滑動方向,但是,循環函數被調用了幾次。 讓我們看看為什麼。

如果初始參數被評估為真,我們將調用循環函數作為requestAnimationFrame回調。 這只發生在第一個滑塊加載觸發立即幻燈片更改時。 使用requestAnimationFrame我們在下一幀重繪之前執行提供的回調。

用於創建滑塊的步驟圖。

但是,由於我們希望在自動播放模式下繼續瀏覽幻燈片,我們將重複調用相同的函數。 這通常通過 setInterval 來實現。 但在這種情況下,我們將使用其中一個實用函數—— 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 類被重置,我們檢查新的。 如果我們來自自動播放功能或由用戶調用的功能wheelControltouchControl ,方向參數默認提供為“下一個”。
  3. 根據方向,我們計算活動滑動索引並將當前方向 CSS 類提供給滑塊。 這個 CSS 類用於確定將使用哪種過渡效果(例如從右到左或從左到右)
  4. 幻燈片使用另一個實用函數來重置它們的“狀態”CSS 類(prev、active),該實用函數刪除 CSS 類但可以在 NodeList 上調用,而不僅僅是單個 DOM 元素。 之後,只有以前和當前活動的幻燈片才能將這些 CSS 類添加到其中。 這允許 CSS 僅針對那些幻燈片並提供足夠的過渡。
  5. setCurrent是一個回調,它根據 activeIndex 更新滑塊指示器。
  6. 最後,我們等待活動圖像幻燈片的過渡結束,以觸發waitForIdle回調,如果之前被用戶中斷,它將重新啟動自動播放。

用戶控制

根據屏幕尺寸,我添加了兩種類型的用戶控件——滾輪和触摸。 車輪控制:

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

在這裡,即使滑塊當前處於空閒模式(當前沒有動畫幻燈片更改),我們也會監聽滾輪,我們確定滾輪方向,如果正在進行,則調用stopAutoplay停止自動播放功能,並根據方向更改滑塊。 stopAutoplay函數只不過是一個簡單的函數,它將我們的自動播放標誌設置為 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 。 然後,我們計算差異。 如果它返回一個負值,當用戶從右向左滑動時,我們切換到下一張幻燈片。 另一方面,如果值為正數,意味著用戶從左向右滑動,我們會觸發slideChange ,方向傳遞為“previous”。 在這兩種情況下,自動播放功能都會停止。

這是一個非常簡單的用戶手勢實現。 為此,我們可以添加上一個/下一個按鈕以在單擊時觸發slideChange ,或者添加一個項目符號列表以根據其索引直接轉到幻燈片。

關於 CSS 的總結和最終想法

所以你去吧,一種純 CSS/JS 的方式,用現代的過渡效果來編碼一個非標準的滑塊佈局。

我希望您發現這種方法作為一種思維方式很有用,並且在為不一定按常規設計的項目進行編碼時,可以在您的前端項目中使用類似的東西。

對於那些對圖像過渡效果感興趣的人,我將在接下來的幾行中介紹這一點。

如果我們重新訪問我在介紹部分中提供的幻燈片 HTML 結構,我們會看到每張圖片幻燈片都有一個div圍繞它,CSS 類為abs-mask 。 這個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-index 相結合會產生一種覆蓋效果 - 活動圖像覆蓋前一張,同時通過移動提供完整視圖的遮罩來擴展其可見區域。