使用 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 相结合会产生一种覆盖效果 - 活动图像覆盖前一张,同时通过移动提供完整视图的遮罩来扩展其可见区域。