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 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;
}
I want an animation that runs on every click.
However, it runs only the first time and not after the second click.
Is there a way?
const inputEl = useRef();
const onButtonClick = () => {
inputEl.current.style.animation = "0.7s ease-in-out 0s 1 testKeyword";
};
return(
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
);
#keyframes testKeyword {
0% {
opacity: 0;
transform: scale(1);
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
I have 2 different solutions depending on your needs:
Solution 1: using useRef
js file
const inputEl = useRef();
const handleButtonClick = () => {
inputEl.current.style.animation = "0.7s ease-in-out 0s 1 testKeyword";
}
const handleAnimationEnd = () => {
inputEl.current.style.animation = "none";
}
<input ref={inputEl} onAnimationEnd={handleAnimationEnd} type="text"/>
<button onClick={handleButtonClick} >Focus the input</button>
css file:
#keyframes testKeyword {
0% {
opacity: 0;
transform: scale(1);
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
Solution 2: using useState
js file:
const [clicked, setClicked] = useState(false);
<input type="text" class={clicked === true ? "animate" : null} onAnimationEnd={() => setClicked(false)}/>
<button onClick={() => setClicked(true)}>Focus the input</button>
css file:
.animate {
animation: 0.7s ease-in-out 0s 1 testKeyword;
}
#keyframes testKeyword {
0% {
opacity: 0;
transform: scale(1);
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
This should help! I am using a class that contains the animation and then I am adding it on button click, then after 700ms I am removing this class using setTimeout
class Demo extends React.Component {
handleClick() {
const inpEl = document.querySelector(".Demo-input");
inpEl.classList.add("Demo-animate");
setTimeout(() => {
inpEl.classList.remove("Demo-animate");
}, 700);
}
render() {
return (
<div className="Demo">
<input className="Demo-input" type="text" />
<button onClick={this.handleClick}> Click Me </button>
</div>
);
}
}
ReactDOM.render(<Demo />, document.getElementById("root"));
.Demo-animate {
animation: 0.7s ease-in-out 0s 1 testKeyword;
}
#keyframes testKeyword {
0% {
opacity: 0;
transform: scale(1);
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
You are just setting the animation style property of the element once and after the animation has completed 100%, it won't start again. What you can do is add a class to the element which changes the style of the element and remove it after some time interval.
const onButtonClick = () => {
const element = document.querySelector("input");
element.classList.add("fade");
setTimeout(() => {
element.classList.remove("fade");
}, 200);
};
return (
<>
<input type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
The CSS for the same would look something like this
.fade {
animation: 0.7s ease-in-out 0s 1 testKeyword;opacity: 0;
}
#keyframes testKeyword {
0% {
opacity: 0;
transform: scale(1);
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
Note:
This implementation uses setTimeout which is an asynchronous API. It may happen at some point in time when the stack is not empty and the callback to remove the fade class is waiting in the task queue, then the code will freeze until the task is cleared. This is a rare case but it is important to know about it.
You can refer to this video on eventloop (What the heck is the event loop anyway? | Philip Roberts | JSConf EU) on youtube to understand this better. Here is the link https://youtu.be/8aGhZQkoFbQ?t=769
Hi I am making a site with google maps in Reactjs using the npm module google-map-react.
I was first using the module npm google-maps-react and have now decided to use google-map-react.
With the first npm package this was working perfectly but now my side navigation appears above the map instead of side by side.
The side nav is using react-transtion-group if that makes any difference.
It looks like this
side nav css
#mySidenav a {
position: absolute;
left: -10px;
transition: 0.3s;
padding: 9px;
text-decoration: none;
font-size: 20px;
color: black;
border-radius: 0 5px 5px 0;
border: 1px solid #000000;
}
#mySidenav a:hover {
left: 0px;
cursor: pointer;
background-color: gray;
}
#contact {
background-color: rgb(228, 225, 225);
}
/* slide enter */
.slide-enter {
opacity: 0;
transform: scale(0.97) translateX(5px);
z-index: 1;
}
.slide-enter.slide-enter-active {
opacity: 1;
transform: scale(1) translateX(0);
transition: opacity 3000ms linear 1000ms, transform 3000ms ease-in-out 1000ms;
}
/* slide exit */
.slide-exit {
opacity: 1;
transform: scale(1) translateX(0);
}
.slide-exit.slide-exit-active {
opacity: 0;
transform: scale(0.97) translateX(5px);
transition: opacity 1500ms linear, transform 1500ms ease-out;
}
.slide-exit-done {
opacity: 0;
}
App.Js
class App extends Component {
render() {
return (
<Switch>
<Route
exact
path="/reactapp"
render={() => (
<div id="wrapper">
<TopNav />
<SideNav />
<MapContainer />
</div>
)}
/>
</Switch>
);
}
}
Map
import React, { Component } from "react";
import GoogleMapReact from "google-map-react";
import { connect } from "react-redux";
import { fetchPosts, fetchItins } from "../../actions/postActions";
import PropTypes from "prop-types";
export class MapContainer extends Component {
static defaultProps = {
center: {
lat: 59.95,
lng: 30.33,
},
zoom: 11,
};
state = {
showingInfoWindow: false,
activeMarker: {},
selectedPlace: {},
};
onMarkerClick = (props, marker, e) =>
this.setState({
selectedPlace: props,
activeMarker: marker,
showingInfoWindow: true,
});
onMapClicked = (props) => {
if (this.state.showingInfoWindow) {
this.setState({
showingInfoWindow: false,
activeMarker: null,
});
}
};
componentDidMount() {}
toggleWindow() {
if (this.state.showingInfoWindow) {
} else if (!this.state.showingInfoWindow) {
}
}
render() {
console.log(this.props.posts);
return (
<div style={{ height: "100vh", width: "100%" }}>
<GoogleMapReact
bootstrapURLKeys={{ key: "AIzaSyAfpKoor5CLGg-HbDwdKHq9mGij2JA-YzE" }}
defaultCenter={this.props.center}
defaultZoom={this.props.zoom}
></GoogleMapReact>
</div>
);
}
}
MapContainer.propTypes = {
fetchPosts: PropTypes.func.isRequired,
posts: PropTypes.object.isRequired,
// newPost: PropTypes.object
};
const mapStateToProps = (state) => ({
posts: state.posts.items,
// newPost: state.posts.item
});
export default connect(mapStateToProps, { fetchPosts })(MapContainer);
Ideally I would just have this behind as like a background image and then everything else could just sit on top.
I resolved this by setting each layer to have a zindex (css). You can then load things on top of others depending on their z index.
e.g map z index = 0
sidenav z index = 1;
sidenav will appear on top
I have a straight forward react-comp. which I want to test the styling depending on the react state. The comp looks like the following:
React-comp.
const Backdrop = ({ showBackdrop }) => {
const backdropRef = useRef();
function getBackdropHeight() {
if (typeof window !== 'undefined') {
return `calc(${document.body.clientHeight}px -
${backdropRef.current?.offsetTop || 0}px)`;
}
return 0;
}
return (
<div
data-testid="backdrop"
className={classNames(styles.backdrop, showBackdrop ? styles.show : styles.hide)}
ref={backdropRef}
style={{ height: getBackdropHeight() }}
/>
);
};
Styling
.backdrop {
width: 100%;
position: absolute;
left: 0;
right: 0;
top: 156px;
background-color: rgba(0, 0, 0, 0.7);
z-index: 3;
...
}
.show {
opacity: 0;
visibility: hidden;
transition: visibility 0.25s, opacity 0.25s ease-out;
}
.hide {
opacity: 1;
visibility: hidden;
transition: opacity 0.25s ease-in;
}
And the error that I always get from the test is, that the element is visible:
Received element is visible:
<div class="backdrop hide" data-testid="backdrop" style="height: calc(0px -
0px);" />
21 | const { getByTestId } = renderWithIntl(<Backdrop showBackdrop={false} />);
22 |
> 23 | expect(getByTestId('backdrop')).not.toBeVisible();
| ^
24 | });
25 | });
26 |
Test
it("should not render visible backdrop on falsy state", () => {
const { getByTestId } = render(<Backdrop showBackdrop={false} />);
expect(getByTestId('backdrop')).not.toBeVisible();
});
Any way on how to get the element as not visible without using react inline styling!?
You can use toBeVisible() function from RTL.
Here you have docs:
https://github.com/testing-library/jest-dom#tobevisible
Example:
// element should not be visible on screen
expect(screen.queryByText('1')).not.toBeVisible();