Dynamic SVG images using Next.js

Dynamic SVG images using Next.js

SVG has emerged as a dominant image format, offering unparalleled flexibility and scalability for today’s web development landscape. From its humble beginnings to its widespread adoption, SVG has undergone significant evolution and now plays a pivotal role in shaping the modern web.

What are SVG files used for?

SVG, or Scalable Vector Graphics, was first introduced in 1999 as a web standard by the World Wide Web Consortium (W3C). Since then, it has experienced significant advancements and widespread adoption within the web development community.

Unlike raster images, which are composed of pixels and can lose quality when resized, SVG images are composed of scalable vector shapes defined by mathematical equations. This vector-based format ensures that SVG images retain their clarity and sharpness regardless of the display size, making them ideal for responsive and high-resolution interfaces.

In a nutshell, SVG images are ideal for:

  • Icons

  • Logos

  • Illustrations

Tools and Techniques

Before diving into the code example, it’s important to familiarize yourself with the available tools. Not all tools are created equal, and if you’re committed to standardizing your SVG files for dynamic manipulation, it’s crucial to be aware of the right tools for the job.

Vector Magic: Converting PNG to SVG

When it comes to converting PNG images to SVG, Vector Magic stands out as a reliable tool with a long-standing reputation. While Vector Magic is not free, its automatic vectorization capabilities have made it a go-to choice for many designers and developers. It’s worth noting that it also supports other raster image formats like JPG, BMP, and GIF. With a few simple steps, you can convert bitmap images to high-quality SVG vectors.

However, it’s important to consider certain factors when converting images to SVG. Highly detailed images may result in a larger file size in the SVG format compared to the raster image. Due to the nature of vector graphics, some complex images may not translate as effectively, potentially impacting the overall quality of the resulting SVG.

Inkscape: A Free SVG Editing Software

Inkscape is a versatile and powerful open-source vector graphics editor that excels at editing SVG images. It offers a comprehensive set of tools for creating and modifying SVGs, including drawing, text manipulation, and object transformations. Inkscape also allows for precise editing at the individual node level, making it an excellent choice when precision edits are necessary. While Inkscape provides a rich feature set, it’s important to note that working with complex operations or large files may require additional system resources. Additionally, keep in mind that Inkscape has certain limitations in terms of performance and file handling.

Figma: Smart Resizing Capabilities

Figma, primarily known as a collaborative design tool, also offers robust capabilities for working with SVG images. While not specifically marketed as an SVG editor, Figma provides an intuitive interface and powerful features that make it a viable option for manipulating SVGs. It’s worth mentioning that Figma’s SVG editing capabilities are not as extensive as dedicated tools like Inkscape. However, Figma excels in its ability to resize SVG images without having to rely heavily on the transform attribute. This makes Figma a valuable complement to Inkscape, particularly if you are looking to keep the size consistent across images of the same type (e.g., icons).

Free SVG Assets Online

In addition to creating and editing SVG images, it’s often beneficial to leverage existing SVG assets to enhance your designs. Fortunately, numerous sources offer free SVG assets online. Websites such as SVG Repo, Freepik, and Flaticon provide extensive collections of high-quality SVG icons, illustrations, and other graphical resources that can serve as a starting point or inspiration for your SVG creations.

Optimization Tools for SVG Images

In addition to the techniques we’ve discussed so far, there are optimization tools available that can further enhance SVG images. These tools, such as SVGO and ImageOptim, offer valuable features to reduce file size and clean up SVG markup, making it easier to standardize and optimize the overall performance of SVG assets.

  • SVGO (SVG Optimizer) is a popular tool known for its ability to optimize SVG files. It analyzes the markup and applies various techniques to reduce unnecessary code and optimize the structure of the SVG. By eliminating redundant elements, attributes, and whitespace, SVGO helps reduce file size without compromising the visual quality of the image. It’s like a magic wand that streamlines your SVGs and makes them lighter, ready to be delivered to your users at lightning speed.

  • ImageOptim, on the other hand, is a versatile optimization tool that supports a wide range of image formats, including SVG. It employs advanced compression algorithms to squeeze out every unnecessary byte from your SVG files, resulting in smaller file sizes. By reducing the file size, you not only enhance the loading speed of your web pages but also save precious bandwidth, making your website more efficient and eco-friendly. Plus, ImageOptim has a friendly interface that makes the optimization process enjoyable and effortless.

  • Vector Magic offers a lesser-known technique to further reduce the size of SVG images. SVG files can sometimes turn out larger than expected, even after applying other optimization methods. One clever “hack” involves converting the SVG image back to a PNG file and then re-processing it through Vector Magic with different settings. By selectively reducing the level of detail during the conversion, you can achieve remarkable size reductions. I’ve witnessed SVG images being compressed by up to 60% using this advanced technique.

Making SVG images Dynamic

Making SVG images Dynamic

Next.js comes with an image component that supports both traditional raster images like PNG and SVG files. It uses the <img> HTML tags and loads the images from the public directory, which works well for static images like illustrations. However, if you want to make SVG images dynamic, you have a few options to consider.

What are the options to make SVG images dynamic? One popular approach is to create SVG components using JSX. Here’s an example:

const CheckMarkComponent: React.FC<{
  color?: string;
  strokeWidth?: string;
}> = ({ color, strokeWidth }) => {
  return (
    <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
      <g
        fill="none"
        stroke={color ?? "black"}
        strokeLinecap="round"
        strokeLinejoin="round"
        strokeWidth={strokeWidth ?? "1px"}
        transform="matrix(1,0,0,1,0,0)"
      >
        <path d="M23.25.749,8.158,22.308a2.2,2.2,0,0,1-3.569.059L.75,17.249" />
      </g>
    </svg>
  );
};

The benefit of this approach is that you have more flexibility to customize the SVG since it becomes a React component. However, there’s a drawback: it’s no longer a standalone SVG image. This means you won’t have the convenience of basic tools like SVG previews or thumbnail browsing in directories. It also makes editing the image a bit more cumbersome, as you’ll need to copy and paste between different tools.

As you may have noticed in this example, the attributes (e.g., strokeLinecap) are also converted to camelCase. This is because snake-case is not compatible with JSX format. Depending on the attributes you're using, converting between this format and the traditional SVG format can become quite complex.

Considering these limitations, I suggest using the SVGR Webpack loader, which allows you to import SVG images directly into your Node.js application.

⚠ It’s important to note that when using this solution, the SVG images will be imported directly into your JavaScript bundles. As a result, the overall page size will increase compared to the native Next.js version that loads images asynchronously. For this reason, I don’t recommend using this solution for large illustrations unless they require dynamic functionality. However, it’s an excellent solution for icons since they become part of the page, eliminating any flickering during image loading.

Getting started

As we delve into this discussion, it’s important to know that we’ve conveniently made the solutions discussed in this article accessible via our GitHub repository. This will allow you to engage more practically with the topics we explore.

Now, in order to transform SVG images within your Next.js application, our first step involves installing the SVGR Webpack loader. To do this, open your terminal and execute the following command:

npm i @svgr/webpack --save-dev

Next, we need to configure Next.js (next.config.js) to recognize the SVGR Webpack loader when importing SVG files. Add the following code to your next.config.js file:

/** @type {import('next').NextConfig} */
const nextConfig = {};

nextConfig.webpack = (config, context) => {
  config.module.rules.push({
    test: /\.svg$/,
    use: "@svgr/webpack",
  });
  return config;
};

module.exports = nextConfig;

Now, to ensure TypeScript recognizes the imported SVG files properly, we’ll create a global.d.ts file at the root of your project (if you don’t already have another type file you’re using). Add the following declaration to the global.d.ts file:

// Allow TypeScript to import SVG files using Webpack's `svgr` loader.
declare module "*.svg" {
  import React from "react";
  const content: React.FC<React.SVGProps<SVGSVGElement>>;
  export default content;
}

In order to eliminate the necessity of adding boilerplate code across our examples, we will be utilizing the following two files located under the /app directory of our Next.js project:

layout.tsx

import "./global.css";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html>
      <body
        style={{
          position: "absolute",
          padding: 0,
          margin: 0,
          height: "100%",
          width: "100%",
        }}
      >
        <div
          style={{
            display: "flex",
            flexDirection: "row",
            justifyContent: "center",
            alignItems: "center",
            minHeight: "100%",
            height: "auto",
          }}
        >
          <div style={{ margin: "10%" }}>{children}</div>
        </div>
      </body>
    </html>
  );
}

global.css

svg {
  width: 100%;
}

Now that we have the basic setup ready, let’s test it out by creating a new page under the /app directory. Unlike the Next.js <Image> component, we don’t need to place the SVG file in the /public directory. Instead, we can import the SVG files directly into the pages.

To begin, let’s create a new directory named check-mark and add a new SVG image called CheckMark.svg with the following markup:

<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
  <g
    fill="none"
    stroke="currentColor"
    stroke-linecap="round"
    stroke-linejoin="round"
    stroke-width="currentStrokeWidth"
    transform="matrix(1,0,0,1,0,0)"
  >
    <path d="M23.25.749,8.158,22.308a2.2,2.2,0,0,1-3.569.059L.75,17.249" />
  </g>
</svg>

Next, we can create a new file named page.tsx to display the SVG file:

import CheckMark from "./CheckMark.svg";

export default function CheckMarkPage() {
  return <CheckMark />;
}

Now, if we start Next.js and load the page, we should see a black check mark in the center.

black check mark

Adding dynamism

If you’ve been paying attention to the SVG images, you might have noticed a special value used for the stroke attribute: currentColor. This value is not exclusive to SVG images but is a CSS value that can be utilized in various contexts, including SVG. It refers to the value of the color property of the element that is currently being styled.

In the case of SVG, the stroke property determines the color of the outline or stroke of a shape or path. When you set stroke="currentColor", you’re instructing the SVG element to use the same color as its parent element.

This means that if you set the color of the parent <div> to red, the <CheckMark> element, the SVG image, will also become red. This is particularly handy when using SVG images as icons embedded within text. In most designs, icons share the same color as the text. However, if you ever need to use a different color, you can specify it directly on the SVG component like this:

<CheckMark color={"green"} />

green check mark

This feature is incredibly powerful because it allows you to dynamically select the exact color you need from the same SVG file without duplicating it. You can also apply this technique to almost every supported property.

Another common use case is modifying the stroke width. By default, you can have a small stroke width that works well for icons, but you might need to adjust it if you want to reuse the same image in a larger format.

Let’s try modifying the stroke width of the image:

<CheckMark strokeWidth={"5px"} color={"blue"} />

As expected, this changes the stroke width. However, now we encounter a new problem—the SVG image is larger than its viewbox, and it appears truncated.

blue truncated check mark

Fixing truncated images

Unfortunately, there is no concept of margin for SVG viewboxes. However, there is a way to fix this issue by resizing the viewbox and using negative values to center the image. Luckily, we have already created a component to handle this. You can create the helper function under the /src directory and name it SvgMargin.ts:

import React from "react";

/**
 * Type guard to check if a React node is a functional component.
 *
 * @param node - A React node to check.
 *
 * @returns True if it's a functional component, otherwise false.
 */
const isFunctionalComponent = (
  node: React.ReactNode
): node is React.FunctionComponentElement<React.SVGProps<SVGSVGElement>> => {
  return (
    node !== null &&
    typeof node === "object" &&
    "type" in node &&
    typeof node.type === "function"
  );
};

/**
 * Get the name of a component.
 *
 * @param component - A component.
 *
 * @returns The component name.
 */
const getComponentName = (component: React.ReactElement) =>
  typeof component.type === "string"
    ? component.type
    : (component?.type as React.FunctionComponent)?.displayName ||
      component?.type?.name ||
      "Unknown";

/**
 * Component to add margin around an SVG image.
 */
export const SvgMargin: React.FC<{
  children: React.ReactElement<React.SVGProps<SVGSVGElement>>;
  /** The size of the margin to apply to the SVG image (e.g., 5 will be 5% of the image height/width). */
  size: number;
}> = ({ children, size: marginRatio }) => {
  if (!isFunctionalComponent(children)) {
    return children;
  }

  const SvgComponent = children.type({});

  if (!React.isValidElement<React.SVGProps<SVGSVGElement>>(SvgComponent)) {
    return children;
  }

  const viewBox =
    children?.props?.viewBox ?? SvgComponent?.props?.viewBox ?? "";

  const [x, y, width, height] = viewBox
    .split(" ")
    .map((value) => parseFloat(value));

  if ([x, y, width, height].some((val) => val == null || isNaN(val))) {
    console.error(
      `missing viewBox property for svg ${getComponentName(SvgComponent)}`
    );
    return children;
  }

  const margin = marginRatio / 100;

  // Calculate new x and width values.
  const widthMargin = width * margin;
  const newX = x - widthMargin;
  const newWidth = width + 2 * widthMargin;

  // Calculate new y and height values.
  const heightMargin = height * margin;
  const newY = y - heightMargin;
  const newHeight = height + 2 * heightMargin;

  return React.cloneElement(
    SvgComponent,
    {
      ...children.props,
      viewBox: `${newX} ${newY} ${newWidth} ${newHeight}`,
    },
    SvgComponent.props.children
  );
};

We can use it like this:

import CheckMark from "./CheckMark.svg";
import { SvgMargin } from "@/src/SvgMargin";

export default function CheckMarkPage() {
  return (
    <SvgMargin size={10}>
      <CheckMark strokeWidth={"5px"} color={"blue"} />
    </SvgMargin>
  );
}

And the output:

blue check mark

Controlling shapes and paths independently

The current solution we just demonstrated works for the most common use case. It allows you to change properties for the entire SVG image. However, it has a limitation: you can only change each property to a single value. This means that if you change the color property, it will apply to all shapes and paths that use the currentColor. So, how can we achieve more control?

There are three primary solutions to gaining more control:

  1. Use inline (in the SVG file) <style>

  2. Use Next.js’ built-in CSS modules

  3. Use another helper function

1) Using inline style

Using inline styles is a straightforward option that allows you to leverage the SVG’s inline style capabilities, similar to HTML CSS. The main difference is that the SVGR loader, by default, renames both the class names and id properties to avoid collisions with HTML styles. However, this renaming process occurs behind the scenes, and you’ll only see the end result when inspecting the SVG within your browser. To understand how this works, let’s create a new directory called circles-inline-style and add a new SVG file called CirclesInlineStyle.svg:

<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
  <style>
    /** Ocean breeze */
    svg[data-color-mode="oceanBreeze"] #top-right-circle {
      fill: DeepSkyBlue;
    }

    svg[data-color-mode="oceanBreeze"] #top-left-circle {
      fill: MediumAquaMarine;
    }

    svg[data-color-mode="oceanBreeze"] #bottom-right-circle {
      fill: CornflowerBlue;
    }

    /** Sunset glow */
    svg[data-color-mode="sunsetGlow"] #top-right-circle {
      fill: Coral;
    }

    svg[data-color-mode="sunsetGlow"] #top-left-circle {
      fill: Gold;
    }

    svg[data-color-mode="sunsetGlow"] #bottom-right-circle {
      fill: DarkOrange;
    }
  </style>
  <g transform="matrix(1,0,0,1,0,0)">
    <!-- Top-left circle -->
    <circle id="top-left-circle" fill="black" cx="50" cy="50" r="40" />
    <!-- Top-right circle -->
    <circle id="top-right-circle" fill="black" cx="150" cy="40" r="30" />
    <!-- Bottom-right circle -->
    <circle id="bottom-right-circle" fill="black" cx="150" cy="150" r="20" />
  </g>
</svg>

In this example, we’ve added id properties to all circles, allowing us to target them individually within the <style> tag. I’ve defined two “color modes” called oceanBreeze and sunsetGlow. You can set the color mode by adding a data-color-mode property to the <svg> element. To preview the SVG and see the live result, you can install an SVG preview plugin for VSCode or use other similar tools. For example, to view the SVG with the oceanBreeze color mode, modify the code as follows:

<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" data-color-mode="oceanBreeze">
    <!-- Rest of the SVG file... -->
</svg>

ocean breeze dots

To control the color mode using React, let’s create a new page.tsx in a directory named circles-inline-style:

import CirclesInlineStyle from "./CirclesInlineStyle.svg";

export default function CirclesInlineStylePage() {
  return <CirclesInlineStyle data-color-mode="sunsetGlow" />;
}

The resulting SVG will display the circles with the expected colors:

sunset glow dots

⚠ Be cautious when using inline styles, as different browsers may produce varying results. Make sure to test carefully. Additionally, setting styles on elements like <mask> or using advanced CSS may lead to cross-browser issues. Sticking to basic styles will help avoid these problems.

2) Using Next.js’ built-in CSS modules

To implement this approach, let’s create a new page in a directory called circles-css-module. Within this directory, we’ll create a new CSS module file named circles.module.css:

/** Ocean breeze */
.oceanBreeze [data-node-id="top-right-circle"] {
  fill: DeepSkyBlue;
}

.oceanBreeze [data-node-id="top-left-circle"] {
  fill: MediumAquaMarine;
}

.oceanBreeze [data-node-id="bottom-right-circle"] {
  fill: CornflowerBlue;
}

/** Sunset glow */
.sunsetGlow [data-node-id="top-right-circle"] {
  fill: Coral;
}

.sunsetGlow [data-node-id="top-left-circle"] {
  fill: Gold;
}

.sunsetGlow [data-node-id="bottom-right-circle"] {
  fill: DarkOrange;
}

In this CSS module file, we define different selectors for the color modes “oceanBreeze” and “sunsetGlow”. Instead of using the id selector, we use attribute selectors with data-node-id. This is because the SVGR loader removes any id attributes from elements when inline styles are not used, to avoid collisions with the parent HTML document.

Next, let’s create a new SVG file named CirclesCssModule.svg in the same directory. This SVG file will be identical to the one used in the inline style approach but without the <style> tag. Also, we’ll use data-node-id attributes instead of id to identify the circles:

<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
  <g transform="matrix(1,0,0,1,0,0)">
    <!-- Top-left circle -->
    <circle data-node-id="top-left-circle" fill="black" cx="50" cy="50" r="40" />
    <!-- Top-right circle -->
    <circle data-node-id="top-right-circle" fill="black" cx="150" cy="40" r="30" />
    <!-- Bottom-right circle -->
    <circle data-node-id="bottom-right-circle" fill="black" cx="150" cy="150" r="20" />
  </g>
</svg>

Now, let’s create a new page.tsx file where we can import the SVG component and apply the CSS module:

import CirclesCssModule from "./CirclesCssModule.svg";
import styles from "./circles.module.css";

export default function CirclesCssModulePage() {
  return <CirclesCssModule className={styles.sunsetGlow} />;
}

In this example, we import the CirclesCssModule component from the CirclesCssModule.svg file. We also import the generated CSS module file styles that contain the class names for the color modes.

By applying the styles.sunsetGlow class name as a prop to the CirclesCssModule component, we can dynamically set the color mode.

The resulting SVG will appear the same as the inline style approach but with the advantage of being able to separate the CSS into a style sheet. This means you can utilize additional features like Sass, which is not natively supported within SVG files.

However, one drawback of this approach is that you can no longer preview the styles using standard preview tools since React is required to apply the styles dynamically.

3) Using a helper function

If predefining styles isn’t an option for you and you’re seeking greater flexibility, using a wrapping component may be your only remaining option. We’ve already created such a component to demonstrate how this can be achieved. However, please note that due to the nature of SVGR imports, this is not entry-level code. You can create the helper function under the /src directory and name it SvgController.ts:

import React from "react";

type ExtendedSVGProps = React.SVGProps<SVGSVGElement> & {
  [attr: string]: string;
};

type ControlRule = {
  selector: {
    attributeName: string;
    attributeValue: string;
  };
  props: React.SVGProps<SVGSVGElement>;
};

type ControlRules = ControlRule[];

/**
 * Get an object that indexes control rules.
 *
 * @param rules - The control rules.
 *
 * @returns An object that indexes control rules.
 */
const getPropsSelectorIndex = (
  rules: ControlRules
): { [key: string]: React.SVGProps<SVGSVGElement> } => {
  return rules.reduce((acc, config) => {
    const { attributeName, attributeValue } = config.selector;
    acc[attributeName + ":" + attributeValue] = config.props;
    return acc;
  }, {} as { [key: string]: React.SVGProps<SVGSVGElement> });
};

/**
 * Clone a React node and its children while trying to inject new props.
 *
 * @param node - The node to clone.
 * @param propsSelectorIndex - An object that indexes control rules.
 *
 * @returns The cloned node with new props when applicable.
 */
const cloneNode = (
  node: React.ReactElement<ExtendedSVGProps>,
  propsSelectorIndex: { [key: string]: React.SVGProps<SVGSVGElement> }
): React.ReactElement<ExtendedSVGProps> => {
  const { children, ...restProps } = node.props;
  let nodeProps: Partial<ExtendedSVGProps> & React.Attributes = {
    ...restProps,
  };

  const matchingProps =
    propsSelectorIndex[
      Object.entries(nodeProps).find(
        ([key, value]) => propsSelectorIndex[key + ":" + value]
      )?.[0] +
        ":" +
        Object.entries(nodeProps).find(
          ([key, value]) => propsSelectorIndex[key + ":" + value]
        )?.[1]
    ];

  if (matchingProps) {
    const compatibleProps = Object.entries(matchingProps).reduce(
      (acc, [propKey, propValue]) => {
        if (typeof propValue === "string") {
          acc[propKey] = propValue;
        }
        return acc;
      },
      {} as ExtendedSVGProps
    );
    nodeProps = { ...nodeProps, ...compatibleProps };
  }

  const clonedChildren = React.Children.map(children, (child) =>
    React.isValidElement<ExtendedSVGProps>(child)
      ? cloneNode(child, propsSelectorIndex)
      : child
  );

  return React.cloneElement(node, nodeProps, clonedChildren);
};

/**
 * Type guard to check if a React node is a functional component.
 *
 * @param node - A React node to check.
 *
 * @returns True if it's a functional component, otherwise false.
 */
const isFunctionalComponent = (
  node: React.ReactNode
): node is React.FunctionComponentElement<React.SVGProps<SVGSVGElement>> => {
  return (
    node !== null &&
    typeof node === "object" &&
    "type" in node &&
    typeof node.type === "function"
  );
};

/**
 * Component to control the internal nodes of an SVG image.
 */
export const SvgController: React.FC<{
  rules: ControlRules;
  children: React.ReactElement<ExtendedSVGProps>;
}> = ({ rules, children }) => {
  if (!isFunctionalComponent(children)) {
    return children;
  }

  const SvgComponent = children.type({});
  const propsSelectorIndex = getPropsSelectorIndex(rules);

  return React.isValidElement<ExtendedSVGProps>(SvgComponent)
    ? cloneNode(SvgComponent, propsSelectorIndex)
    : children;
};

What this component does is that it deconstructs the child React element provided by SVGR Webpack and then scans for any matching rules to inject the desired props. The main benefit of this approach is the ability to dynamically specify the injected color instead of predefining them in a style sheet. However, there may be a drawback in terms of performance. Although I haven’t benchmarked this function, it is possible that heavy usage could lead to performance issues.

Now, let’s see it in action! First, create a new directory called circles-helper-function where we can add a new SVG image named CirclesHelperFunction.svg. Use the same SVG markup as the one used by the CSS module (CirclesCssModule.svg).

Next, let’s create a new page.tsx file to demonstrate how the helper function can be utilized:

"use client";

import { useEffect, useState } from "react";
import CirclesHelperFunction from "./CirclesHelperFunction.svg";
import { SvgController } from "@/src/SvgController";

const getRandomColor = () =>
  "#" + Math.floor(Math.random() * 16777215).toString(16);

export default function CirclesHelperFunctionPage() {
  const [topLeftCircleColor, setTopLeftCircleColor] = useState("none");
  const [topRightCircleColor, setTopRightCircleColor] = useState("none");
  const [bottomLeftCircleColor, setBottomLeftCircleColor] = useState("none");

  useEffect(() => {
    const interval = setInterval(() => {
      setTopLeftCircleColor(getRandomColor());
      setTopRightCircleColor(getRandomColor());
      setBottomLeftCircleColor(getRandomColor());
    }, 100);

    return () => clearInterval(interval);
  }, []);

  return (
    <>
      <SvgController
        rules={[
          {
            selector: {
              attributeName: "data-node-id",
              attributeValue: "top-left-circle",
            },
            props: { fill: topLeftCircleColor },
          },
          {
            selector: {
              attributeName: "data-node-id",
              attributeValue: "top-right-circle",
            },
            props: { fill: topRightCircleColor },
          },
          {
            selector: {
              attributeName: "data-node-id",
              attributeValue: "bottom-left-circle",
            },
            props: { fill: bottomLeftCircleColor },
          },
        ]}
      >
        <CirclesHelperFunction />
      </SvgController>
    </>
  );
}

Because the color of each circle is tracked as a state, every time useEffect changes the color, the SvgController component is updated, which then propagates those changes to the CirclesHelperFunction component.

This approach allows you to use a standard SVG file and control all of its nodes using standard React code.

Using the components provided in this article

While we’ve provided useful components in this article, it’s important to understand that they should be used with caution.

The main challenge I faced when creating these components was due to the SVGR Webpack loader returning a functional component that doesn’t offer the same extended capabilities as the original component. This means that, to add these capabilities, we have to use APIs like React.cloneElement, which is not recommended by React. I even had to go a step further and invoke the functional component using .type(), an internal React API not documented in the official React resources.

The “proper” approach to creating these components would likely involve writing a wrapper for the SVGR webpack loader, enabling the same manipulations directly from the imported component.

That being said, while generally it is advised against using cloneElement or internal APIs, the risk is relatively low in this specific use case given the widespread usage of React.

If you prefer to err on the side of caution, all the other examples presented without using these components are safe to use and already offer sufficient flexibility for most common use cases.

Conclusion

In conclusion, the information shared in this article aims to empower you to fully utilize the potential of SVG images. SVGs are incredibly useful for design purposes, especially when combined with popular UI frameworks and tools. This combination allows you to create visually appealing and interactive web experiences. While the focus of this article is mainly on Next.js and React, you can achieve similar results using other technologies by following similar approaches. Ultimately, these techniques work within HTML documents and web browsers, making them universally applicable. So go ahead and dive into using SVGs in your projects using these new tricks, and experience the incredible results they can offer.