作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Stefan Vitasovic的头像

Stefan Vitasovic

Stefan是一位受现代交互式布局启发的前端工程师. 他参与了数百个项目,专注于高端UI和UX.

Expertise

Years of Experience

9

Share

我经常使用自定义全屏布局,几乎每天都是如此. 通常,这些布局意味着大量的交互和动画. 无论是时间触发的复杂过渡时间线,还是基于滚动的用户驱动事件集, 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 Overview

下面是我们将要使用的基本HTML:

A div with the id of hero-slider is our main holder. 在内部,布局分为几个部分:

  • Logo (a static section)
  • 幻灯片,这是我们主要要做的
  • Info (a static section)
  • 滑块导航,它将指示当前活动的幻灯片以及幻灯片的总数

让我们把重点放在幻灯片部分,因为这是本文的重点. 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 Overview

源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滑块部分——初始加载动画:

Load animation for slider.

通常,这种同步动画行为是使用库来实现的 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概述和全页面滑块逻辑

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, 因为您可以将数据存储在状态中或使用新添加的钩子. 为了保持重点,让我们来看看这里的每个键值对代表什么:

  • 前四个属性是对我们要操作的DOM元素的HTML引用.
  • The handle 属性将用于启动和停止自动播放功能.
  • The 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类.

Autoplay

该布局的核心功能之一是自动播放功能. 我们来看看对应的函数:

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.

Slide Change

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:

  1. Set slider idle flag to false. 这表明幻灯片更改正在进行中,滚轮和触摸手势被禁用.
  2. 之前的滑块方向CSS类被重置,我们检查新的. 如果我们来自自动播放函数,方向参数默认为' next ',或者由用户调用函数-提供wheelControl or touchControl.
  3. Based on the direction, 我们计算活动幻灯片索引,并为滑动条提供当前方向的CSS类. 这个CSS类用于确定将使用哪种过渡效果(例如.g. 从右到左或从左到右)
  4. 幻灯片获得它们的“状态”CSS类, 激活)重置使用另一个实用程序函数,它删除CSS类,但可以在NodeList上调用, 而不仅仅是一个DOM元素. 之后,只有之前和当前活动的幻灯片才会添加这些CSS类. 这允许CSS只针对这些幻灯片并提供适当的过渡.
  5. setCurrent 是一个基于activeIndex更新滑块指示器的回调.
  6. 最后,我们等待活动图像幻灯片的过渡结束,以触发 waitForIdle 如果先前被用户中断,则重新开始自动播放的回调.

User Controls

根据屏幕大小,我添加了两种用户控制方式——滚轮和触摸. 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 单击或添加项目符号列表,可根据其索引直接转到幻灯片.

CSS/JavaScript滑块总结和对CSS的最后思考

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指数相结合,会产生一种覆盖效果——活动图像覆盖之前的图像,同时通过移动遮罩扩展其可见区域,从而提供完整的视图.

Understanding the basics

  • 哪些CSS属性可以动画化?

    我们可以制作动画的最标准的CSS属性是transform, opacity, color, background-color, height, width, etc. 完整的列表可以在Mozilla技术文档中找到.

  • 什么是CSS关键帧动画?

    CSS关键帧动画是以0-100%的时间表示选定元素在指定时间段内发生的所有过渡. 通过这种方式,多个过渡可以组合成一个无缝的视觉表示.

  • What is a transition property?

    过渡是一种属性,它允许CSS属性在两个值之间进行转换. 例如,将鼠标悬停在一个元素上,让它将不透明度从0转换为1.

  • What is CSS specificity?

    通过解释特异性,浏览器决定要解释哪条CSS规则. CSS的特殊性取决于选择器类型:类型选择器、类选择器、ID选择器. Combining multiple selectors, 添加兄弟和子组合子也可以操纵特异性.

  • 浏览器中的CSS:它是如何工作的?

    为了让HTML内容使用CSS样式,浏览器使用以下方法. Upon loading HTML, 它将其内容与所提供的样式信息结合起来, creates a DOM tree, 最后显示其内容.

聘请Toptal这方面的专家.
Hire Now
Stefan Vitasovic的头像
Stefan Vitasovic

Located in Belgrade, Serbia

Member since October 18, 2018

About the author

Stefan是一位受现代交互式布局启发的前端工程师. 他参与了数百个项目,专注于高端UI和UX.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Expertise

Years of Experience

9

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.