Currently, I have a text animation that is functioning flawlessly. However, my goal now is to implement an Intersection Observer so that the animation only triggers when I scroll down to the designated box.
To achieve this, I utilized the useRef React hook as a reference point for the element I want to observe. I then applied this reference to my Box using ref={containerRef}
. Subsequently, I defined a callback function that receives an array of IntersectionObserverEntries. Within this function, I extract the first entry and check if it intersects with the viewport. If it does, I call setIsVisible with the value of entry.isIntersecting (true/false). Following this, I incorporated the useEffect React hook and established an observer constructor by utilizing the previously created callback function and options. This logic was embedded in a custom hook dubbed useElementOnscreen
.
Nevertheless, Typescript is flagging an error at containerRef?.current
:
Argument of type 'IntersectionObserver' is not assignable to parameter of type 'Element'.
Type 'IntersectionObserver' is missing the following properties from type 'Element': attributes, classList, className, clientHeight, and 160 more.
Regrettably, I am unsure how to rectify this issue. It appears that this also leads to errors with ref={containerRef}
.
The anticipated type originates from property 'ref' as declared here on type 'IntrinsicAttributes & { component: ElementType<any>; } & SystemProps<Theme> & { children?: ReactNode; component?: ElementType<...> | undefined; ref?: Ref<...> | undefined; sx?: SxProps<...> | undefined; } & CommonProps & Omit<...>'
About the animation: The TopAnimateBlock and BottomAnimateBlock include properties such as numOfLine indicating the number of lines within the block. The delayTopLine property in BottomAnimateBlock should align with numOfLine in TopAnimateBlock since we need to wait for the top lines to animate firstly.
TextAnimation.tsx
import { Box, Stack, Typography } from '@mui/material';
import React, { useRef, useEffect, useState } from 'react';
import styled, { keyframes } from 'styled-components';
const showTopText = keyframes`
0% { transform: translate3d(0, 100% , 0); }
40%, 60% { transform: translate3d(0, 50%, 0); }
100% { transform: translate3d(0, 0, 0); }
`;
const showBottomText = keyframes`
0% { transform: translate3d(0, -100%, 0); }
100% { transform: translate3d(0, 0, 0); }
`;
const Section = styled.section`
width: calc(100% + 10vmin);
display: flex;
flex-flow: column;
padding: 2vmin 0;
overflow: hidden;
&:last-child {
border-top: 1vmin solid white;
}
`;
const Block = styled.div<{ numOfLine: number }>`
position: relative;
`;
const TopAnimateBlock = styled(Block)`
animation: ${showTopText} calc(0.5s * ${props => props.numOfLine}) forwards;
animation-delay: 0.5s;
transform: translateY(calc(100% * ${props => props.numOfLine}));
`;
const BottomAnimateBlock = styled(Block)<{ delayTopLine: number }>`
animation: ${showBottomText} calc(0.5s * ${props => props.numOfLine}) forwards;
animation-delay: calc(0.7s * ${props => props.delayTopLine});
transform: translateY(calc(-100% * ${props => props.numOfLine}));
`;
const TextStyle = styled.p<{ color: string }>`
font-family: Roboto, Arial, sans-serif;
font-size: 12vmin;
color: ${props => props.color};
`;
const useElementOnScreen = (options) => {
const containerRef = useRef<IntersectionObserver | null>(null);
const [isVisible, setIsVisible] = useState(false);
const callbackFunction = (entries) => {
const [entry] = entries;
setIsVisible(entry.isIntersecting);
};
useEffect(() => {
const observer = new IntersectionObserver(callbackFunction, options);
if (containerRef.current) observer.observe(containerRef?.current);
return () => {
if (containerRef.current) observer.unobserve(containerRef?.current);
};
}, [containerRef, options]);
return [containerRef, isVisible];
};
export function Details() {
const [containerRef, isVisible] = useElementOnScreen({
root: null,
rootMargin: '0px',
threshold: 1.0,
});
return (
<>
<Typography>Scroll Down</Typography>
<Box ref={containerRef}>
<Section>
<TopAnimateBlock numOfLine={2}>
<TextStyle color="grey">mimicking</TextStyle>
<TextStyle color="white">apple's design</TextStyle>
</TopAnimateBlock>
</Section>
<Section>
<BottomAnimateBlock numOfLine={1} delayTopLine={2}>
<TextStyle color="white">for the win!</TextStyle>
</BottomAnimateBlock>
</Section>
</Box>
</>
);
};