NeuroAgent

How to Animate SVG in React from the Middle of the Stroke

Learn how to properly configure SVG stroke animation in React so the line starts moving from the middle. Complete code examples and optimization methods.

Question

How can I animate SVG in React so that the stroke animation starts from the middle?

I have an SVG file with a rectangle that needs to be animated so that the green line starts moving not from the beginning of the stroke, but from its middle. The current code uses requestAnimationFrame to animate stroke-dashoffset but doesn’t achieve the desired effect.

javascript
useEffect(() => {
    if (!svgRef.current) return;

    const svgElement = svgRef.current;
    const animatedElement = svgElement.querySelector('#svg_5');
    
    // Initialize stroke dasharray
    const length = animatedElement.getTotalLength ? animatedElement.getTotalLength() : 1000;
    animatedElement.style.strokeDasharray = length;
    animatedElement.style.strokeDashoffset = length;
    animatedElement.style.strokeWidth = 2;
    animatedElement.style.stroke = "#00ff7f";

    if (!isAnimated) return;

    let startTime = null;
    const duration = 6000;

    const animate = (timestamp) => {
        if (!startTime) startTime = timestamp;
        const elapsed = timestamp - startTime;
        const progress = Math.min(elapsed / duration, 1);

        setIsProgress(progress);

        const totalLength = animatedElement.getTotalLength ? animatedElement.getTotalLength() : 1000;
        const startOffset = (startStoke / 100) * totalLength;
        const endOffset = 0.6 * totalLength;
        
        const currentOffset = endOffset + (startOffset-endOffset) * progress;
        
        animatedElement.style.strokeDashoffset = currentOffset;
        animatedElement.style.stroke = "#00ff7f";

        if (progress < 1 && isAnimated) {
            animationRef.current = requestAnimationFrame(animate);
        } else if (progress >= 1) {
            setIsAnimated(false);
        }
    };

    animationRef.current = requestAnimationFrame(animate);

    return () => {
        if (animationRef.current) {
            cancelAnimationFrame(animationRef.current);
        }
    };
}, [isAnimated, startStoke, endStoke]);

How do I properly configure the animation parameters so that the line starts moving from the middle of the SVG stroke element?

NeuroAgent

To animate an SVG stroke in React so that the animation starts from the middle, you need to properly configure the stroke-dasharray and stroke-dashoffset parameters. The current code has several issues in calculations and parameter setup.

Table of Contents

Problem with Current Approach

The current code has several key issues:

  1. Incorrect startStoke and endStoke parameters - variables are used but not defined
  2. Incorrect offset calculation - the offset animation logic doesn’t account for the middle of the path
  3. Lack of proper stroke-dasharray configuration - for middle animation, a different configuration is needed

As experts from CSS-Tricks note, the key idea is to “configure the offset to occupy the entire path” and use the correct dash and gap ratios.

Correct Method for Middle Animation

For middle animation, you need to use the following approach:

javascript
useEffect(() => {
    if (!svgRef.current) return;

    const svgElement = svgRef.current;
    const animatedElement = svgElement.querySelector('#svg_5');
    
    // Get the full path length
    const length = animatedElement.getTotalLength();
    
    // Configure parameters for middle animation
    animatedElement.style.strokeDasharray = length / 2; // Half length for dash
    animatedElement.style.strokeDashoffset = length / 4; // Start from quarter path (middle)
    animatedElement.style.strokeWidth = 2;
    animatedElement.style.stroke = "#00ff7f";

    if (!isAnimated) return;

    let startTime = null;
    const duration = 6000;

    const animate = (timestamp) => {
        if (!startTime) startTime = timestamp;
        const elapsed = timestamp - startTime;
        const progress = Math.min(elapsed / duration, 1);

        setIsProgress(progress);

        const totalLength = animatedElement.getTotalLength();
        
        // Animation from middle to end
        const startOffset = length / 4; // Start from middle
        const endOffset = length; // Move to end
        const currentOffset = startOffset + (endOffset - startOffset) * (1 - progress);
        
        animatedElement.style.strokeDashoffset = currentOffset;

        if (progress < 1 && isAnimated) {
            animationRef.current = requestAnimationFrame(animate);
        } else if (progress >= 1) {
            setIsAnimated(false);
        }
    };

    animationRef.current = requestAnimationFrame(animate);

    return () => {
        if (animationRef.current) {
            cancelAnimationFrame(animationRef.current);
        }
    };
}, [isAnimated]);

Full React Component Example

Here’s a complete component with stroke animation starting from the middle:

javascript
import React, { useRef, useEffect, useState } from 'react';

const SVGStrokeAnimation = () => {
    const svgRef = useRef(null);
    const animationRef = useRef(null);
    const [isAnimated, setIsAnimated] = useState(false);
    const [isProgress, setIsProgress] = useState(0);
    const [isAnimatingFromMiddle, setIsAnimatingFromMiddle] = useState(false);

    const startAnimation = () => {
        setIsAnimated(true);
        setIsAnimatingFromMiddle(true);
    };

    const resetAnimation = () => {
        setIsAnimated(false);
        setIsAnimatingFromMiddle(false);
    };

    useEffect(() => {
        if (!svgRef.current) return;

        const svgElement = svgRef.current;
        const animatedElement = svgElement.querySelector('#svg_5');
        
        if (!animatedElement) return;

        // Get the full path length
        const length = animatedElement.getTotalLength();
        
        // Configure parameters for animation
        animatedElement.style.strokeDasharray = length;
        animatedElement.style.strokeDashoffset = length;
        animatedElement.style.strokeWidth = 2;
        animatedElement.style.stroke = "#00ff7f";
        animatedElement.style.transition = 'none';

        if (!isAnimated) return;

        let startTime = null;
        const duration = 6000;

        const animate = (timestamp) => {
            if (!startTime) startTime = timestamp;
            const elapsed = timestamp - startTime;
            const progress = Math.min(elapsed / duration, 1);

            setIsProgress(progress);

            const totalLength = animatedElement.getTotalLength();
            
            if (isAnimatingFromMiddle) {
                // Middle animation
                const middleOffset = length / 2; // Middle of path
                const endOffset = 0; // End of path
                
                // Split animation: first draw half, then second half
                let currentOffset;
                if (progress < 0.5) {
                    // First half: draw from middle to start
                    const firstHalfProgress = progress * 2;
                    currentOffset = middleOffset - (middleOffset - 0) * firstHalfProgress;
                } else {
                    // Second half: draw from middle to end
                    const secondHalfProgress = (progress - 0.5) * 2;
                    currentOffset = middleOffset + (length - middleOffset) * secondHalfProgress;
                }
                
                animatedElement.style.strokeDashoffset = currentOffset;
            } else {
                // Regular animation from start
                const startOffset = length;
                const endOffset = 0;
                const currentOffset = startOffset + (endOffset - startOffset) * progress;
                
                animatedElement.style.strokeDashoffset = currentOffset;
            }

            if (progress < 1 && isAnimated) {
                animationRef.current = requestAnimationFrame(animate);
            } else if (progress >= 1) {
                setIsAnimated(false);
            }
        };

        animationRef.current = requestAnimationFrame(animate);

        return () => {
            if (animationRef.current) {
                cancelAnimationFrame(animationRef.current);
            }
        };
    }, [isAnimated, isAnimatingFromMiddle]);

    return (
        <div>
            <div style={{ marginBottom: '20px' }}>
                <button onClick={startAnimation} disabled={isAnimated}>
                    {isAnimatingFromMiddle ? 'Middle Animation' : 'Start Animation'}
                </button>
                <button onClick={resetAnimation} style={{ marginLeft: '10px' }}>
                    Reset
                </button>
                <div style={{ marginTop: '10px' }}>
                    Progress: {(isProgress * 100).toFixed(1)}%
                </div>
            </div>

            <svg 
                ref={svgRef} 
                width="400" 
                height="300" 
                viewBox="0 0 400 300"
                style={{ border: '1px solid #ccc' }}
            >
                <rect 
                    id="svg_5"
                    x="50" 
                    y="50" 
                    width="300" 
                    height="200" 
                    fill="none"
                    style={{ strokeDasharray: '0', strokeDashoffset: '0' }}
                />
            </svg>
        </div>
    );
};

export default SVGStrokeAnimation;

Alternative CSS Animation Methods

For simpler cases, you can use pure CSS animation, as experts from DEV Community suggest:

css
.svg-stroke-middle {
    stroke-dasharray: 100%;
    stroke-dashoffset: 50%;
    animation: drawFromMiddle 2s ease-in-out forwards;
}

@keyframes drawFromMiddle {
    0% {
        stroke-dashoffset: 50%;
    }
    100% {
        stroke-dashoffset: 0%;
    }
}

In a React component, this can be used like this:

javascript
const SVGStrokeAnimationCSS = () => {
    const [isAnimated, setIsAnimated] = useState(false);

    const startAnimation = () => {
        setIsAnimated(true);
    };

    return (
        <div>
            <button onClick={startAnimation} disabled={isAnimated}>
                Start CSS Animation
            </button>
            
            <svg width="400" height="300" viewBox="0 0 400 300">
                <rect 
                    x="50" 
                    y="50" 
                    width="300" 
                    height="200" 
                    fill="none"
                    className={`svg-stroke-middle ${isAnimated ? 'animated' : ''}`}
                    style={{
                        strokeWidth: 2,
                        stroke: "#00ff7f",
                        strokeDasharray: '100%',
                        strokeDashoffset: '50%',
                        transition: stroke-dashoffset 2s ease-in-out
                    }}
                />
            </svg>
        </div>
    );
};

Parameter Calculations for Different SVG Types

Calculations may differ for different SVG element types:

  1. For rectangles:

    javascript
    const length = 2 * (width + height); // Perimeter of rectangle
    const middleOffset = length / 2; // Middle of perimeter
    
  2. For circles:

    javascript
    const radius = circleElement.getAttribute('r');
    const length = 2 * Math.PI * radius; // Circumference
    const middleOffset = length / 2; // Middle of circumference
    
  3. For complex paths:

    javascript
    const length = pathElement.getTotalLength();
    const middleOffset = length / 2;
    

As experts from Team Treehouse explain, stroke-dasharray and stroke-dashoffset work together to create stroke animation effects.

Performance Optimization

For better performance:

  1. Use will-change:

    css
    .animated-element {
        will-change: stroke-dashoffset;
    }
    
  2. Optimize animation:

    javascript
    // Use requestAnimationFrame with proper frequency
    const animate = (timestamp) => {
        // ... animation logic
        animationRef.current = requestAnimationFrame(animate);
    };
    
  3. Cache values:

    javascript
    const cachedLength = animatedElement.getTotalLength();
    // Use cachedLength instead of repeated calls
    
  4. Use CSS transitions for simple animations:

    javascript
    animatedElement.style.transition = 'stroke-dashoffset 2s ease-in-out';
    animatedElement.style.strokeDashoffset = '0';
    

Thus, to make SVG stroke animation start from the middle, you need to correctly calculate stroke-dashoffset, starting from half the path length, and configure the animation to move to the start and end simultaneously or in stages.

Sources

  1. Animate SVG Lines and Closed Paths with Dash Attributes - Let’s WP
  2. How SVG Line Animation Works | CSS-Tricks
  3. Animating stroke-dasharray and stroke-dashoffset (How To) | Team Treehouse
  4. Simple SVG drawing effect with stroke-dasharray & stroke-dashoffset - DEV Community
  5. stroke-dashoffset | CSS-Tricks Almanac
  6. Animating a complex SVG icon with dash-array and dash-offset in CSS | Justin Marsan