I'm currently using Slick in order to make a carousel.
I'm having two issues right now, let's start with the first one.
1)
I'm currently using a slider in which i want to show 3 slides: the current image (Spyro), the previous one (Crash) and the next one (Tekken).
As you see, while the current slide correctly overlaps the previous one (Spyro > Crash), the next one overlaps the current slide (Tekken > Spyro).
Of course i want the current slide to be on top of both of them... How can i fix this?
I'm attacching the code below.
App.js
import "./App.css";
import { useEffect, useState } from "react";
import Slider from "react-slick";
import SliderData from "./SliderData";
import { AiOutlineArrowLeft, AiOutlineArrowRight } from "react-icons/ai";
function useWindowSize() {
const [size, setSize] = useState([window.innerHeight, window.innerWidth]);
useEffect(() => {
const handleResize = () => setSize([window.innerHeight, window.innerWidth]);
window.addEventListener("resize", handleResize);
}, [])
return size;
}
const array = SliderData.map((x) => {
return x.image;
})
console.log(array);
function App() {
const NextArrow = ({ onClick }) => {
return (
<div className="arrow next" onClick={onClick}>
<AiOutlineArrowRight />
</div>
);
};
const PrevArrow = ({ onClick }) => {
return (
<div className="arrow prev" onClick={onClick}>
<AiOutlineArrowLeft />
</div>
);
};
const [imageIndex, setImageIndex] = useState(0);
const [height, width] = useWindowSize();
const settings = {
className: "center",
infinite: true,
lazyLoad: true,
speed: 300,
slidesToShow: width > 1000 ? 3: 1,
centerMode: true,
centerPadding: "60px",
nextArrow: <NextArrow />,
prevArrow: <PrevArrow />,
beforeChange: (current, next) => {
console.log(current);
setImageIndex(next);
}
};
return (
<div className="App">
<Slider {...settings}>
{array.map((img, idx) => (
<div className={idx === imageIndex ? "slide activeSlide" : "slide"}>
<img src={img} alt={img} />
</div>
))}
</Slider>
</div>
);
}
export default App;
App.css
#import "~slick-carousel/slick/slick.css";
#import "~slick-carousel/slick/slick-theme.css";
.App {
width: 100%;
margin: 10rem auto;
height: 570px;
}
.slide img {
width: 35rem;
align-items: center;
margin: 0 auto;
z-index: 1;
}
.slide {
transform: scale(0.8);
transition: transform 300ms;
opacity: 0.5;
z-index: -1;
}
.activeSlide {
transform: scale(1.1);
align-items: center;
opacity: 1;
}
.arrow {
background-color: #fff;
position: absolute;
cursor: pointer;
z-index: 10;
}
.arrow svg {
transition: color 300ms;
}
.arrow svg:hover {
color: #68edff;
}
.next {
right: 3%;
top: 50%;
}
.prev {
left: 3%;
top: 50%;
}
SliderData.js
const SliderData = [
{
image:
"https://www.spaziogames.it/wp-content/uploads/2020/06/Crash-4-Pirate_06-29-20.jpg"
},
{
image:
"https://d2skuhm0vrry40.cloudfront.net/2018/articles/2018-07-18-14-24/news-videogiochi-spyro-reignited-trilogy-video-di-gameplay-livello-colossus-1531920251281.jpg/EG11/thumbnail/750x422/format/jpg/quality/60"
},
{
image: "https://i.ytimg.com/vi/OUh82pOFGDU/maxresdefault.jpg"
},
{
image: "https://www.psu.com/wp/wp-content/uploads/2020/07/MetalGearSolidRemake-1024x576.jpg"
}
];
export default SliderData;
2)
As you see, the active slide is not perfectly centered. Since i suck in CSS i did not use the display: flex; command.
What do you suggest? how can i fix this?
Thank you all.
You need to apply a position element to .slide for the z-index to work properly.
Note: z-index only works on positioned elements (position: absolute, position: relative, position: fixed, or position: sticky) and flex items (elements that are direct children of display:flex elements).
You can read more on z-index here
This is my answer for this issue. Basically you should set position and z-index for every item and set higher z-index for current active item
.App .slick-slide {
position: relative;
z-index: 1;
/* your choice, but make sure z-index of active slide is higher than this value */
}
.App .slick-slide.slick-current {
z-index: 10;
}
Related
I got inspired to create this animation effect. What I want to achieve is that the overlapped images get a little bigger when scrolling down and then again smaller when I scroll back.
For the scrolling part I know I need to use Intersection Observer API. I think I managed it to do right but I cant get it to work. I use React Typescript with inline styling.
The original animation - Three overlaping images - getting bigger on scroll down:
Codepen
My React Code - OverlappingImages.tsx :
import React from 'react';
const styles = {
container: {
position: 'relative',
height: '400px',
margin: '0 50px',
div: {
width: '380px',
border: '1px solid #000',
overflow: 'hidden',
lineHeight: 0,
transition: 'transform .4s ease-in-out',
img: {
width: '100%',
fontSize: 0,
},
},
img1: {
left: '5%',
top: 0,
position: 'absolute',
transform: 'rotate(-4deg) translateY(20%)',
transitionDelay: '0s',
},
img2: {
left: '50%',
top: 0,
position: 'absolute',
transform: 'translate(-50%, 0)',
transitionDelay: '.1s',
zIndex: 1,
},
img3: {
right: '5%',
top: 0,
position: 'absolute',
transform: 'rotate(4deg) translateY(20%)',
transitionDelay: '.2s',
},
' &.active': {
img1: {
transform: 'rotate(-6deg) translateY(50%) scale(1.9)',
},
img2: {
transform: 'translate(-50%, -2%) scale(1.2)',
},
img3: {
transform: 'rotate(6deg) translateY(24%) scale(1.2)',
},
},
},
body: {
fontFamily: 'sans-serif',
fontSize: '48px',
fontWeight: 'bold',
letterSpacing: '1px',
margin: 0,
},
section: {
textAlign: 'center',
padding: '500px 0',
'&:nth-child(odd)': {
background: '#eee',
},
},
};
function OverlappingImages() {
const wrapper = document.querySelector('.container');
const className = 'active';
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
wrapper.classList.add(className);
return;
}
wrapper.classList.remove(className);
});
},
{
threshold: 1,
}
);
observer.observe(wrapper);
return (
<>
<section>
<p>(scroll down!)</p>
</section>
<section>
<div style={styles.container}>
<div style={styles.container.img1}>
<img src="https://via.placeholder.com/350x250" alt="img1" />
</div>
<div style={styles.container.img2}>
<img src="https://via.placeholder.com/350x250" alt="img2" />
</div>
<div style={styles.container.img3}>
<img src="https://via.placeholder.com/350x250" alt="img3" />
</div>
</div>
</section>
<section>
<p>(scroll up!)</p>
</section>
</>
);
}
export { OverlappingImages };
Here's the result:
You need to wrap your code above reutrn(), into the window.onload because if you run it in the way your currently doing it, document.querySelector('.container') is going to return nothing but null or undefined
Your container has no class or id and your trying to access it with document.querySelector('.container') again you'll get null
Make sure you assign an id or a class to it
Style.css
#container * {
transition: all .5s ease;
}
.active div:nth-child(1) {
transform: rotate(-4deg) translateY(20%) scale(1.1) !important;
}
.active div:nth-child(2) {
transform: translate(-50%, 0%) scale(1.1) !important;
}
.active div:nth-child(3) {
transform: rotate(4deg) translateY(20%) scale(1.1) !important;
}
OverlappingImages.tsx
const styles = {
container: {
position: "relative",
height: "400px",
margin: "0 50px",
padding: "30px",
transition: "all .5s ease",
img1: {
left: "5%",
top: 0,
position: "absolute",
transform: "rotate(-4deg) translateY(20%)",
transitionDelay: "0s",
},
img2: {
left: "50%",
top: 0,
position: "absolute",
transform: "translate(-50%, 0)",
transitionDelay: ".1s",
zIndex: 1,
},
img3: {
right: "5%",
top: 0,
position: "absolute",
transform: "rotate(4deg) translateY(20%)",
transitionDelay: ".2s",
},
},
whiteSpace: {
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "100vh",
},
};
function OverlappingImages() {
window.onload = function () {
const wrapper = document.querySelector("#container");
const className = "active";
let preY = 0, preR = 0;
const observer = new IntersectionObserver(
entries => {
entries.forEach(e => {
const currentY = e.boundingClientRect.y;
const currentR = e.intersectionRatio;
if (currentY < preY || e.isIntersecting) {
wrapper?.classList.add(className);
} else if (currentY > preY && currentR < preR) {
wrapper?.classList.remove(className);
}
preY = currentY;
preR = currentR;
});
},
{ threshold: 0.8 }
);
observer.observe(wrapper);
};
return (
<>
<section>
<div style={styles.whiteSpace}>
<p>(scroll down!)</p>
</div>
</section>
<section>
<div style={styles.container} id="container">
<div style={styles.container.img1}>
<img src="https://via.placeholder.com/350x250" alt="img1" />
</div>
<div style={styles.container.img2}>
<img src="https://via.placeholder.com/350x250" alt="img2" />
</div>
<div style={styles.container.img3}>
<img src="https://via.placeholder.com/350x250" alt="img3" />
</div>
</div>
</section>
<section>
<div style={styles.whiteSpace}>
<p>(scroll up!)</p>
</div>
</section>
</>
);
}
export default OverlappingImages;
Second approach(using ref)
Style.css
.active div:nth-child(1) {
transform: rotate(-4deg) translateY(20%) scale(1.1) !important;
}
.active div:nth-child(2) {
transform: translate(-50%, 0%) scale(1.1) !important;
}
.active div:nth-child(3) {
transform: rotate(4deg) translateY(20%) scale(1.1) !important;
}
OverlappingImages.tsx
import {useRef, useEffect} from 'react';
const styles = {
container: {
position: "relative",
height: "400px",
margin: "0 50px",
padding: "30px",
img1: {
left: "5%",
top: 0,
position: "absolute",
transform: "rotate(-4deg) translateY(20%)",
transition: "all .5s ease",
},
img2: {
left: "50%",
top: 0,
position: "absolute",
transform: "translate(-50%, 0)",
transition: "all .5s ease .1s",
zIndex: 1,
},
img3: {
right: "5%",
top: 0,
position: "absolute",
transform: "rotate(4deg) translateY(20%)",
transition: "all .5s ease .2s",
},
},
whiteSpace: {
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "100vh",
},
};
function OverlappingImages() {
const ref = useRef(null);
useEffect(()=>{
const wrapper = ref.current;
const className = "active";
let preY = 0, preR = 0;
const observer = new IntersectionObserver(
entries => {
entries.forEach(e => {
const currentY = e.boundingClientRect.y;
const currentR = e.intersectionRatio;
if (currentY < preY || e.isIntersecting) {
wrapper?.classList.add(className);
} else if (currentY > preY && currentR < preR) {
wrapper?.classList.remove(className);
}
preY = currentY;
preR = currentR;
});
},
{ threshold: 0.8 }
);
observer.observe(wrapper);
},[])
return (
<>
<section>
<div style={styles.whiteSpace}>
<p>(scroll down!)</p>
</div>
</section>
<section>
<div ref={ref} style={styles.container}>
<div style={styles.container.img1}>
<img src="https://via.placeholder.com/350x250" alt="img1" />
</div>
<div style={styles.container.img2}>
<img src="https://via.placeholder.com/350x250" alt="img2" />
</div>
<div style={styles.container.img3}>
<img src="https://via.placeholder.com/350x250" alt="img3" />
</div>
</div>
</section>
<section>
<div style={styles.whiteSpace}>
<p>(scroll up!)</p>
</div>
</section>
</>
);
}
export default OverlappingImages;
The styled-components approach:
I created dummy data for the loop.
Created simple components for section, figure and img. I used figure as a wrapper.
Replaced all necessary style from img to figure and changed styled logic from position: absolute to grid solution. It will allow us to keep the images in the center of the screen if screen size is large and make it flexible for the small screens.
The PictureWrapper (figure) can pass 2 props, position and state.
OverlappingImages.tsx
import { useRef, useEffect, useState, useMemo } from "react";
import styled, { css } from "styled-components";
import data from "./data";
export type TypePosition = "left" | "center" | "right";
interface IProps {
position: TypePosition;
active: boolean;
}
const Image = styled.img`
width: 100%;
height: auto;
`;
// Left image wrapper style with active, inactive state
const left = (active: boolean) => css`
${!active && css`transform: rotate(-4deg) translateX(calc(-1 * clamp(25%, 20vw, 75%)));`}
${active && css`transform: rotate(-6deg) translateX(calc(-1 * clamp(25%, 20vw, 75%))) scale(1.2);`}
transition-delay: 0s;
z-index: 1;
`;
// Center image wrapper style with active, inactive state
const center = (active: boolean) => css`
${active && css`transform: scale(1.2);`}
transition-delay: 0.1s;
z-index: 2;
`;
// Right image wrapper style with active, inactive state
const right = (active: boolean) => css`
${!active && css`transform: rotate(4deg) translateX(clamp(25%, 20vw, 75%));`}
${active && css`transform: rotate(6deg) translateX(clamp(25%, 20vw, 75%)) scale(1.2);`}
transition-delay: 0.2s;
z-index: 1;
`;
// Image wrapper component with 2 props:
// position: left | center | right
// active: true / false
const PictureWrapper = styled.figure<IProps>`
grid-column: 1;
grid-row: 1;
width: clamp(200px, 40vw, 380px);
display: flex;
border: 1px solid #000;
transition: transform 0.4s ease-in-out;
${({ position, active }) => position === "left" && left(active)}
${({ position, active }) => position === "center" && center(active)}
${({ position, active }) => position === "right" && right(active)}
`;
const Container = styled.section`
display: grid;
place-content: center;
position: relative;
margin: 0 50px;
`;
export const OverlappingImages = () => {
const [active, setActive] = useState(false);
const ref = useRef<HTMLElement>(null);
const callback = (entries: IntersectionObserverEntry[]) => {
const [entry] = entries;
if (entry.isIntersecting) {
setActive(entry.isIntersecting);
return;
}
setActive(false);
};
const options = useMemo(() => ({
root: null,
rootMargin: "0px",
threshold: 0.75
}), []);
useEffect(() => {
const container = ref.current;
// Observer with external callback function and options
const observer = new IntersectionObserver(callback, options);
if (container) observer.observe(container);
//cleanup when a component unmounted
return () => {
if (container) observer.unobserve(container);
};
}, [ref, options]);
const images = data.map((img) => {
return (
<PictureWrapper key={img.id} position={img.position} active={active}>
<Image src={img.image} />
</PictureWrapper>
);
});
return <Container ref={ref}>{images}</Container>;
};
data.ts
import { TypePosition } from "./OverlappingImages";
interface IData {
id: string;
image: string;
position: TypePosition;
}
export const data: IData[] = [
{
id: "d4a54w5s1d2sd24",
image: "https://via.placeholder.com/350x250",
position: "left"
},
{
id: "ad4e5qe4545d7ew4",
image: "https://via.placeholder.com/350x250",
position: "center"
},
{
id: "das54w5e1sa2dw5e5",
image: "https://via.placeholder.com/350x250",
position: "right"
}
];
export default data;
App.tsx
import "./styles.css";
import { OverlappingImages } from "./OverlappingImages";
export default function App() {
return (
<div className="App">
<section>
<p>(scroll down!)</p>
</section>
<OverlappingImages />
<section>
<p>(scroll up!)</p>
</section>
</div>
);
}
sections style
section {
display: grid;
place-content: center;
min-height: 100vh;
text-align: center;
}
section:nth-child(odd) {
background: #eee;
}
I'm trying to add and remove active class when I click on different panels that triggers a transition, so if I click on different panels it works, as in it triggers the transition and then it ends it when other panel gets clicked, but if I want to click on a panel that was already opened and closed it won't trigger it again on the first click adn that's not good UX.
I'm writing it in React and I am a beginner so maybe I'm not doing something right.
You can see the code below, I hope I gave all the right information.
componentDidMount() {
ReactDom.findDOMNode(this).addEventListener("transitionend", (e) => {
if (
e.propertyName.includes("flex") &&
e.target.classList.contains("open")
) {
e.target.classList.add("open-active");
}
});
ReactDom.findDOMNode(this).addEventListener("click", (e) => {
const elems = document.querySelector(".open-active");
if (elems !== null) {
elems.classList.remove("open-active", "open", "opac");
}
e.target.className = "open-active";
console.log(e);
});
}
render() {
const { index, top, bottom, image, open, onClick } = this.props;
const style = {
backgroundImage: `url(${image})`,
};
const openValue = open ? "open opac" : "";
return (
<div
className={`panel ${openValue}`}
style={style}
onClick={() => {
onClick(index);
}}
>
</div>
And the CSS
.panel > * {
margin: 0;
width: 100%;
transition: transform 0.5s;
flex: 1 0 auto;
display: flex;
justify-content: center;
align-items: center;
}
.panel > *:first-child {
transform: translateY(-100%);
}
.panel.open-active > *:first-child {
transform: translateY(0);
}
.panel > *:last-child {
transform: translateY(100%);
}
.panel.open-active > *:last-child {
transform: translateY(0);
}
.panel p:nth-child(2) {
font-size: 4em;
}
.panel.open {
font-size: 16px;
flex: 5;
}
Hi you can follow this example:
import React, {useState} from "react";
import './styles/style.css'
export default function ShowHideExample() {
const [cssClass, setCssClass] = useState('hide');
return (
<div className="App">
<h2>Show or Hide div</h2>
<button onClick={() => {
(cssClass === 'hide')? setCssClass('show') : setCssClass('hide');
}}>Click me to show or hide the div
</button>
<div className={cssClass}>
<h1>This is dynamically shown</h1>
</div>
</div>
);
}
Here is the style.css file
.show{
display: block;
background: dodgerblue;
padding:20px;
}
.hide{
display: none;
}
I am building a carousel, very minimalist, using CSS snap points. It is important for me to have CSS only options, but I'm fine with enhancing a bit with javascript (no framework).
I am trying to add previous and next buttons to scroll programmatically to the next or previous element. If javascript is disabled, buttons will be hidden and carousel still functionnal.
My issue is about how to trigger the scroll to the next snap point ?
All items have different size, and most solution I found require pixel value (like scrollBy used in the exemple). A scrollBy 40px works for page 2, but not for others since they are too big (size based on viewport).
function goPrecious() {
document.getElementById('container').scrollBy({
top: -40,
behavior: 'smooth'
});
}
function goNext() {
document.getElementById('container').scrollBy({
top: 40,
behavior: 'smooth'
});
}
#container {
scroll-snap-type: y mandatory;
overflow-y: scroll;
border: 2px solid var(--gs0);
border-radius: 8px;
height: 60vh;
}
#container div {
scroll-snap-align: start;
display: flex;
justify-content: center;
align-items: center;
font-size: 4rem;
}
#container div:nth-child(1) {
background: hotpink;
color: white;
height: 50vh;
}
#container div:nth-child(2) {
background: azure;
height: 40vh;
}
#container div:nth-child(3) {
background: blanchedalmond;
height: 60vh;
}
#container div:nth-child(4) {
background: lightcoral;
color: white;
height: 40vh;
}
<div id="container">
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
</div>
<button onClick="goPrecious()">previous</button>
<button onClick="goNext()">next</button>
Nice question! I took this as a challenge.
So, I increased JavaScript for it to work dynamically. Follow my detailed solution (in the end the complete code):
First, add position: relative to the .container, because it need to be reference for scroll and height checkings inside .container.
Then, let's create 3 global auxiliary variables:
1) One to get items scroll positions (top and bottom) as arrays into an array. Example: [[0, 125], [125, 280], [280, 360]] (3 items in this case).
3) One that stores half of .container height (it will be useful later).
2) Another one to store the item index for scroll position
var carouselPositions;
var halfContainer;
var currentItem;
Now, a function called getCarouselPositions that creates the array with items positions (stored in carouselPositions) and calculates the half of .container (stored in halfContainer):
function getCarouselPositions() {
carouselPositions = [];
document.querySelectorAll('#container div').forEach(function(div) {
carouselPositions.push([div.offsetTop, div.offsetTop + div.offsetHeight]); // add to array the positions information
})
halfContainer = document.querySelector('#container').offsetHeight/2;
}
getCarouselPositions(); // call it once
Let's replace the functions on buttons. Now, when you click on them, the same function will be called, but with "next" or "previous" argument:
<button onClick="goCarousel('previous')">previous</button>
<button onClick="goCarousel('next')">next</button>
Here is about the goCarousel function itself:
First, it creates 2 variables that store top scroll position and bottom scroll position of carousel.
Then, there are 2 conditionals to see if the current carousel position is on most top or most bottom.
If it's on top and clicked "next" button, it will go to the second item position. If it's on bottom and clicked "previous" button, it will go the previous one before the last item.
If both conditionals failed, it means the current item is not the first or the last one. So, it checks to see what is the current position, calculating using the half of the container in a loop with the array of positions to see what item is showing. Then, it combines with "previous" or "next" checking to set the correct next position for currentItem variable.
Finally, it goes to the correct position through scrollTo using currentItem new value.
Below, the complete code:
var carouselPositions;
var halfContainer;
var currentItem;
function getCarouselPositions() {
carouselPositions = [];
document.querySelectorAll('#container div').forEach(function(div) {
carouselPositions.push([div.offsetTop, div.offsetTop + div.offsetHeight]); // add to array the positions information
})
halfContainer = document.querySelector('#container').offsetHeight/2;
}
getCarouselPositions(); // call it once
function goCarousel(direction) {
var currentScrollTop = document.querySelector('#container').scrollTop;
var currentScrollBottom = currentScrollTop + document.querySelector('#container').offsetHeight;
if (currentScrollTop === 0 && direction === 'next') {
currentItem = 1;
} else if (currentScrollBottom === document.querySelector('#container').scrollHeight && direction === 'previous') {
console.log('here')
currentItem = carouselPositions.length - 2;
} else {
var currentMiddlePosition = currentScrollTop + halfContainer;
for (var i = 0; i < carouselPositions.length; i++) {
if (currentMiddlePosition > carouselPositions[i][0] && currentMiddlePosition < carouselPositions[i][1]) {
currentItem = i;
if (direction === 'next') {
currentItem++;
} else if (direction === 'previous') {
currentItem--
}
}
}
}
document.getElementById('container').scrollTo({
top: carouselPositions[currentItem][0],
behavior: 'smooth'
});
}
window.addEventListener('resize', getCarouselPositions);
#container {
scroll-snap-type: y mandatory;
overflow-y: scroll;
border: 2px solid var(--gs0);
border-radius: 8px;
height: 60vh;
position: relative;
}
#container div {
scroll-snap-align: start;
display: flex;
justify-content: center;
align-items: center;
font-size: 4rem;
}
#container div:nth-child(1) {
background: hotpink;
color: white;
height: 50vh;
}
#container div:nth-child(2) {
background: azure;
height: 40vh;
}
#container div:nth-child(3) {
background: blanchedalmond;
height: 60vh;
}
#container div:nth-child(4) {
background: lightcoral;
color: white;
height: 40vh;
}
<div id="container">
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
</div>
<button onClick="goCarousel('previous')">previous</button>
<button onClick="goCarousel('next')">next</button>
Another good detail to add is to call getCarouselPositions function again if the window resizes:
window.addEventListener('resize', getCarouselPositions);
That's it.
That was cool to do. I hope it can help somehow.
I've just done something similar recently. The idea is to use IntersectionObserver to keep track of which item is in view currently and then hook up the previous/next buttons to event handler calling Element.scrollIntoView().
Anyway, Safari does not currently support scroll behavior options. So you might want to polyfill it on demand with polyfill.app service.
let activeIndex = 0;
const container = document.querySelector("#container");
const elements = [...document.querySelectorAll("#container div")];
function handleIntersect(entries){
const entry = entries.find(e => e.isIntersecting);
if (entry) {
const index = elements.findIndex(
e => e === entry.target
);
activeIndex = index;
}
}
const observer = new IntersectionObserver(handleIntersect, {
root: container,
rootMargin: "0px",
threshold: 0.75
});
elements.forEach(el => {
observer.observe(el);
});
function goPrevious() {
if(activeIndex > 0) {
elements[activeIndex - 1].scrollIntoView({
behavior: 'smooth'
})
}
}
function goNext() {
if(activeIndex < elements.length - 1) {
elements[activeIndex + 1].scrollIntoView({
behavior: 'smooth'
})
}
}
#container {
scroll-snap-type: y mandatory;
overflow-y: scroll;
border: 2px solid var(--gs0);
border-radius: 8px;
height: 60vh;
}
#container div {
scroll-snap-align: start;
display: flex;
justify-content: center;
align-items: center;
font-size: 4rem;
}
#container div:nth-child(1) {
background: hotpink;
color: white;
height: 50vh;
}
#container div:nth-child(2) {
background: azure;
height: 40vh;
}
#container div:nth-child(3) {
background: blanchedalmond;
height: 60vh;
}
#container div:nth-child(4) {
background: lightcoral;
color: white;
height: 40vh;
}
<div id="container">
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
</div>
<button onClick="goPrevious()">previous</button>
<button onClick="goNext()">next</button>
An easier approach done with react.
export const AppCarousel = props => {
const containerRef = useRef(null);
const carouselRef = useRef(null);
const [state, setState] = useState({
scroller: null,
itemWidth: 0,
isPrevHidden: true,
isNextHidden: false
})
const next = () => {
state.scroller.scrollBy({left: state.itemWidth * 3, top: 0, behavior: 'smooth'});
// Hide if is the last item
setState({...state, isNextHidden: true, isPrevHidden: false});
}
const prev = () => {
state.scroller.scrollBy({left: -state.itemWidth * 3, top: 0, behavior: 'smooth'});
setState({...state, isNextHidden: false, isPrevHidden: true});
// Hide if is the last item
// Show remaining
}
useEffect(() => {
const items = containerRef.current.childNodes;
const scroller = containerRef.current;
const itemWidth = containerRef.current.firstElementChild?.clientWidth;
setState({...state, scroller, itemWidth});
return () => {
}
},[props.items])
return (<div className="app-carousel" ref={carouselRef}>
<div className="carousel-items shop-products products-swiper" ref={containerRef}>
{props.children}
</div>
<div className="app-carousel--navigation">
<button className="btn prev" onClick={e => prev()} hidden={state.isPrevHidden}><</button>
<button className="btn next" onClick={e => next()} hidden={state.isNextHidden}>></button>
</div>
</div>)
}
I was struggling with the too while working with a react project and came up with this solution. Here's a super basic example of the code using react and styled-components.
import React, { useState, useRef } from 'react';
import styled from 'styled-components';
const App = () => {
const ref = useRef();
const [scrollX, setScrollX] = useState(0);
const scrollSideways = (px) => {
ref.current.scrollTo({
top: 0,
left: scrollX + px,
behavior: 'smooth'
});
setScrollX(scrollX + px);
};
return (
<div>
<List ref={ref}>
<ListItem color="red">Card 1</ListItem>
<ListItem color="blue">Card 2</ListItem>
<ListItem color="green">Card 3</ListItem>
<ListItem color="yellow">Card 4</ListItem>
</List>
<button onClick={() => scrollSideways(-600)}> Left </button>
<button onClick={() => scrollSideways(600)}> Right </button>
</div>
);
};
const List = styled.ul`
display: flex;
overflow-x: auto;
padding-inline-start: 40px;
scroll-snap-type: x mandatory;
list-style: none;
padding: 40px;
width: 700px;
`;
const ListItem = styled.li`
display: flex;
flex-shrink: 0;
scroll-snap-align: start;
background: ${(p) => p.color};
width: 600px;
margin-left: 15px;
height: 200px;
`;
I have an image on a React page. When the state is updated to a new image I want to perform the following transition effect:
The original image should zoom in and fade out
The new image should also zoom in and fade in
The effect should look similar to passing through a wall to a new scene.
How am I able to do this in React?
As #pgsandstrom mentioned, React Transition Group is the way to go. Unfortunately, it's not very developer friendly (pretty steep learning curve).
Here's a working example: https://codesandbox.io/s/6lmv669kz
✔ Original image zooms in while fading out
✔ New image zooms in while fading in
TransitionExample.js
import random from "lodash/random";
import React, { Component } from "react";
import { CSSTransition, TransitionGroup } from "react-transition-group";
import uuid from "uuid/v1";
const arr = [
{
id: uuid(),
url: `https://loremflickr.com/600/100?lock=${random(0, 999)}`
},
{
id: uuid(),
url: `https://loremflickr.com/600/100?lock=${random(0, 999)}`
},
{
id: uuid(),
url: `https://loremflickr.com/600/100?lock=${random(0, 999)}`
}
];
export default class TransitionExample extends Component {
state = {
index: 0,
selected: arr[0]
};
nextImage = () =>
this.setState(prevState => {
const newIndex = prevState.index < arr.length - 1 ? prevState.index + 1 : 0;
return {
index: newIndex,
selected: arr[newIndex]
};
});
render = () => (
<div className="app">
<div style={{ marginBottom: 30, height: 100 }}>
<TransitionGroup>
<CSSTransition
key={this.state.selected.id}
timeout={1000}
classNames="messageout"
>
<div style={{ marginTop: 20 }}>
<img className="centered-image" src={this.state.selected.url} />
</div>
</CSSTransition>
</TransitionGroup>
</div>
<div style={{ textAlign: "center" }}>
<button
className="uk-button uk-button-primary"
onClick={this.nextImage}
>
Next Image
</button>
</div>
</div>
);
}
styles.css
.app {
margin: 0 auto;
overflow: hidden;
width: 700px;
height: 800px;
}
.centered-image {
display: block;
margin: 0 auto;
}
/* starting ENTER animation */
.messageout-enter {
position: absolute;
top: 0;
left: calc(13% + 5px);
right: calc(13% + 5px);
opacity: 0.01;
transform: translateY(0%) scale(0.01);
}
/* ending ENTER animation */
.messageout-enter-active {
opacity: 1;
transform: translateY(0%) scale(1);
transition: all 1000ms ease-in-out;
}
/* starting EXIT animation */
.messageout-exit {
opacity: 1;
transform: scale(1.01);
}
/* ending EXIT animation */
.messageout-exit-active {
opacity: 0;
transform: scale(4);
transition: all 1000ms ease-in-out;
}
It sounds like you are looking for React Transition Group. It is the "official" way of solving these issues. Specifically I think this is what you should use. It can be a bit tricky to get a hang of, but it is really nice and powerful once you understand it.
This worked for me (link):
index.js:
import React from "react";
import { render } from "react-dom";
import "./styles.scss";
const src1 =
"https://www.nba.com/dam/assets/121028030322-james-harden-traded-102712-home-t1.jpg";
const src2 = "https://www.nba.com/rockets/sites/rockets/files/wcwebsite.jpg";
var state = {
toggle: true
};
class App extends React.Component {
render() {
const cn1 = "imgFrame " + (state.toggle ? "toggleOut" : "toggleIn");
const cn2 = "imgFrame " + (state.toggle ? "toggleIn" : "toggleOut");
return (
<div>
<img className={cn1} src={src1} alt={"img1"} />
<img className={cn2} src={src2} alt={"img2"} />
<button
onClick={() => {
state.toggle = !state.toggle;
this.forceUpdate();
}}
>
click me to toggle
</button>
<h1>Hello</h1>
</div>
);
}
}
render(<App />, document.getElementById("app"));
style.scss:
html,
body {
background-color: papayawhip;
font-family: sans-serif;
h1 {
color: tomato;
}
}
#keyframes fadeout {
0% {
opacity: 1;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(0.9);
}
}
#keyframes fadein {
0% {
opacity: 0;
transform: scale(1.1);
}
100% {
opacity: 1;
transform: scale(1);
}
}
.toggleOut {
animation: fadeout 500ms;
opacity: 0;
}
.toggleIn {
animation: fadein 500ms;
opacity: 1;
}
.imgFrame {
position: absolute;
top: 10px;
left: 10px;
width: 200px;
height: 200px;
}
button {
position: absolute;
top: 220px;
}
Wrap with a simple <Animate on={value} /> component that triggers an animation when value changes and is not undefined.
function Animate({ children, on }) {
return (on === undefined)
? <div>{children}</div>
: <div className="fade-in" key={on}>{children}</div>
}
import { useEffect, useState } from 'react'
function TestAnimate() {
const [value, setValue] = useState() // undefined
// update value every second
useEffect(() => {
setInterval(() => setValue(new Date().toLocaleString()), 1_000)
}, [])
return (
<Animate on={value}>
Value: {value}
</Animate>
)
}
#keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.fade-in {
animation: fadeIn 500ms ease-in-out;
}
I am using swiper slider in my project, and I am wondering how can I have some smooth transition from image placeholders that I would replace with real images when they are loaded. So, in my case I have made a slider that looks like this:
<div class="swiper-slide">
<a href="/player/{{ $player->id }}/{{ $player->fullName }}">
<div class="mdc-card player-slider-card">
<img data-src="https://placeimg.com/250/300/any" class="player-images"/>
<div class="card-img-overlay">
<section class="mdc-card__media">
<i class="material-icons player-icon">person_outline</i>
<h3 class="mdc-card__title mdc-card__title--large">{{ $player->first_name }} {{ $player->last_name }}</h3>
</section>
<div class="mdc-card__primary">
<h4 class="mdc-card__subtitle">
{{ $player->nationality }}
#if($player->position != '')
| {{ $player->position }}
#endif
</h4>
</div>
</div>
</div>
</a>
</div>
I have made img tags with data-src attribute that I am later removing and adding the src attribute, when the images are loaded and I am at the same time removing the icon person_outline that I have as an image placeholder. This is the scss file:
app.scss
$white: #fff;
#mixin pulse {
-webkit-animation: pulse .65s infinite alternate;
animation: pulse .65s infinite alternate;
}
#keyframes pulse {
0%{opacity:.5}100%{opacity:1}
}
.slider-section {
position: relative;
.arrow-left, .arrow-right {
position: absolute;
top: 40%;
z-index: 10;
}
.arrow-left {
left: 5px;
}
.arrow-right {
right: 5px;
}
.swiper-slide {
a {
text-decoration: none;
}
}
.player-slider-card {
background: #E1E9EE;
color: $white;
width: 250px;
#include pulse;
h3, h4 {
#include pulse;
color: $white;
}
img {
opacity: 1;
transition: opacity 0.3s;
}
img[data-src] {
opacity: 0;
}
.mdc-card__media {
height: 300px;
}
.player-icon {
margin: auto;
font-size: 78px;
margin-bottom: 50px;
margin-top: 50px;
}
}
}
And this is the js file:
app.js
import Swiper from 'swiper';
const removePlaceholder = (item) => {
setTimeout(() => {
let slides = document.querySelectorAll(`.${item}-slider-card`);
let images = document.querySelectorAll(`.${item}-images`);
let placeholders = document.querySelectorAll(`.${item}-icon`);
slides.forEach(el => {
el.classList.remove(`${item}-slider-card`);
});
images.forEach(el => {
el.setAttribute('src', el.getAttribute('data-src'));
el.removeAttribute('data-src');
})
placeholders.forEach(el => {
el.remove();
})
}, 3000);
}
const sliderOptions = (nextEl, prevEl) => {
return {
direction: 'horizontal',
slidesPerView: 5,
simulateTouch: false,
spaceBetween: 1,
navigation: {
nextEl,
prevEl,
},
on: {
imagesReady:() => {
removePlaceholder('player');
},
},
breakpoints: {
1181: {
slidesPerView: 5
},
1180: {
slidesPerView: 4
},
1020: {
slidesPerView: 3
},
700: {
slidesPerView: 2
}
}
}
}
let videoOptions = sliderOptions('.js-arrow-videos-right', '.js-arrow-videos-left');
let playerOptions = sliderOptions('.js-arrow-players-right', '.js-arrow-players-left');
let swiperVideos = new Swiper('.js-videos-slider', videoOptions);
let swiperPlayers = new Swiper('.js-players-slider', playerOptions);
I have made a jsfiddle here. What I am wondering is how can I make a smooth transition when the images are being loaded. I thought I would maybe solve this with just a simple transition:
img {
opacity: 1;
transition: opacity 0.3s;
}
img[data-src] {
opacity: 0;
}
But, that didn't help, how can I achieve the similar effect to images on for example on themedium site or facebook.