In this tutorial we will create an SVG image with an animated draw effect in React, using hooks, that will be "drawn" onto the page when scrolled into view
In this tutorial we will create an SVG image with an animated draw effect in React, using hooks, that will be “drawn” onto the page when scrolled into view, it will look something like the signature below.
SVG Image
You’ll need an SVG image to get started, so let’s get that created. I’m going to use Inkscape for this since it’s free and I don’t have a graphics tablet for my computer, you can download Inkscape from their website .
I went ahead and drew my image by hand first, snapped a photo of it, and then traced it. Once you have Inkscape open, you can drag any image file onto the page to import it.
Select the “Draw Bezier curves and straight lines” tool

This part is a bit tricky, but take your time and you’ll get the hang of it pretty quickly. You’ll want to try to plan the drawing so that it is one continuous line, if you have multiple lines that’s ok, they will just both be drawn at once (it looks better as one continuous line, you could make two separate paths if you had to and use CSS animation timing to make them draw one ofter the other).
Click on the starting point of the drawing (over top of your template image if you are using one, I just used a fun font in this example).

If Inkscape starts rendering shapes over your template that get in the way, skip ahead to the steps for turning off the fill and turning on stroke, that should clear up the screen for easier tracing.
Click the next point in the image, but hold the mouse button down, now as you move the mouse it will adjust the curve, once you are happy with it you can let go of the mouse button and continue on to the next point. Here is a straight line (single click):

Here is a curved line (click and drag):

You may have to get creative to connect the lines:

When you get to the end, hit the enter
key on your keyboard to finalize the line, you’ll see a dotted outline around it.

Select the “Select and transform objects” tool.

Click and drag your new line away from the original drawing, I hope yours looks better than mine does here! (Yours may not be solid black yet, we will fix that in the next steps).

Choose “Fill and Stroke…” from the top menu bar.

Make sure the fill is set to “No paint”.

Set the “Stroke paint” to “Flat color”, and choose a color here if you like.

Set the “Stroke style” width value to taste, and set the “Cap” to rounded so it looks nice and clean. (You can also round out the “Join” setting if needed).

Click on the original drawing and hit the delete
key on your keyboard to remove it.

Select your line drawing and press ctrl + shift + R
on your keyboard to shrink the page down to the size of the image, it should look like this:

Save the file, then head over to
smooth-code.com
and utilize their very awesome SVGR tool to convert the SVG image into a React component (You can do this by hand if you wish, most of the property name conversions are just going from kebab-case-like-this
to camelCaseLikeThis
, it’s just faster and easier to use a tool).
Drag the SVG file into the left side of the page, then copy the output from the right side into your project. Here’s the output for my example:
import React from 'react';
const SvgComponent = (props) => (
<svg
width={127.237}
height={53.457}
viewBox='0 0 33.665 14.144'
{...props}
>
<path
d='M5.27 7.113l-1.7-4.158S3.475.593 4.23.215c.756-.378 3.78.567 4.158 3.118.378 2.552-.472 6.237-2.551 8.316-2.08 2.079-6.142.189-5.67-.567.472-.756 3.024-5.292 2.079-6.331C1.302 3.71.263 12.877.64 13.255c.378.378 5.009 1.323 6.426.284 1.417-1.04 0-1.323 1.323-1.323s3.969-1.89 4.158-5.386c.189-3.497 1.606.283 1.228 1.606-.378 1.323-.756 3.024.473 4.063 1.228 1.04 3.023-1.228 3.118-2.55.094-1.324 3.213-4.348 2.362-3.12-.85 1.23-2.646 3.12-2.362 4.725.283 1.607 2.646 1.134 2.835.19.189-.945 1.134-6.71 1.04-4.914-.095 1.795-.284 5.008.472 5.291.756.284 1.512.756 2.834-1.417 1.323-2.173 1.89-6.804 1.512-4.441-.378 2.362-1.7 5.291-.567 5.858 1.134.567 2.174-.283 2.646-2.362.473-2.079.945-7.181.662-3.874-.284 3.307-.756 5.953.472 6.33 1.229.379 1.701.095 1.701-1.794 0-1.89-.661-4.158.095-4.064.756.095 2.55.756 2.55.756'
fill='none'
stroke='#000'
strokeWidth={0.265}
/>
</svg>
);
export default SvgComponent;
Animated React Component
You’ll want to have styled-components
, and react-intersection-observer
installed in your project before you proceed.
npm install --save styled-components react-intersection-observer
Let’s add the necessary imports to the component and wrap the functional component body in curly braces to remove the implicit return.
import React, { useState, createRef, useEffect } from 'react';
import styled from 'styled-components';
import { useInView } from 'react-intersection-observer';
const SvgComponent = (props) => {
return (
<svg
width={127.237}
height={53.457}
viewBox='0 0 33.665 14.144'
{...props}
>
<path
d='M5.27 7.113l-1.7-4.158S3.475.593 4.23.215c.756-.378 3.78.567 4.158 3.118.378 2.552-.472 6.237-2.551 8.316-2.08 2.079-6.142.189-5.67-.567.472-.756 3.024-5.292 2.079-6.331C1.302 3.71.263 12.877.64 13.255c.378.378 5.009 1.323 6.426.284 1.417-1.04 0-1.323 1.323-1.323s3.969-1.89 4.158-5.386c.189-3.497 1.606.283 1.228 1.606-.378 1.323-.756 3.024.473 4.063 1.228 1.04 3.023-1.228 3.118-2.55.094-1.324 3.213-4.348 2.362-3.12-.85 1.23-2.646 3.12-2.362 4.725.283 1.607 2.646 1.134 2.835.19.189-.945 1.134-6.71 1.04-4.914-.095 1.795-.284 5.008.472 5.291.756.284 1.512.756 2.834-1.417 1.323-2.173 1.89-6.804 1.512-4.441-.378 2.362-1.7 5.291-.567 5.858 1.134.567 2.174-.283 2.646-2.362.473-2.079.945-7.181.662-3.874-.284 3.307-.756 5.953.472 6.33 1.229.379 1.701.095 1.701-1.794 0-1.89-.661-4.158.095-4.064.756.095 2.55.756 2.55.756'
fill='none'
stroke='#000'
strokeWidth={0.265}
/>
</svg>
);
};
export default SvgComponent;
We need to set up useInView
to determine if the component is in the viewport or not, a reference for the actual SVG path to determine it’s length, and a state variable for the length value.
...
const SvgComponent = props => {
const [inViewRef, inView] = useInView({
triggerOnce: true
})
const pathRef = createRef()
const [pathLength, setPathLength] = useState()
return (
...
We also need to calculate the length of the SVG path, since the useEffect
hook will run before the SVG is loaded we’ll use the useEffect
hook to make sure it updates when the SVG does get loaded.
...
const SvgComponent = props => {
const [inViewRef, inView] = useInView({
triggerOnce: true
})
const pathRef = createRef()
const [pathLength, setPathLength] = useState()
useEffect(
() => {
if (pathRef.current) {
setPathLength(pathRef.current.getTotalLength())
}
},
[pathRef]
)
return (
...
Let’s wrap our svg
in a Wrapper
styled component that we will create down below, we will pass some properties to the Wrapper
, svg
, and path
elements:
<Wrapper>
ref={inViewRef}
: This will makeWrapper
the target of intersection observer, so theinView
property will change fromfalse
totrue
when theWrapper
is scrolled into viewpathLength={pathLength}
: We need to know the length of the SVG path in the CSS for the styled component, so we’ll pass that property in here for that purpose, this could also be written as{...{pathLength}}
if the destructured declaration looks cleaner to you.
<svg>
className={inView ? 'animated visible' : 'animated'}
: We will add thevisible
class name to this element when theinView
property becomes true using a ternary operator
<path>
ref={pathRef}
: Makes this path the target of theuseEffect
hook above to determine it’s length
...
return (
<Wrapper ref={inViewRef} pathLength={pathLength}>
<svg
className={inView ? 'animated visible' : 'animated'}
width={127.237}
height={53.457}
viewBox='0 0 33.665 14.144'
{...props}
>
<path
ref={pathRef}
d='M5.27 7.113l-1.7-4.158S3.475.593 4.23.215c.756-.378 3.78.567 4.158 3.118.378 2.552-.472 6.237-2.551 8.316-2.08 2.079-6.142.189-5.67-.567.472-.756 3.024-5.292 2.079-6.331C1.302 3.71.263 12.877.64 13.255c.378.378 5.009 1.323 6.426.284 1.417-1.04 0-1.323 1.323-1.323s3.969-1.89 4.158-5.386c.189-3.497 1.606.283 1.228 1.606-.378 1.323-.756 3.024.473 4.063 1.228 1.04 3.023-1.228 3.118-2.55.094-1.324 3.213-4.348 2.362-3.12-.85 1.23-2.646 3.12-2.362 4.725.283 1.607 2.646 1.134 2.835.19.189-.945 1.134-6.71 1.04-4.914-.095 1.795-.284 5.008.472 5.291.756.284 1.512.756 2.834-1.417 1.323-2.173 1.89-6.804 1.512-4.441-.378 2.362-1.7 5.291-.567 5.858 1.134.567 2.174-.283 2.646-2.362.473-2.079.945-7.181.662-3.874-.284 3.307-.756 5.953.472 6.33 1.229.379 1.701.095 1.701-1.794 0-1.89-.661-4.158.095-4.064.756.095 2.55.756 2.55.756'
fill='none'
stroke='#000'
strokeWidth={0.265}
/>
</svg>
</Wrapper>
)
}
export default SvgComponent
To make the SVG image responsive, we need to create or modify the viewBox
property in the svg
element, this may or may not be there already depending on the original SVG image. The viewBox
attribute takes four values, min-x min-y width height
, you’ll want to leave the first two (min-x
and min-y
) as 0
, and take the width
and height
values from the actual width
and height
attributes, and then remove those attributes. This is so you can set the width and height of the image in CSS and it will scale the image rather than crop it.
So we will go from this:
<svg
className={inView ? 'animated visible' : 'animated'}
width={127.237}
height={53.457}
viewBox='0 0 33.665 14.144'
{...props}
>
To this:
<svg
className={inView ? 'animated visible' : 'animated'}
viewBox='0 0 127.237 53.457'
{...props}
>
Now we just need the Wrapper
styled component, we’ll use 100%
for the width
and height
here, and set max-width
to 300px
to make it responsive, you can play with the max-width
as needed.
The stroke-dasharray
property will transform the stroke into a dashed line, by setting this to the length of the SVG path we will get one dash that is the full length of the path and one blank section that is the same length.
Setting the stroke-dashoffset
to the same value will shift the blank part of the dashed stroke into view, and setting it to 0
in the keyframes will move the blank part back out of view thereby exposing the single dash (i.e. our SVG image), pretty simple, right? Feel free to adjust the timing to taste here as well, it is set to six seconds here in the line animation: draw 6s linear forwards;
.
...
const Wrapper = styled.div`
.animated {
max-width: 300px;
width: 100%;
height: 100%;
stroke-dasharray: ${props => props.pathLength};
stroke-dashoffset: ${props => props.pathLength};
}
.animated.visible {
animation: draw 6s linear forwards;
}
@keyframes draw {
from {
stroke-dashoffset: ${props => props.pathLength};
}
to {
stroke-dashoffset: 0;
}
}
`
export default SvgComponent
Here’s the whole thing all together for reference.
import React, { useState, createRef, useEffect } from 'react';
import styled from 'styled-components';
import { useInView } from 'react-intersection-observer';
const SvgComponent = (props) => {
const [inViewRef, inView] = useInView({
triggerOnce: true,
});
const pathRef = createRef();
const [pathLength, setPathLength] = useState();
useEffect(() => {
if (pathRef.current) {
setPathLength(pathRef.current.getTotalLength());
}
}, [pathRef]);
return (
<Wrapper
ref={inViewRef}
pathLength={pathLength}
>
<svg
className={inView ? 'animated visible' : 'animated'}
viewBox='0 0 127.237 53.457'
{...props}
>
<path
ref={pathRef}
d='M5.27 7.113l-1.7-4.158S3.475.593 4.23.215c.756-.378 3.78.567 4.158 3.118.378 2.552-.472 6.237-2.551 8.316-2.08 2.079-6.142.189-5.67-.567.472-.756 3.024-5.292 2.079-6.331C1.302 3.71.263 12.877.64 13.255c.378.378 5.009 1.323 6.426.284 1.417-1.04 0-1.323 1.323-1.323s3.969-1.89 4.158-5.386c.189-3.497 1.606.283 1.228 1.606-.378 1.323-.756 3.024.473 4.063 1.228 1.04 3.023-1.228 3.118-2.55.094-1.324 3.213-4.348 2.362-3.12-.85 1.23-2.646 3.12-2.362 4.725.283 1.607 2.646 1.134 2.835.19.189-.945 1.134-6.71 1.04-4.914-.095 1.795-.284 5.008.472 5.291.756.284 1.512.756 2.834-1.417 1.323-2.173 1.89-6.804 1.512-4.441-.378 2.362-1.7 5.291-.567 5.858 1.134.567 2.174-.283 2.646-2.362.473-2.079.945-7.181.662-3.874-.284 3.307-.756 5.953.472 6.33 1.229.379 1.701.095 1.701-1.794 0-1.89-.661-4.158.095-4.064.756.095 2.55.756 2.55.756'
fill='none'
stroke='#000'
strokeWidth={0.265}
/>
</svg>
</Wrapper>
);
};
const Wrapper = styled.div`
.animated {
max-width: 300px;
width: 100%;
height: 100%;
stroke-dasharray: ${(props) => props.pathLength};
stroke-dashoffset: ${(props) => props.pathLength};
}
.animated.visible {
animation: draw 6s linear forwards;
}
@keyframes draw {
from {
stroke-dashoffset: ${(props) => props.pathLength};
}
to {
stroke-dashoffset: 0;
}
}
`;
export default SvgComponent;