Skip to content
SVGtoReact

SVG to TSX: Typed SVG Icons in React

Move from SVG to TSX with typed props: SVGProps or ComponentProps, size and title props, refs in React 19 and 18, a typed icon map, and .svg module types.

By SVG to React. Published . Updated .

Converting SVG to TSX takes one extra step on top of JSX: a type for the props. That step is small, but it decides how pleasant the icon set is to use. Good types catch an invalid icon name at compile time. They suggest strokeWidth in the editor, and they let a wrapper component accept everything an svg element accepts. This post builds that up in stages, starting from what SVGR generates.

The starting point: SVGProps

With the typescript option, SVGR types the component's props as SVGProps<SVGSVGElement>. This is the output of @svgr/core for a one-path icon, reformatted:

Icon.tsx (SVGR output)
import * as React from "react";
import type { SVGProps } from "react";
const Icon = (props: SVGProps<SVGSVGElement>) => (
  <svg xmlns="http://www.w3.org/2000/svg" width={24} height={24} viewBox="0 0 24 24" {...props}>
    <path strokeWidth={2} d="M1 1h10" />
  </svg>
);
export default Icon;

SVGProps<SVGSVGElement> comes from React's type definitions. It covers every SVG attribute React knows, plus event handlers typed for an SVGSVGElement target. It also includes ref and key. That is enough for most icons, and it is what the SVG to TSX converter emits.

ComponentProps and ComponentPropsWithoutRef

React's types offer a second way to describe the same thing. ComponentProps<"svg"> reads the props of the intrinsic svg element from the JSX type definitions. ComponentPropsWithoutRef<"svg"> is the same type without ref. The version without ref is useful for a public props type. You decide separately whether the component accepts a ref, instead of inheriting that decision.

A shared props type for an icon set usually adds two things: a single size prop, and an accessible title.

types.ts
import type { ComponentPropsWithoutRef } from "react";

export type IconProps = Omit<ComponentPropsWithoutRef<"svg">, "children"> & {
  /** Sets width and height together. Defaults to 1em so the icon follows font size. */
  size?: number | string;
  /** Accessible name. Without it, the icon is hidden from assistive technology. */
  title?: string;
};

children is removed because an icon draws its own shapes. If a caller passes children, it is almost always a mistake.

An icon that uses the type

CheckIcon.tsx
import { useId } from "react";
import type { IconProps } from "./types";

export function CheckIcon({ size = "1em", title, ...props }: IconProps) {
  const titleId = useId();
  const labelled = title !== undefined && title !== "";
  return (
    <svg
      viewBox="0 0 24 24"
      width={size}
      height={size}
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      role={labelled ? "img" : undefined}
      aria-hidden={labelled ? undefined : true}
      aria-labelledby={labelled ? titleId : undefined}
      {...props}
    >
      {labelled ? <title id={titleId}>{title}</title> : null}
      <path d="m5 12 5 5L20 7" />
    </svg>
  );
}

Three details are worth copying. The size default is "1em", so the icon matches the surrounding text unless told otherwise. The accessibility attributes depend on whether a title was given. Without a title the icon is decorative and hidden. With a title it gets role="img" and a label. And {...props} is spread last, so a caller can still override any of those defaults, including aria-hidden.

useId gives each instance its own title ID. Two check icons on one page then never point aria-labelledby at the same element. SVGR's titleProp option generates a similar pattern with title and titleId props, where the caller supplies the ID.

What TypeScript catches, and what it does not

A misspelled camelCase prop such as strokeWidht is a compile error. So is class, because SVGProps has no prop by that name. A wrong value type is an error too, such as an object where a string is expected.

Hyphenated names are the gap. TypeScript does not type-check JSX attributes whose name contains a hyphen, so stroke-width="2" compiles without complaint. That rule exists so that data-* and aria-* attributes work without declarations. It also lets SVG names pasted from a file slip through. React's development warning catches them at runtime, and converting with a tool rather than by hand avoids them in the first place. The rules themselves are listed in SVG to JSX: Attribute Rules Explained.

Refs in React 19 and React 18

You need a ref on an icon to measure it, focus it, or hand it to an animation library. How you accept one depends on the React version.

Refs
// React 19: ref is a regular prop on function components
import type { ComponentProps } from "react";

export function DotIcon(props: ComponentProps<"svg">) {
  return (
    <svg viewBox="0 0 8 8" {...props}>
      <circle cx="4" cy="4" r="4" fill="currentColor" />
    </svg>
  );
}

// React 18: forwardRef is required to accept a ref
import { forwardRef } from "react";
import type { SVGProps } from "react";

export const DotIcon18 = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(
  function DotIcon18(props, ref) {
    return (
      <svg ref={ref} viewBox="0 0 8 8" {...props}>
        <circle cx="4" cy="4" r="4" fill="currentColor" />
      </svg>
    );
  },
);

In React 19, ref is passed to function components as an ordinary prop. Typing props as ComponentProps<"svg"> and spreading them is enough. In React 18, a function component never receives ref in its props, so you need forwardRef. React 19 still supports forwardRef, so the React 18 form works in both versions if a library has to support both. SVGR's ref option emits the forwardRef form.

A typed icon map

When icons are chosen by name, for example from a config file or a CMS field, a map with as const turns the names into a union type:

Icon.tsx
import { CheckIcon } from "./CheckIcon";
import { CloseIcon } from "./CloseIcon";
import { InfoIcon } from "./InfoIcon";
import type { IconProps } from "./types";

const icons = {
  check: CheckIcon,
  close: CloseIcon,
  info: InfoIcon,
} as const;

export type IconName = keyof typeof icons; // "check" | "close" | "info"

export function Icon({ name, ...props }: IconProps & { name: IconName }) {
  const Component = icons[name];
  return <Component {...props} />;
}

// <Icon name="chek" />  -> type error: "chek" is not assignable to IconName

Adding an icon to the object adds its name to IconName automatically, and there is no separate list to keep in sync. One caution: a map like this imports every icon wherever Icon is used, so a bundler cannot drop the unused ones. For a large set, export each icon directly and keep the map for the places that really choose icons at runtime.

Typing .svg imports

If a bundler turns .svg files into components at build time, TypeScript needs to know what an import of such a file returns. Declare it once:

Declaration files
// src/svg.d.ts, for @svgr/webpack with the default export as the component
declare module "*.svg" {
  import type { FC, SVGProps } from "react";
  const Component: FC<SVGProps<SVGSVGElement>>;
  export default Component;
}

// src/vite-env.d.ts, for vite-plugin-svgr and imports ending in ?react
/// <reference types="vite-plugin-svgr/client" />

Match the declaration to the loader's real export. Some setups export the component as ReactComponent and a URL as the default export. If the declaration says one thing and the loader does another, the types will compile and the import will fail at runtime.

Generating typed icons in bulk

For a handful of icons, paste each file into the SVG to TSX converter and adapt the output to your shared props type. For a folder, the batch tool emits TSX for every file in one zip. Icon library mode adds an index file that re-exports each component. SVGR's CLI does the same locally with --typescript, and its template option lets every generated file use your own IconProps type instead of the default.

The JavaScript version of the steps in this post, including IDs and accessibility, is in SVG to React Component: A Practical Guide. For Next.js specifics, see SVG in Next.js: Inline, img, or Component.

Questions

SVGProps<SVGSVGElement> or ComponentProps<"svg">?

Both describe the props of an svg element, and in current React types both include ref. SVGProps<SVGSVGElement> is what SVGR emits. ComponentProps<"svg"> reads the type from React's JSX intrinsic elements. Use ComponentPropsWithoutRef<"svg"> when a wrapper must not accept a ref.

Why does TypeScript not flag stroke-width in my TSX?

TypeScript does not type-check JSX attribute names that contain a hyphen, so stroke-width passes the compiler even though React expects strokeWidth. Misspelled camelCase names such as strokeWidht are caught. React's development warning is the safety net for hyphenated names.

Do I still need forwardRef?

In React 19, function components receive ref as a regular prop, so an icon can read ref from its props and pass it to the svg element. In React 18, a function component needs forwardRef to accept a ref. SVGR's ref option generates the forwardRef form.

How do I type imports of .svg files?

Add a declaration file that declares the module pattern your bundler uses. For vite-plugin-svgr, reference vite-plugin-svgr/client. For @svgr/webpack, declare module "*.svg" with a default export typed as a component that takes SVGProps<SVGSVGElement>.