Animated underline failing in react

Viewed 232

I modified this underline animation when scrolling down on Codepen in Javascript and it works. You can see it here in the Codepen It works with Intersection Observer and for the underline it uses a generated svg.

Explanation how it works:

This effect is made by positioning an SVG relative the to the text one want to emphasize and animating it in and out.

The words are wrapped to be underlined in a span element in order to allow to target the words directly.

In order to access the path element using CSS the SVG code is directly in the HTML. Add it inside of the span element you wrapped the text you want to underline in.

The SVG element has a class name so it can target it using CSS -> class="squiggle".

The squiggle position as well as the width and height are set inline directly on the SVG, to get the line to appear exactly where I want it.

Now the underline shows up in the right place. The animation happens with the stroke of an SVG path by using a combination of the stroke-dasharray and stroke-dashoffset properties.

For the beginning styles, the stroke-dasharray and stroke-dashoffset should be large enough that the stroke is not visible.

.squiggle path {
  stroke-dasharray: 1600;
  stroke-dashoffset: 1600;
}

When the container element is in view, we can set the animation name, duration, iteration-count, and fill-mode:

.text-effect.in-view .squiggle path {
  animation-name: underline;
  animation-duration: 1s;
  animation-iteration-count: 1;
  animation-fill-mode: forwards;
}

To display the animation in reverse when the element is scrolled out of view the @keyframes underline-out does the opposite animation.

My attempt to rebuild it in React Typescript:

Now first I try only to achieve the animation React Typescript as styled component and then I try to add the Intersection Observer from the Codepen in a next step. But still I have problems with the animation. I declared my styles to use inline but I still having some difficulties to achieve the same result as it does not work.

My App.tsx

import Typography from '@mui/material/Typography';
import { Box, keyframes, styled } from '@mui/material';
import React from 'react';

const Underline = keyframes`
{
   "from": {
     strokeDashoffset: "1600",
   },
   "to": {
     strokeDashoffset: "200",
   }
 }
`;

const styles = {
   textDecoration: {
       textDecoration: 'none',
       cursor: 'pointer',
       color: 'inherit',
   },

   textEffect: {
       strokeDasharray: '1600',
       strokeDashoffset: '200',
       animationName: 'none',
   },

   squiggle: {
       position: 'absolute',
       top: '90%',
       left: '-9px',
   },

   'squiggle path': {
       stroke: '#FB9F18',
       strokeWidth: '14px',
       strokeLinecap: 'round',
       strokeDasharray: '1600',
       strokeDashoffset: '1600',

       animationName: 'underline-out',
       animationDuration: '1s',
       animationIterationCount: '1',
   },

   'textEffect.in-view .squiggle path': {
       animationName: 'underline',
       animationDuration: '1s',
       animationIterationCount: '1',
       animationFillMode: 'forwards',
   },

   '@keyframes underline': {
       from: {
           strokeDashoffset: '1600',
       },
       to: {
           strokeDashoffset: '200',
       },
   },

   '@keyframes underline-out': {
       from: {
           strokeDashoffset: '200',
       },
       to: {
           strokeDashoffset: '1600',
       },
   },
};


function UnderlineEffect() {

   return (
       <Box style={styles.textEffect}>
               <Typography>
                   Double your efficiency,{' '}
                   <span class="underline">
                       <a style={styles.textDecoration} href="www.somewebsite.com">
                           guaranteed.
                       </a>
                       <svg
                           width="215px"
                           height="25px"
                           style={styles.squiggle}
                           viewBox="0 0 466 29"
                           version="1.1"
                           xmlns="http://www.w3.org/2000/svg"
                           xmlns:xlink="http://www.w3.org/1999/xlink"
                           xml:space="preserve"
                           xmlns:serif="http://www.serif.com/"
                           style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
                           <g transform="matrix(1,0,0,1,-4055,-7503)">
                               <g transform="matrix(1,0,0,1,2942.74,0)">
                                   <g
                                       id="squiggle-underline"
                                       transform="matrix(0.59639,0.0106812,-0.0121867,0.680453,651.002,2140.64)">
                                       <path
                                           d="M976.992,7882.9C1019.57,7882.9 1065.7,7874.34 1108.59,7871.65C1246.32,7863.04 1383.5,7857.74 1521.52,7857.74C1541.4,7857.74 1709,7857.97 1714.47,7868.91C1715.77,7871.51 1710.77,7870.69 1710.29,7870.73C1695.62,7871.88 1681.88,7871.57 1667.06,7871.96C1646.09,7872.51 1625.12,7872.92 1604.15,7873.36C1564.57,7874.19 1525.13,7876.05 1485.69,7879.23C1411.5,7885.22 1337.41,7889.35 1263.06,7892.45C1196.87,7895.22 1130.52,7894 1064.4,7897.12C1021.44,7899.14 978.37,7907.57 935.437,7907.57"
                                           style="fill:none;"
                                       />
                                   </g>
                               </g>
                           </g>
                       </svg>
                   </span>
               </Typography>
       </Box>
   );
}

export { UnderlineEffect };

2 Answers

In react jsx or tsx you should be using className not class.

Same goes for other attributes like for which should be written as *htmlFor *

You are using React with Material UI and Styled-components, but for this solution, @Emotion is the better than Styled-components, why?

Warning: Using styled-components as an engine at this moment is not working when used in a SSR projects. The reason is that the babel-plugin-styled-components is not picking up correctly the usages of the styled() utility inside the @mui packages. For more details, take a look at this issue. We strongly recommend using emotion for SSR projects. Material UI

  1. Creating dummy scroll data
  2. Creating custom components from @mui/material components using @emotion
  3. Creating the Underline.tsx component separately. I redrawn the svg path to simplify svg itself. Right here we define our svg styles and interface (for typescript). I assigned 4 props to customize it:
  • a) color - stroke (color)
  • b) size: srtoke-width
  • c) time: transition-duration
  • d) trigger: activates stroke-dashoffset value to toggle
  1. Setting up our IterceptionObserver

App.tsx

import { useRef, useEffect, useState, useMemo } from "react";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import Link from "@mui/material/Link";
import styled from "@emotion/styled";

import { Underline } from "./Underline";

const LinkStyle = styled(Link)`
  text-decoration: none;
  color: inherit;
  position: relative;
`;

const TitleStyle = styled(Typography)`
  font-size: 42px;
  text-align: center;
`;

export default function App() {
  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]);

  // Dummy scroll data
  const scrollDown = new Array(9)
    .fill("SCROLL DONW")
    .map((el, idx) => <TitleStyle key={idx}>{el}</TitleStyle>);

  const scrollUp = new Array(9)
    .fill("SCROLL UP")
    .map((el, idx) => <TitleStyle key={idx}>{el}</TitleStyle>);
  // ----

  return (
    <div className="App">
      {scrollDown}
      <Box>
        <TitleStyle ref={ref}>
          Double your efficiency,{" "}
          <LinkStyle href="#" color="inherit">
            guaranteed.{" "}
            <Underline color="#fb9f18" time={0.5} trigger={active} size={4} />
          </LinkStyle>
        </TitleStyle>
      </Box>
      {scrollUp}
    </div>
  );
}

Underline.tsx

import styled from "@emotion/styled";

export interface Props {
  color: string;
  time: number;
  trigger: boolean;
  size: number;
}

const SVGWrapper = styled.span`
  width: 110%;
  height: 20px;
  position: absolute;
  bottom: 5px;
  left: 0;
`;

const Path = styled.path<Props>`
  stroke: ${(props) => props.color};
  stroke-width: ${(props) => props.size};
  stroke-linecap: round;
  stroke-dashoffset: ${(props) => (props.trigger ? 60 : 390)};
  stroke-dasharray: 390;
  transition: all ${(props) => props.time}s;
`;

export const Underline = ({ color, size, trigger, time }: Props) => {
  return (
    <SVGWrapper>
      <svg viewBox="0 0 174 15" fill="none" xmlns="http://www.w3.org/2000/svg">
        <Path
          color={color}
          time={time}
          trigger={trigger}
          size={size}
          d="M2.184 7.404c17.923 0 24.462-2.474 40.922-2.943 8.028-.229 59.438-1.148 64.175-1.368 14.133-.657 36.013-1.744 62.608 4.311 8.092 1.843-25.485-.13-36.391 1.046-15.655 1.69-32.732 2.346-49.015 2.85-13.589.421-53.558.512-64.376.786"
        />
      </svg>
    </SVGWrapper>
  );
};

Edit dazziling-code

Related