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 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"}
        stroke-linecap="round"
        stroke-linejoin="round"
        stroke-width={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.

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 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:

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

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

to red, the 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
  );
};

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)
    transform="matrix(1,0,0,1,0,0)"> id="top-left-circle" fill="black" cx="50" cy="50" r="40" /> id="top-right-circle" fill="black" cx="150" cy="40" r="30" /> id="bottom-right-circle" fill="black" cx="150" cy="150" r="20" />

In this example, we’ve added id properties to all circles, allowing us to target them individually within the