15
头图
Welcome to my public account: front-end detective

Not long ago, in this article: CSS with :has pseudo-classes can be used to introduce some usage scenarios of :has pseudo-classes, which can be said to greatly subvert the cognition of CSS selectors and make a lot of cumbersome The js logic is easily implemented by the flexible CSS . This time I bring a more common case, a 3d carousel , like this

Kapture 2022-09-12 at 14.46.55.gif

This carousel has several points that need to be implemented:

  1. 3d vision, that is, the middle is big and the sides are small
  2. Automatic rotation, automatic pause when the mouse is placed
  3. Clicking on any card will immediately jump to that card

This time, I use :has to achieve such a function. I believe it can bring different ideas. Let's take a look.

⚠️Warm reminder: Chrome 101+ is required for compatibility, and experimental features are started (105+ is officially supported), Safari 15.4+, Firefox officially says that the experimental features can be supported, but the actual measurement does not support

1. 3d visual style

This part is the point.

First, let's make a simple layout. Suppose HTML is like this:

 <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>

To reduce complexity as much as possible, we can focus all the changes on a single class name.

For example .current means the currently selected item, which is the middle item. Here, the absolute positioning method is used to stack all the card items together, and then set the 3d view to move the Z axis of the card back a certain distance, so that the .current --In the front .current all cards, achieve the effect of near big and far small, the specific implementation is as follows

 .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);
}

The effect is as follows

image-20220912151224848

In fact, the hierarchy is like this, which can be seen in the Chrome layer

image-20220912151329501

Now, we need to let the adjacent left and right sides leak out, the right one is easier, just use the adjacent sibling selector +

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

What about the adjacent left? It used to be unsolvable, and it was very troublesome to set different class names through JS, but now there is a :has pseudo-class, which can also be easily implemented, as follows

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

The effect is as follows

image-20220912151819533

However, there are still some critical situations, such as in the first card, because there is no sibling node in front, so it becomes like this

image-20220912152013476

So you need to put the last element on the left side of the first element, the first element is :first-child , and the last element is :last-child , so the implementation is like this (The last element is processed in the same way)

 .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;
}

This takes care of the edge cases

image-20220912152554132

Further, two cards can also be exposed to both sides, and the implementation is similar. The complete implementation is as follows

 /*当前项*/
.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);
}

In this way, all styles of .current are processed, and all changes are controlled with only one variable

image-20220912152858792

2. Automatic rotation and pause

With the above processing, the next logic is very simple, just need to pass the change of js dynamic control .current .

In general, we may think of using a timer setInterval , but here, we can also do not use a timer. With the power of CSS animation, we can more easily complete such an interaction.

If you are interested, you can refer to the previous article: Are you still using a timer? Use CSS to monitor events , I believe it can bring you some inspiration

The thing to do is simple, add an insignificant CSS animation to the container

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

In this way, an animation with a duration of 3s and an infinite loop is obtained.

Then, listen for the animationiteration event. This event will be called back every time the animation runs. Here, it runs every 3s , just like setInterval same function

GlobalEventHandlers.onanimationiteration - Web API Interface Reference | MDN (mozilla.org)

Process .current in the animationiteration callback. The logic is very simple, remove the current .current , and add .current to the next boundary. That's it, the specific implementation is as follows

 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");
  }
});

The biggest advantage of using animationiteration is that the animation can be controlled directly through CSS, and there is no need to monitor mouse movement events.

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

The effect is as follows (convenient for demonstration, the speed is increased)

Kapture 2022-09-12 at 15.43.28

3. Click to switch quickly

Click to switch, my first thought is to pass :checked , which is similar to radio, such as

 <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>

But at present :has pseudo-class does not seem to support multi-level nesting , such as the following statement

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

.item:has(:checked) The selected parent is the selected parent of the child element, and then .item:has(+.item:has(:checked)) means that the previous sibling node is selected, so that the selection function can be realized, but unfortunately it is not supported now 😌( may be supported in the future)

There is no way, it can only be achieved by traditional methods, directly binding to monitor click event

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

The effect is as follows

Kapture 2022-09-12 at 16.01.23.gif

The complete code can be viewed online demo: CSS 3dscroll(runjs.work)

Fourth, to summarize

The above is to use the :has pseudo-class to realize all the details of a 3d carousel. All visual changes are done in CSS, and JS only needs to deal with the switching logic. The above is more concise and elegant, and the following summarizes

  1. 3d visual style can be achieved by transform-style: preserve-3d; to achieve the effect of near big and far small
  2. By .item:has(+.item.current) can set the sibling node in front of the current item
  3. The first and last two critical cases also need to be considered
  4. The automatic rotation and pause of the carousel image can be called back by animationiteration . The advantage of this method is that it can be controlled by :hover
  5. :has Pseudo-class does not seem to support multi-level nesting , I hope it can be supported in the future~

:has very powerful, the only flaw so far is compatibility. Fortunately, browsers are more active in following up this new feature, and it can be used in internal projects by this time next year. Finally, if you think it's good and helpful to you, please like, bookmark, and forward ❤❤❤

Welcome to my public account: front-end detective

XboxYan
18.1k 声望14.1k 粉丝