欢送关注我的公众号:前端侦探

前不久在这篇文章:CSS 有了:has伪类能够做些什么 中介绍了:has伪类的一些应用场景,能够说大大颠覆了 CSS 选择器的认知,让很多繁琐的 js逻辑通过灵便的CSS轻易实现了。这次带来一个比拟常见的案例,3d 轮播图,就像这样的

这个轮播图有几个须要实现的点:

  1. 3d 视觉,也就是两头大,两边小
  2. 主动轮播,鼠标放上主动暂停
  3. 点击任意卡片会立刻跳转到该卡片

这次借助:has来实现这样的性能,置信能够带来不一样的思路,一起看看吧

⚠️舒适揭示:兼容性要求须要 Chrome 101+,并且开始试验个性(105+正式反对),Safari 15.4+,Firefox 官网说开启试验个性能够反对,然而实测并不反对

一、3d 视觉款式

这一部分才是重点。

首先咱们来简略布局一下,假如HTML是这样的:

<div class="view" id="view">  <div class="item">1</div>  <div class="item">2</div>  <div class="item">3</div>  <div class="item">4</div>  <div class="item current">5</div>  <div class="item">6</div>  <div class="item">7</div>  <div class="item">8</div>  <div class="item">9</div>  <div class="item">10</div></div>

为了尽可能减少复杂度,咱们能够将所有的变动都集中在一个类名上。

比方.current示意以后选中项,也就是两头的那项。这里采纳相对定位的形式,将所有卡片项都重叠在一起,而后设置3d视图,将卡片的Z轴往后挪动一段距离,让.current在所有卡片的最后面,实现近大远小的成果,具体实现如下

.view {  position: relative;  width: 400px;  height: 250px;  transform-style: preserve-3d;    perspective: 500px;}.item {  position: absolute;  width: 100%;  height: 100%;  border-radius: 8px;  display: grid;  place-content: center;  box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.1);  transform: translate3d(0, 0, -100px);}.item.current {  transform: translate3d(0, 0, 0);}

成果如下

其实层级是这样的,能够在 Chrome 图层中看到

当初,咱们须要让相邻左右两边的都漏出来,左边的比拟容易,用相邻兄弟选择器+就能够了

.item.current + .item{  transform: translate3d(30%, 0, -100px);}

那相邻右边的呢?以前是无解的,只能通过 JS 别离设置不同的类名,十分麻烦,但当初有了:has伪类,也能够轻松实现了,如下

.item:has(+.item.current){  transform: translate3d(-30%, 0, -100px);}

成果如下

不过还有一些临界状况,比方在第一个卡片时,因为后面没有兄弟节点了,所以就成了这样

所以须要再在第一个元素时,把最初一个元素放在它的右边,第一个元素是:first-child,最初一个元素是:last-child,所以实现是这样的(最初一个元素解决同理)

.item.current:first-child ~ .item:last-child {  transform: translate3d(-30%, 0, -100px);  opacity: 1;}.item:first-child:has(~ .item.current:last-child) {  transform: translate3d(30%, 0, -100px);  opacity: 1;}

这样就解决好了边界状况

进一步,还能够向两侧露出两个卡片,实现也是相似的,残缺实现如下

/*以后项*/.item.current {  transform: translate3d(0, 0, 0);}/*以后项右1*/.item.current + .item,.item:first-child:has(~ .item.current:last-child) {  transform: translate3d(30%, 0, -100px);}/*以后项左1*/.item:has(+ .item.current),.item.current:first-child ~ .item:last-child {  transform: translate3d(-30%, 0, -100px);}/*以后项右2*/.item.current + .item + .item,.item:first-child:has(~ .item.current:nth-last-child(2)), .item:nth-child(2):has(~ .item.current:last-child) {  transform: translate3d(50%, 0, -150px);}/*以后项左2*/.item:has(+.item + .item.current),.item.current:first-child ~ .item:nth-last-child(2),.item.current:nth-child(2) ~ .item:last-child{  transform: translate3d(-50%, 0, -150px);}

这样就实现了对于.current的全副款式解决了,只用一个变量就管制了所有变动

二、主动轮播和暂停

有了下面的解决,接下来的逻辑就非常简单了,只须要通过js动态控制.current的变动就行了。

个别状况下,咱们可能会想到用定时器setInterval,但这里,咱们也能够不应用定时器,借助 CSS 动画的力量,能够更轻松地残缺这样的交互。

有趣味的能够参考之前这篇文章:还在用定时器吗?借助 CSS 来监听事件,置信能够给你带来一些灵感

要做的事件很简略,给容器增加一个无关紧要的 CSS 动画

.view {  /**/  animation: scroll 3s infinite;}@keyframes scroll {  to {    transform: translateZ(.1px); /*无关紧要的动画款式*/  }}

这样就失去了一个时长为3s,有限循环的动画了。

而后,监听animationiteration事件,这个事件在动画每次运行一次就会回调一次,在这里也就是每3s运行一次,就像setInterval的性能一样

GlobalEventHandlers.onanimationiteration - Web API 接口参考 | MDN (mozilla.org)

animationiteration回调中解决.current逻辑,很简略,移除以后的.current,给下一个增加.current,留神一下边界就行了,具体实现如下

view.addEventListener("animationiteration", () => {  const current = view.querySelector(".current") || view.firstElementChild;  current.classList.remove("current");  if (current.nextElementSibling) {    current.nextElementSibling.classList.add("current");  } else {    view.firstElementChild.classList.add("current");  }});

应用animationiteration的最大益处是,能够间接通过 CSS 进行动画的管制,再也无需监听鼠标移入移出事件了

.view:hover{  animation-play-state: paused;}

成果如下(不便演示,速度调快了)

三、点击疾速切换

点击切换,我其实最先想到的是通过:checked,也就是相似单选,比方

<div class="view" id="view">  <label class="item"><input type="radio" checked name="item"></label>  <label class="item"><input type="radio" name="item"></label>  <label class="item"><input type="radio" name="item"></label>  <label class="item"><input type="radio" name="item"></label>  <label class="item"><input type="radio" name="item"></label>  <label class="item"><input type="radio" name="item"></label></div>

然而目前来看:has伪类貌似不反对多层嵌套,比方上面这条语句

.item:has(+.item:has(:checked)){  /*不失效*/}

.item:has(:checked)选中的是子元素被选中的父级,而后.item:has(+.item:has(:checked))示意选中它的后面一个兄弟节点,这样就能实现选中性能了,惋惜当初并不反对(当前可能反对)

没方法,只能通过传统形式来实现,间接绑定监听click事件

view.addEventListener("click", (ev) => {  const current = view.querySelector(".current") || view.firstElementChild;  current.classList.remove("current");  ev.target.closest('.item').classList.add("current");});

成果如下

残缺代码能够查看线上 demo:CSS 3dscroll(runjs.work)

四、总结一下

以上就是借助:has伪类来实现一个3d轮播图的全副细节了,所有的视觉变动全副在 CSS 中实现,JS 只须要解决切换逻辑就行了,相比以前而言,实现上更加简洁和优雅,上面总结一下

  1. 3d 视觉款式能够通过transform-style: preserve-3d;实现近大远小的成果
  2. 通过.item:has(+.item.current)能够设置以后项后面的兄弟节点
  3. 还须要思考第一个和最初一个这两种临界状况
  4. 轮播图主动轮播和暂停能够借助animationiteration回调,这种形式的劣势是能够通过:hover管制
  5. :has伪类貌似不反对多层嵌套,心愿当前能够反对吧~

:has十分弱小,目前惟一的缺点在于兼容性。好在浏览器对于这一新个性跟进的都比拟踊跃,明年这个时候差不多能够在外部我的项目用起来了。最初,如果感觉还不错,对你有帮忙的话,欢送点赞、珍藏、转发❤❤❤

欢送关注我的公众号:前端侦探