18.2 C
New York
Tuesday, June 17, 2025

Constructing an Infinite Marquee Alongside an SVG Path with React & Movement


You’re possible acquainted with the infinite scrolling marquee impact – that steady horizontal scroll of content material that many web sites use to showcase their work, testimonials, or accomplice manufacturers. It’s a beautiful and easy impact, however on this tutorial, we are going to spice it up by making a marquee that strikes alongside a customized, funky SVG path. We’ll be utilizing React, Typescript and Movement (previously Framer Movement) for the animation, so let’s get began!

You can even discover this element in Fancy Parts, the rising library of ready-to-use React parts & microinteractions.

Preparation

First, we have to create an SVG path that can function the rule for our marquee. You may create one utilizing any vector-drawing software, code it programmatically, and even draw it by hand utilizing SVG path instructions. For this tutorial, I used Figma to create a easy curved path that crosses over itself.

The magic that can make our components transfer alongside the trail is the `offset-path` and `offset-distance` properties.

The offset-path CSS property specifies a path for a component to observe and determines the component’s positioning inside the path’s father or mother container or the SVG coordinate system. The trail is a line, a curve, or a geometrical form alongside which the component will get positioned or strikes.

The offset-distance CSS property specifies a place alongside an `offset-path` for a component to be positioned. (Documentation on MDN)

Let’s begin by making a easy React element that can render an SVG path and a div component. The div will use the offset-path CSS property to observe the trail. This path comes from our SVG’s d attribute.

Additionally, let’s add a CSS animation that can animate the offset-distance property of the div from 0% to 100% in an infinite loop:

See the Pen
cd-marquee-01-preparation by daniel petho (@nonzeroexitcode)
on CodePen.

💡 Discover how we set the SVG’s father or mother container in our CSS file to match the size specified within the SVG’s viewBox attribute. This creates a fixed-size container that can preserve the precise proportions of our path. Whereas this works for now, we’ll discover strategies for making the marquee responsive later within the tutorial.

And that’s the core of our impact! Whereas this primary model works, we are able to make it fancier with interactive options like velocity controls and scroll-based animations. To implement these, and have higher total management over the animation, let’s change from CSS animations to Movement.

Animating with Movement

First, let’s set up the Movement bundle:

npm i movement

We’ll use three key hooks from Movement to make this work:

1. useMotionValue: Creates a worth that we are able to animate easily. We’ll use this to trace our place alongside the trail (0-100%). Consider it as a reactive worth that mechanically triggers visible modifications with out inflicting re-renders (docs).

const baseOffset = useMotionValue(0);

2. useAnimationFrame: Offers us frame-by-frame management over the animation. On every body, we’ll replace our place primarily based on a desired velocity, which we are going to name baseVelocity. That is excellent for steady animations like our marquee (docs).

const baseVelocity = 20;

useAnimationFrame((_, delta) => {
  const moveBy = (baseVelocity * delta) / 1000;
  baseOffset.set(baseOffset.get() + moveBy);
});

3. useTransform: Creates a brand new movement worth that transforms the output of one other movement worth. In our case, it ensures our place (baseOffset) stays inside 0-100% by wrapping round. We will even use a useful wrap perform to do the wrapping (docs).

/**
 * Wraps a quantity between a min and max worth
 * @param min The minimal worth
 * @param max The utmost worth 
 * @param worth The worth to wrap
 * @returns The wrapped worth between min and max
 */
const wrap = (min: quantity, max: quantity, worth: quantity): quantity => {
  const vary = max - min;
  return ((((worth - min) % vary) + vary) % vary) + min;
};

//...

const offset = useTransform(baseOffset, (v) => `${wrap(0, 100, v)}%`);

Placing all of it collectively, that is how our `MarqueeAlongPath.tsx` element appears to be like:

import {
  movement,
  useMotionValue,
  useTransform,
  useAnimationFrame,
} from "movement/react";

import "./index.css";

sort MarqueeAlongPathProps = {
  path: string;
  baseVelocity: quantity;
}

/**
 * Wraps a quantity between a min and max worth
 * @param min The minimal worth
 * @param max The utmost worth 
 * @param worth The worth to wrap
 * @returns The wrapped worth between min and max
 */
const wrap = (min: quantity, max: quantity, worth: quantity): quantity => {
  const vary = max - min;
  return ((((worth - min) % vary) + vary) % vary) + min;
};

const MarqueeAlongPath = ({
  path,
  baseVelocity,
}: MarqueeAlongPathProps) => {
  const baseOffset = useMotionValue(0);
  const offset = useTransform(baseOffset, (v) => `${wrap(0, 100, v)}%`);

  useAnimationFrame((_, delta) => {
    const moveBy = baseVelocity * delta / 1000;
    baseOffset.set(baseOffset.get() + moveBy);
  });

  return (
    <div className="container">
      <svg
        width="100%"
        peak="100%"
        viewBox="0 0 588 187"
        fill="none"
        xmlns="http://www.w3.org/2000/svg"
      >
        <path d={path} stroke="black" fill="none" />
      </svg>
      <movement.div
        className="marquee-item"
        type={{
          offsetPath: `path('${path}')`,
          offsetDistance: offset,
          offsetRotate: "auto",
        }}
      />
    </div>
  );
};

const path =
  "M0 186.219C138.5 186.219 305.5 194.719 305.5 49.7188C305.5 -113.652 -75 186.219 484.5 186.219H587.5";

const App = () => {
  return <MarqueeAlongPath path={path} baseVelocity={20} />;
};

export default App;

Plus the modified index.css file:

physique {
  margin: 0;
  width: 100vw;
  peak: 100vh;
  show: flex;
  align-items: middle;
  justify-content: middle;
  overflow: hidden;
  background-color: #e5e5e5;
}

.container{
  place: relative;
  width: 588px;
  peak: 187px;
}

.marquee-item {
  place: absolute;
  high: 0;
  left: 0;
  width: 32px;
  peak: 32px;
  border-radius: 15%;
  background-color: black;
}

Now we have now the identical outcome as earlier than, solely utilizing Movement this time.

Including Components

Till now, we had been animating one component alongside the trail. Let’s modify our element to assist a number of ones. Whereas we might use an gadgets prop to configure which components to point out, I favor utilizing React kids as a result of it permits us to easily nest any components we wish to animate inside our element.

We’ll additionally introduce a repeat prop that controls what number of instances the youngsters ought to be repeated alongside the trail. That is notably helpful when you’ve a low variety of components however need them to look a number of instances to “fill” the trail.

import React, { useMemo } from "react";

//...

sort MarqueeAlongPathProps = {
  kids: React.ReactNode;
  path: string;
  baseVelocity: quantity;
  repeat?: quantity;
};

//...

  const gadgets = useMemo(() => {
    const childrenArray = React.Youngsters.toArray(kids);

    return childrenArray.flatMap((baby, childIndex) =>
      Array.from({ size: repeat }, (_, repeatIndex) => {
        const itemIndex = repeatIndex * childrenArray.size + childIndex;
        const key = `${childIndex}-${repeatIndex}`;
        return {
          baby,
          childIndex,
          repeatIndex,
          itemIndex,
          key,
        };
      })
    );
  }, [children, repeat]);

//...

Let’s additionally create an inside Merchandise element that can render the precise component. We additionally have to offset the place of every component primarily based on the index of the merchandise. For simplicity, let’s simply distribute the gadgets evenly alongside the trail:

sort MarqueeItemProps = {
  baseOffset: any;
  path: string;
  itemIndex: quantity;
  totalItems: quantity;
  repeatIndex: quantity;
  kids: React.ReactNode;
};

const MarqueeItem = ({ baseOffset, path, itemIndex, totalItems, repeatIndex, kids }: MarqueeItemProps) => {
  const itemOffset = useTransform(baseOffset, (v: quantity) => {
    // Distribute gadgets evenly alongside the trail
    const place = (itemIndex * 100) / totalItems;
    const wrappedValue = wrap(0, 100, v + place);
    return `${wrappedValue}%`;
  });

  return (
    <movement.div
      className="marquee-item"
      type={{
        offsetPath: `path('${path}')`,
        offsetDistance: itemOffset,
        offsetRotate: "auto",
      }}
      aria-hidden={repeatIndex > 0}
    >
      {kids}
    </movement.div>
  );
};

Then, let’s simply use the gadgets array to render the weather inside the primary MarqueeAlongPath element:

{gadgets.map(({ baby, repeatIndex, itemIndex, key }) => {
  return (
    <MarqueeItem
      key={key}
      baseOffset={baseOffset}
      path={path}
      itemIndex={itemIndex}
      totalItems={gadgets.size}
      repeatIndex={repeatIndex}
    >
      {baby}
    </MarqueeItem>
  );
}}

You should utilize any component as kids: photos, movies, customized parts, and so forth. For this train, let’s simply create a easy Card element, and use it as the youngsters of our MarqueeAlongPath element:

const Card = ({ index }: { index: quantity }) => {
  return (
    <div className="card">
      <div className="card-header">
        <div className="card-number">Component {(index + 1).toString().padStart(2, '0')}</div>
        <div>
          <div className="card-brand">Codrops</div>
          <div className="card-year">2025</div>
        </div>
      </div>
      <div className="card-footer">Marquee</div>
    </div>
  );
};

const App = () => {
  return (
    <MarqueeAlongPath path={path} baseVelocity={5} repeat={4}>
      {[...Array(5)].map((_, i) => (
        <Card key={i} index={i} />
      ))}
    </MarqueeAlongPath>
  );
};

Lastly, let’s simply conceal the SVG path by setting the trail stroke="none" inside the primary element for now. And that’s it! We now have a working marquee alongside a path with our personal components!

Z-Index Administration

Should you look intently, chances are you’ll discover one thing off on the self-crossing a part of the trail – components seem to randomly overlap one another quite than sustaining a constant front-to-back order. I recolored the playing cards for illustration functions:

Incorrect z ordering of cards

This occurs as a result of our marquee gadgets are rendered in DOM order, so components that seem later within the markup will at all times be rendered on high of earlier components. When a component that was added later circles again round and crosses paths with earlier components, it incorrectly renders on high, breaking the phantasm of correct depth ordering alongside the trail.

To repair this visible situation, we have to dynamically alter the z-index of every marquee merchandise primarily based on its place alongside the trail. We are able to do that through the use of the itemOffset movement worth we created earlier, which provides us every merchandise’s progress (0-100%) alongside the trail.

We’ll add a zIndexBase prop to our element to set the beginning z-index worth. Then, we’ll create a remodel that converts the share progress into an applicable z-index worth – gadgets additional alongside the trail could have larger z-index values. For instance, if an merchandise is 75% alongside the trail and we have now a zIndexBase of 0, it could get a z-index of 75.

const MarqueeItem = ({
  path,
  baseOffset,
  itemIndex,
  totalItems,
  repeatIndex,
  zIndexBase,
  kids,
}: MarqueeItemProps) => {
  const itemOffset = useTransform(baseOffset, (v: quantity) => {
    // Distribute gadgets evenly alongside the trail
    const place = (itemIndex * 100) / totalItems;
    const wrappedValue = wrap(0, 100, v + place);
    return `${wrappedValue}%`;
  });

  const zIndex = useTransform(itemOffset, (v) => {
    const progress = parseFloat(v.exchange("%", ""));
    return Math.flooring(zIndexBase + progress);
  });

  return (
    <movement.div
      className="marquee-item"
      type={{
        offsetPath: `path('${path}')`,
        offsetDistance: itemOffset,
        offsetRotate: "auto",
        zIndex: zIndex,
      }}
      aria-hidden={repeatIndex > 0}
    >
      {kids}
    </movement.div>
  );
};

And now it ought to be fastened:

Altering Velocity on Hover

We are able to make this marquee extra interactive by introducing a couple of options, which can permit us to vary the velocity interactively. Let’s begin with a easy one, the place we are going to decelerate the marquee on hover.

First, let’s monitor if any of the weather are being hovered over. I favor to make use of a ref as a substitute of state to keep away from pointless re-renders.

// In the primary element
const isHovered = useRef(false);

//...

// Within the marquee merchandise
<movement.div
  key={key}
  className="marquee-item"
  type={{
    offsetPath: `path('${path}')`,
    offsetDistance: itemOffset,
    offsetRotate: "auto", 
    zIndex: zIndex,
  }}
  aria-hidden={repeatIndex > 0}
  onMouseEnter={() => (isHovered.present = true)}
  onMouseLeave={() => (isHovered.present = false)}
>
  {baby}
</movement.div>

//...

Whereas we might merely change the baseVelocity prop when hovering, this might create an abrupt transition. As an alternative, we are able to create a {smooth} animation between the conventional and hover states utilizing Movement’s useSpring hook (docs).

The useSpring hook permits us to animate a movement worth with spring physics. We’ll create a movement worth to trace the hover state and use a spring to easily animate it between 1 (regular velocity) and 0.3 (slowed down) when hovering:

const hoverFactorValue = useMotionValue(1)

const smoothHoverFactor = useSpring(hoverFactorValue, {
  stiffness: 100,
  damping: 20,
})

Now, we are able to use this worth to multiply our moveBy worth, which can make the marquee transfer slower when hovered over.

useAnimationFrame((_, delta) => {
  if (isHovered.present) {
    hoverFactorValue.set(0.3);
  } else {
    hoverFactorValue.set(1);
  }

  const moveBy = ((baseVelocity * delta) / 1000) * smoothHoverFactor.get();
  baseOffset.set(baseOffset.get() + moveBy);
});

The distinction is refined, however these small particulars are value caring for too.

Scroll-based Velocity

We are able to additionally affect the marquee’s velocity primarily based on the scroll velocity and course. We are able to use useScroll to trace absolutely the scroll place in pixels (docs). You may wish to add a container the place you wish to monitor the scroll. In any other case it’s going to simply use the web page scroll, which is okay for us now. Then, we are able to get the speed of our scroll place with the useVelocity hook (docs), by passing the scrollY worth as a parameter.

Now, to keep away from the abrupt modifications in scroll velocity, we’ll use a spring animation once more with useSpring to {smooth} it out. Should you use Lenis or another smooth-scroll library, you may skip this step.

// Scroll monitoring
const { scrollY } = useScroll()

const scrollVelocity = useVelocity(scrollY)

// {smooth} out the scroll velocity
const smoothScrollVelocity = useSpring(scrollVelocity, scrollSpringConfig)

The scroll velocity’s worth might be fairly excessive, so let’s map it to a decrease vary with a useTransform hook.

const { scrollY } = useScroll()

const scrollVelocity = useVelocity(scrollY);
const smoothScrollVelocity = useSpring(scrollVelocity, springConfig);

// map to an affordable vary
const scrollVelocityFactor = useTransform(
  smoothScrollVelocity,
  [0, 1000],
  [0, 5],
  { clamp: false }
)

Let’s additionally reverse the motion course for the marquee once we change the scroll course too. For that, we are going to monitor the course in a ref:

const directionFactor = useRef(1)

Then, we simply want to make use of the scrollVelocityFactor worth to multiply our moveBy worth:

useAnimationFrame((_, delta) => {
  if (isHovered.present) {
    hoverFactorValue.set(0.3);
  } else {
    hoverFactorValue.set(1);
  }
  
  // we have to multiply the bottom velocity with the course issue right here too!
  let moveBy = ((baseVelocity * delta) / 1000) * directionFactor.present * smoothHoverFactor.get();

  if (scrollVelocityFactor.get() < 0) {
    directionFactor.present = -1
  } else if (scrollVelocityFactor.get() > 0) {
    directionFactor.present = 1
  }

  // apply the scroll velocity issue
  moveBy += directionFactor.present * moveBy * scrollVelocityFactor.get();
  baseOffset.set(baseOffset.get() + moveBy);
});

That’s it, now our marquee is influenced by scroll velocity and course!

Mapping Different CSS Properties

One other fascinating characteristic we are able to add right here is to map different CSS properties to the progress on the trail. For instance, we are able to map the opacity of the marquee gadgets to the progress alongside the trail, in order that they may fade out and in on the edges of our path. We are able to use the identical useTransform hook to rework the progress worth with the next components:

f(x) = (1 − |2x − 1|¹⁰)²

Don’t fear if this perform appears to be like complicated. It’s only a good curve that goes from 0 to 1 tremendous fast, holds at 1 for some time, then goes again to 0. We are able to visualize it with a software like GeoGebra:

Visualization of the function f(x) = (1 - |2x - 1|^{10})^2

Right here is how the components interprets to code:

//...

const opacity = useTransform(itemOffset, (v) => {
  const progress = parseFloat(v.exchange("%", "")) / 100;

  // that is only a good curve which matches from 0 to 1 tremendous fast, holds at 1 for some time, then goes again to 0
  // makes the gadgets fade out and in on the edges of the trail
  const x = 2 * progress - 1;
  return Math.pow(1 - Math.pow(Math.abs(x), 10), 2);
});

return (
  <movement.div
    className="marquee-item"
    type={{
      offsetPath: `path('${path}')`,
      offsetDistance: itemOffset,
      offsetRotate: "auto",
      zIndex: zIndex,
      opacity: opacity, // <-- add this line
    }}
    aria-hidden={repeatIndex > 0}
    onMouseEnter={onMouseEnter}
    onMouseLeave={onMouseLeave}
  >
    {kids}
  </movement.div>
);

And the outcome:

Responsiveness

First, let’s simply change again the SVG path’s stroke to black to see what’s occurring.

Now, making the SVG component itself responsive is sort of simple, we are able to simply set the SVG’s container to have a width of 100% and peak of 100%, and do the identical for the SVG component itself as nicely. We are able to preserve the viewBox attribute unchanged, and the path component will scale with the wrapper:

<div className="container">
  <svg
    width="100%"
    peak="100%"
    viewBox="0 0 588 187"
    fill="none"
    xmlns="http://www.w3.org/2000/svg"
  >
    <path d={path} stroke="black" fill="none" />
  </svg>
  {/* ... */}
</div>

And the `index.css` file:

.container {
  place: relative;
  width: 100%;
  peak: 100%;
  margin: 0 auto;
}

Now that is nice for scaling the SVG path itself, however there’s a catch – the marquee gadgets’ offset-path property nonetheless references the unique 588×187 coordinate area! Which means whereas our SVG and its container visually scales, the trail within the offset-path CSS property will nonetheless be within the authentic coordinate area.

Scaling only the svg path but not the offset-path will result in a misaligned path and items

We have now a couple of choices to unravel this:

1. A number of Path Variations

Create a number of SVG path variations for various viewport sizes. Whereas this offers management over the trail at every breakpoint, it requires sustaining a number of path definitions and might be fairly tedious.

2. Scale the Container

Scale the marquee gadgets’ container proportionally utilizing CSS scale transforms. This maintains the unique coordinate area but additionally scales every part contained in the container.

const wrapperRef = useRef<HTMLDivElement>(null);
const marqueeContainerRef = useRef<HTMLDivElement>(null);

useEffect(() => {
  const updateScale = () => {
    const wrapper = wrapperRef.present;
    const marqueeContainer = marqueeContainerRef.present;
    if (!wrapper || !marqueeContainer) return;

    const scale = wrapper.clientWidth / 588;
    marqueeContainer.type.remodel = `scale(${scale})`;
    marqueeContainer.type.transformOrigin = 'high left';
  };

  updateScale();
  window.addEventListener("resize", updateScale);
  return () => window.removeEventListener("resize", updateScale);
}, []);

return (
  <div className="container" ref={wrapperRef}>
    <svg
      width="100%"
      peak="100%"
      viewBox="0 0 588 187"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
    >
      <path d={path} stroke="black" fill="none" />
    </svg>
    <div className="marquee-container" ref={marqueeContainerRef}>
      {/* ... */}
    </div>
  </div>
);

This works nicely, however comes with some vital gotchas. Since we’re scaling every part contained in the marquee-container div, any dimensions you set in your marquee gadgets (like width: 80px) gained’t mirror their true remaining measurement – they’ll be scaled together with every part else. This scaling may trigger undesirable distortions, primarily with textual content components, which might look ugly in some circumstances.

3. Scale the Path

A extra refined (and one way or the other cleaner) strategy is to scale solely the SVG path itself (what’s contained in the d attribute), leaving different components at their authentic measurement. Then, you possibly can outline how your inside components ought to behave on completely different viewports.

Whereas implementing this ourselves could be complicated, there may be an superior article by Jhey Tompkins on the how-to. I actually advocate you undergo it! It’s actually value it, and he does a significantly better job of explaining it than I do. For that reason I’m not going to enter too many particulars, i’ll simply offer you a fast rundown of how we are going to port the identical logic to our marquee.

Let’s set up the D3 library, as we are going to use it to parse and remodel our SVG path.

npm i d3 sorts/d3

First, we have to course of our SVG path to get the precise factors for later. For this, we’ll create a short lived SVG component with the assistance of d3, and use the getTotalLength() methodology to get the entire size of the trail. Then, we’ll use the getPointAtLength() methodology to pattern factors alongside the trail (docs). If the trail is simply too lengthy, we’ll pattern solely a subset of factors, outlined within the maxSamples parameter. Alter it if the scaling takes too lengthy.

/**
 * Parse SVG path string into coordinate factors utilizing D3
 * This extracts the precise coordinates from the trail for scaling
 */
const parsePathToPoints = (pathString: string, maxSamples: quantity = 100): Array<[number, number]> => {
  const factors: Array<[number, number]> = [];
  
  // Create a short lived SVG component to parse the trail
  const svg = d3.create("svg");
  const path = svg.append("path").attr("d", pathString);
  
  // Pattern factors alongside the trail
  const pathNode = path.node() as SVGPathElement;
  if (pathNode) {
    const totalLength = pathNode.getTotalLength();

    // If the trail is simply too lengthy, pattern solely a subset of factors.
    const numSamples = Math.min(maxSamples, totalLength);
    
    for (let i = 0; i <= numSamples; i++) {
      const level = pathNode.getPointAtLength((i / numSamples) * totalLength);
      factors.push([point.x, point.y]);
    }
  }
  
  return factors;

Then, we create a perform that takes the unique path, the unique width and peak of the trail container, and our new container dimensions which we have now to scale to. Within the perform, we use d3’s scaleLinear() perform to create a scale that can map the unique path factors to the brand new container dimensions. Then, we simply use the line() perform to create a brand new path from the scaled factors. We additionally use d3.curveBasis() to {smooth} out the brand new path. There are a bunch of different choices for that, take a look on the docs.

/**
 * Create a scaled path utilizing D3's line generator
 * That is the strategy advisable within the CSS-Tips article
 */
const createScaledPath = (
  originalPath: string,
  originalWidth: quantity,
  originalHeight: quantity,
  newWidth: quantity,
  newHeight: quantity
): string => {
  // Parse the unique path into factors
  const factors = parsePathToPoints(originalPath);
  
  // Create scales for X and Y coordinates
  const xScale = d3.scaleLinear()
    .area([0, originalWidth])
    .vary([0, newWidth]);
    
  const yScale = d3.scaleLinear()
    .area([0, originalHeight])
    .vary([0, newHeight]);
  
  // Scale the factors
  const scaledPoints = factors.map(([x, y]) => [xScale(x), yScale(y)] as [number, number]);
  
  // Create a {smooth} curve utilizing D3's line generator
  const line = d3.line()
    .x(d => d[0])
    .y(d => d[1])
    .curve(d3.curveBasis); // Use foundation curve for {smooth} interpolation
  
  return line(scaledPoints) || "";
};

Lastly, contained in the element, register an occasion listener for the resize occasion, and replace the trail every time the container dimensions change. We are going to do it in a useEffect hook:

//...
  useEffect(() => {
    const updatePath = () => {
      const wrapper = wrapperRef.present;
      if (!wrapper) return;

      const containerWidth = wrapper.clientWidth;
      const containerHeight = wrapper.clientHeight;
      
      // Unique SVG dimensions
      const originalWidth = 588;
      const originalHeight = 187;
      
      // Use D3 to create the scaled path
      const newPath = createScaledPath(
        path, 
        originalWidth, 
        originalHeight, 
        containerWidth, 
        containerHeight
      );
      
      setScaledPath(newPath);
      setCurrentViewBox(`0 0 ${containerWidth} ${containerHeight}`);
    };

    updatePath();
    window.addEventListener("resize", updatePath);
    return () => window.removeEventListener("resize", updatePath);
  }, [path]);

  return (
    <div className="container" ref={wrapperRef}>
      <svg
        width="100%"
        peak="100%"
        viewBox={currentViewBox}
        fill="none"
        xmlns="http://www.w3.org/2000/svg"
      >
        <path d={scaledPath} stroke="black" fill="none" />
      </svg>
      <div className="marquee-container">
        {gadgets.map(({ baby, repeatIndex, itemIndex, key }) => (
          <MarqueeItem
            key={key}
            baseOffset={baseOffset}
            itemIndex={itemIndex}
            totalItems={gadgets.size}
            repeatIndex={repeatIndex}
            zIndexBase={zIndexBase}
            scaledPath={scaledPath}
            isHovered={isHovered}
          >
            {baby}
          </MarqueeItem>
        ))}
      </div>
    </div>
  );

//...

In each circumstances, you could possibly implement a debounced resize occasion listener since constantly resizing the trail is usually a bit laggy. Additionally, for a cleaner implementation you can too use ResizeObserver, however for the sake of simplicity, I’ll depart the implementation of those as much as you.

Remaining Demo

And there you’ve it! For the ultimate demo, I’ve changed the playing cards with some lovely art work items. You could find attribution for every art work within the remaining supply code beneath:

Remaining ideas

Efficiency

  1. The animation complexity will increase with each the variety of components and path complexity. Extra components or complicated paths will influence efficiency, particularly on cellular gadgets. Monitor your body charges and efficiency metrics fastidiously.
  2. Hold your SVG paths easy with {smooth} curves and minimal management factors. Keep away from sharp angles and sudden course modifications. In my expertise, overly complicated paths with too many management factors may end up in uneven or inflexible animations.

Sources

Some useful sources I used whereas researching and constructing this impact, for those who’d prefer to dig deeper:



Supply hyperlink

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles