作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Stefan是一位受现代交互式布局启发的前端工程师. 他参与了数百个项目,专注于高端UI和UX.
9
我经常使用自定义全屏布局,几乎每天都是如此. 通常,这些布局意味着大量的交互和动画. 无论是时间触发的复杂过渡时间线,还是基于滚动的用户驱动事件集, in most cases, UI需要的不仅仅是使用一个开箱即用的插件解决方案,只需进行一些调整和更改. On the other hand, I see many JavaScript developers 倾向于使用他们最喜欢的滑块JS插件来使他们的工作更容易, 即使任务可能不需要某个插件提供的所有花哨功能.
Disclaimer: 当然,使用众多可用的插件之一也有它的好处. 您将获得多种选项,可以根据您的需要进行调整,而无需编写太多代码. Also, 大多数插件作者都会优化他们的代码, 使其跨浏览器和跨平台兼容, and so on. But still, 项目中包含了一个完整大小的库,它可能只提供了一两个不同的功能. 我并不是说使用任何形式的第三方插件自然是一件坏事, 在我的项目中,我每天都这样做, 只是权衡每种方法的利弊通常是个好主意,因为这是编码中的一种良好实践. 当你用这种方式做自己的事情时, 它需要更多的编码知识和经验来知道你在寻找什么, but in the end, 你应该得到一段代码,它只按你想要的方式做一件事.
本文的目的是展示一个 pure CSS使用/JS方法开发带有自定义内容动画的全屏滚动触发滑块布局. In this scaled-down approach, 我将介绍您期望从CMS后端交付的基本HTML结构, modern CSS (SCSS)布局技术,以及用于完全交互性的普通JavaScript编码. Being bare-bones, 这个概念可以很容易地扩展到更大规模的插件和/或在各种核心没有依赖的应用程序中使用.
我们要创建的设计是一个极简主义的建筑师组合展示与特色图像和每个项目的标题. CSS中带有动画的完整滑块看起来是这样的:
You can check out the demo here, and you can access my Github repo for further details.
下面是我们将要使用的基本HTML:
A div with the id of hero-slider
is our main holder. 在内部,布局分为几个部分:
让我们把重点放在幻灯片部分,因为这是本文的重点. Here we have two parts— main and aux. Main是包含特色图像的div,而aux保存图像标题. 这两个支架内的每个幻灯片的结构都很基本. 这里我们有一个图像幻灯片在主支架内:
index data属性将用于跟踪我们在幻灯片中的位置. 我们将使用abs-mask div创建有趣的过渡效果,slide-image div包含特定的特征图像. 图像内联呈现,就像它们直接来自CMS一样,并由最终用户设置.
类似地,标题在aux holder内部滑动:
#64 Paradigm
每个幻灯片标题都是一个H2标记,带有相应的数据属性和一个链接,可以指向该项目的单个页面.
HTML的其余部分也非常简单. We have a logo at the top, 静态信息,告诉用户他们在哪个页面上, some description, 和滑块电流/总指示.
源CSS代码写在 SCSS,一个CSS预处理器,然后将其编译成浏览器可以解释的常规CSS. SCSS为您提供了使用变量的优势, nested selection, mixins, and other cool stuff, 但它需要被编译成CSS让浏览器读取代码,因为它应该. 在本教程中,我使用 Scout-App 来处理编译,因为我想要最少的工具.
我使用flexx来处理基本的并排布局. 这个想法是把幻灯片放在一边,信息部分放在另一边.
#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
property. 以提供与幻灯片标题更多的对比, 我已经设置了一个绝对的伪元素作为覆盖. 包含幻灯片标题的辅助滑块位于屏幕的底部和图像的顶部.
因为一次只能显示一张幻灯片, 我也将每个标题设置为绝对值, 并通过JS计算支架尺寸,以确保没有截断, 但在我们即将到来的章节中会有更多的内容. 在这里你可以看到SCSS特性扩展的用法:
%abs {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
因为我经常使用绝对定位, 我把这个CSS拉到一个可扩展的样式中,以便在各种选择器中轻松使用. Also, 我创建了一个名为“概述”的mixin,以在样式化标题和主滑块标题时提供DRY方法.
@mixin outline ($color: $dark-color, $size: 1px) {
color: transparent;
-webkit-text-stroke: $size $color;
}
至于这个布局的静态部分, 它没有什么复杂的,但在这里你可以看到一个有趣的方法,当定位文本必须在Y轴上,而不是正常的流:
.slider-title-wrapper {
position: absolute;
top: $offset;
左:calc(100% - #{$offset});
transform-origin: 0% 0%;
transform: rotate(90deg);
@include outlined;
}
我想提请大家注意 transform-origin
因为我发现它在这种布局中没有得到充分利用. 这个元素的定位方式是它的锚点停留在元素的左上角, 设置旋转点并让文本从该点持续向下流动,当涉及到不同的屏幕尺寸时没有问题.
让我们来看一个更有趣的CSS滑块部分——初始加载动画:
通常,这种同步动画行为是使用库来实现的 GSAP, for example, is one of the best out there, 提供出色的渲染功能, is easy to use, 并且具有时间轴功能,使开发人员能够以编程方式将元素转换到彼此之间.
However, as this is a pure CSS我已经决定去真正的基本在这里. 因此,每个元素默认设置为其起始位置——通过变换或不透明度隐藏,并在由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);
过渡:变换15 $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
property. 移动HTML元素时,无论是过渡还是动画,都建议使用 transform
property. 我看到很多人倾向于使用边距或内边距,甚至是offset - top, left等等. 当涉及到渲染时,哪个不能产生足够的结果.
以更深入地掌握在添加交互行为时如何使用CSS, I couldn’t recommend the following article enough.
It’s by Paul Lewis, a Chrome engineer, 几乎涵盖了你应该知道的所有关于网络像素渲染的知识,无论是CSS还是JS.
JavaScript滑块动画文件分为两个不同的函数.
The heroSlider
函数,它负责我们这里需要的所有功能,还有 utils
函数,其中我添加了几个可重用的实用函数. 如果您希望在项目中重用它们,我已经注释了这些实用程序函数中的每一个,以提供上下文.
main函数的编码方式有两个分支: init
and resize
. 这些分支可以通过main函数的返回获得,并在必要时调用. init
是初始化的主要功能,它是触发的窗口加载事件. 类似地,调整大小分支是在窗口调整大小时触发的. resize函数的唯一目的是重新计算窗口调整大小时标题的滑块大小, as title font size may vary.
In the heroSlider
function, 我已经提供了一个slider对象,它包含了我们需要的所有数据和选择器:
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
};
As a side-note, 这种方法可以很容易地适应,例如,如果你使用React, 因为您可以将数据存储在状态中或使用新添加的钩子. 为了保持重点,让我们来看看这里的每个键值对代表什么:
handle
属性将用于启动和停止自动播放功能.idle
属性是一个标志,它将防止用户在幻灯片处于过渡状态时强制滚动.activeIndex
将允许我们跟踪当前活动的幻灯片interval
表示滑块的自动播放间隔在滑动块初始化时,我们调用两个函数:
setHeight(slider.aux, slider.aux.querySelectorAll('.slide-title'));
loadingAnimation();
The setHeight
函数调用一个实用函数,根据最大标题大小设置辅助滑块的高度. 这样我们可以确保提供足够的尺寸,并且即使幻灯片的内容下降到两行,也不会切断幻灯片标题.
loadingAnimation函数为元素添加一个CSS类,提供CSS过渡:
const loadingAnimation = function () {
slider.hero.classList.add('ready');
slider.current.addEventListener('transitionend', start, {
once: true
});
}
因为我们的滑块指示器是CSS过渡时间轴上的最后一个元素, 我们等待它的转换结束并调用start函数. 通过提供附加参数作为对象,我们可以确保这只触发一次.
让我们看一下start函数:
const start = function () {
autoplay(true);
wheelControl();
window.innerWidth <= 1024 && touchControl();
slider.aux.addEventListener('transitionend', loaded, {
once: true
});
}
因此,当布局完成时,它的初始转换由 loadingAnimation
函数和start函数接管. 然后触发自动播放功能, enables wheel control, 确定我们是在触摸设备上还是在桌面设备上, 并等待标题滑动的第一个过渡来添加适当的CSS类.
该布局的核心功能之一是自动播放功能. 我们来看看对应的函数:
Const autoplay = function (initial) {
slider.autoplay = true;
slider.items = slider.hero.querySelectorAll(“[材料指数]”);
slider.total = slider.items.length / 2;
const loop = () => changeSlide('next');
initial && requestAnimationFrame(loop);
slider.handle = utils().requestInterval(loop, slider.interval);
}
首先,我们将自动播放标志设置为true,表示滑块处于自动播放模式. 当确定用户与滑块交互后是否重新触发自动播放时,此标志很有用. 然后我们引用所有滑动条项(幻灯片), 因为我们将改变它们的活动类,并通过将所有项目相加并除以2来计算滑块将拥有的总迭代,因为我们有两个同步的滑块布局(主滑块和辅助滑块),但只有一个“滑块”本身同时改变它们.
代码中最有趣的部分是循环函数. It invokes slideChange
, 提供幻灯片方向,我们一会儿会讲到, however, 循环函数被调用了几次. Let’s see why.
如果初始参数的值为true,则调用循环函数 requestAnimationFrame
callback. 这只发生在第一次滑动条加载触发立即滑动变化. Using requestAnimationFrame
我们在下一个帧重绘之前执行提供的回调.
However, 当我们想在自动播放模式下继续播放幻灯片时,我们将重复调用这个函数. 这通常是通过setInterval实现的. 但在这个例子中,我们将使用其中一个效用函数-requestInterval
. While setInterval
would work just well, requestInterval
一个先进的概念是靠什么 requestAnimationFrame
并提供了一种性能更高的方法. 它确保只有在浏览器选项卡处于活动状态时才会重新触发函数.
在这篇很棒的文章中可以找到更多关于这个概念的内容 CSS tricks. 请注意,我们将这个函数的返回值赋给了 slider.handle
property. 函数返回的唯一ID对我们是可用的,我们将使用它来取消稍后使用的自动播放 cancelAnimationFrame
.
The slideChange
函数是整个概念中的主要函数. 它可以通过自动播放或用户触发来改变幻灯片. 它能感知滑块的方向, 提供循环,所以当你到最后一张幻灯片时,你可以继续到第一张幻灯片. Here’s how I’ve coded it:
const changeSlide =函数(方向){
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['上一页']);
utils().addClasses (activeItems['主动']);
setCurrent();
const activeImageItem =滑动条.main.querySelector('.active');
activeImageItem.addEventListener('transitionend', waitForIdle, {
once: true
});
}
我们的想法是根据从HTML中获得的数据索引来确定活动幻灯片. Let’s address each step:
wheelControl
or touchControl
.setCurrent
是一个基于activeIndex更新滑块指示器的回调.waitForIdle
如果先前被用户中断,则重新开始自动播放的回调.根据屏幕大小,我添加了两种用户控制方式——滚轮和触摸. Wheel control:
const wheelControl = function () {
slider.hero.addEventListener('wheel', e => {
if (slider.idle) {
const direction = e.deltaY > 0 ? 'next' : 'prev';
stopAutoplay();
changeSlide(direction);
}
});
}
Here, 我们甚至监听轮子,如果滑块当前处于空闲模式(当前没有动画滑动变化),我们确定轮子的方向, invoke stopAutoplay
停止正在进行的自动播放功能,并根据方向改变幻灯片. The stopAutoplay
函数只是一个简单的函数,它将自动播放标志设置为false值,并通过调用取消间隔 cancelRequestInterval
传递适当句柄的实用函数:
const stopAutoplay = function () {
slider.autoplay = false;
utils().clearRequestInterval(slider.handle);
}
Similar to wheelControl
, we have 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);
}
We listen for two events: touchstart
and touchmove
. 然后,计算差值. 如果它返回一个负值, 当用户从右向左滑动时,我们切换到下一张幻灯片. On the other hand, if the value is positive, 这意味着用户已经从左向右滑动, we trigger slideChange
用方向传为“前”.“在这两种情况下,自动播放功能都会停止.
这是一个非常简单的用户手势实现. 在此基础上,我们可以添加previous/next按钮来触发 slideChange
单击或添加项目符号列表,可根据其索引直接转到幻灯片.
So there you go, a pure CSS/JS的方式编码一个非标准的滑块布局与现代过渡效果.
我希望您发现这种方法作为一种思维方式是有用的,并且在为不一定按惯例设计的项目编码时,可以在您的前端项目中使用类似的方法.
对于那些对图像过渡效果感兴趣的人,我将在接下来的几行中讨论这个问题.
如果我们回顾一下我在介绍部分中提供的幻灯片HTML结构,就会发现每个图像幻灯片都有一个 div
的CSS类 abs-mask
. What this 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%, 将其移动到当前幻灯片的左侧, however, the inner abs-mask
Div向右平移80%,提供更窄的视口. This, 与活动幻灯片的较大z指数相结合,会产生一种覆盖效果——活动图像覆盖之前的图像,同时通过移动遮罩扩展其可见区域,从而提供完整的视图.
我们可以制作动画的最标准的CSS属性是transform, opacity, color, background-color, height, width, etc. 完整的列表可以在Mozilla技术文档中找到.
CSS关键帧动画是以0-100%的时间表示选定元素在指定时间段内发生的所有过渡. 通过这种方式,多个过渡可以组合成一个无缝的视觉表示.
过渡是一种属性,它允许CSS属性在两个值之间进行转换. 例如,将鼠标悬停在一个元素上,让它将不透明度从0转换为1.
通过解释特异性,浏览器决定要解释哪条CSS规则. CSS的特殊性取决于选择器类型:类型选择器、类选择器、ID选择器. Combining multiple selectors, 添加兄弟和子组合子也可以操纵特异性.
为了让HTML内容使用CSS样式,浏览器使用以下方法. Upon loading HTML, 它将其内容与所提供的样式信息结合起来, creates a DOM tree, 最后显示其内容.
Located in Belgrade, Serbia
Member since October 18, 2018
Stefan是一位受现代交互式布局启发的前端工程师. 他参与了数百个项目,专注于高端UI和UX.
9
世界级的文章,每周发一次.
世界级的文章,每周发一次.
Join the Toptal® community.