How to declare types of props of SVG component? [React, TypeScript and Webpack]

Basically, what I want to do is to import an SVG icon to my react component and add props to it. Like size="24px" to make it more flexible as a component. Or make it editable with CSS by adding className prop (so I could add e.g. hover prop to it). As it's my first time using TypeScript with Webpack, I'm being confused about how should I declare types for SVG element and I get an error (shown below)

As there are many ways of include SVG I decided to import it as a ReactComponent.

menu-icon.svg

<svg width="24" height="24" viewBox="0 0 24 24"> <path fill="currentColor" fillRule="evenodd" d="M4.5 5h15a.5.5 0 1 1 0 1h-15a.5.5 0 0 1 0-1zm0 6h15a.5.5 0 1 1 0 1h-15a.5.5 0 1 1 0-1zm0 6h15a.5.5 0 1 1 0 1h-15a.5.5 0 1 1 0-1z"></path> </svg> 

header.tsx (here I want my svg icon)

import React from 'react'; import MenuIcon from '../assets/menu-icon.svg'; const Header: React.SFC = () => { return ( <header className="c-header u-side-paddings"> <MenuIcon className="c-header__icon" /> // <-- className prop doesn't match provided type </header> ); }; export default Header; 

index.d.ts (so the .svg file can be treated as a component)

declare module '*.svg' { import React = require('react'); export const ReactComponent: React.SFC<React.SVGProps<SVGSVGElement>>; const src: string; export default src; } 

Adding className prop to MenuIcon SVG Component causes an error:

(JSX attribute) className: string Type '{ className: string; }' is not assignable to type 'IntrinsicAttributes'. Property 'className' does not exist on type 'IntrinsicAttributes'.ts(2322) 

My understanding so far

  • I could just wrap svg component inside a div, and add a className to it like that: <div className="c-header__icon"><MenuIcon/></div> but I feel like it's an inelegant solution and not really a good practice
  • I learned from this answer that SVG props aren't strings, cuz they're an SVGAnimatedString objects. So:
  • I tried to create .tsx file instead of .svg (I wouldn't need index.d.ts file then), but it works only if className's type is string. Also I'm not sure if it's a good practice to store SVG icons in files with different extension that .svg. In my opinion it's not good for clarity. If I'm wrong, tell me what actually good practices are, please. Here's the example:
 import React from 'react'; interface MenuIcon { className?: SVGAnimatedString; } export class MenuIcon extends React.PureComponent<MenuIcon> { render() { return ( <svg width="24" height="24" viewBox="0 0 24 24"> <path fill="currentColor" fillRule="evenodd" d="M4.5 5h15a.5.5 0 1 1 0 1h-15a.5.5 0 0 1 0-1zm0 6h15a.5.5 0 1 1 0 1h-15a.5.5 0 1 1 0-1zm0 6h15a.5.5 0 1 1 0 1h-15a.5.5 0 1 1 0-1z"></path> </svg> ); } } 

I feel like I'm in lack of some basics, It's just really hard for me to figure out what should I focus on, as there are several topics combined

8 Answers

I've been facing this problem, and this solution worked fine for me:

declare module "*.svg" { import { FC, SVGProps } from "react"; const content: FC<SVGProps<SVGElement>>; export default content; }

Besides, I'm using @svgr/webpack as SVG loader along with Next.js

3
import React, { SVGProps } from 'react' // SVGElement props type TSVGElementProps = SVGProps<SVGSVGElement> 
2

You can parameterize svgs for easy JSX.Element level customization via global abstraction. How? Use React's FC. Try implementing the following:

  • Create the following interface
interface SvgIconConstituentValues { strokeColor?: string; strokeWidth?: string; strokeWidth2?: string; strokeWidth3?: string; strokeFill?: string; fillColor?: string; fillColor2?: string; fillColor3?: string; fillColor4?: string; fillColor5?: string; fillColor6?: string; fillColor7?: string; imageWidth?: string; imageHeight?: string; width?: string; height?: string; rotateCenter?: number; className?: string; className2?: string; className3?: string; className4?: string; className5?: string; } export default SvgIconConstituentValues; 
  • import SvgIconConstituentValues into a tsx file
  • import { FC } from React into the same tsx file
import { FC } from 'react'; import SvgIconConstituentValues from 'types/svg-icons'; // FC can be parameterized via Abstraction 
  • create an SvgIcon interface that extends FC and SvgIconConstituentValues
export interface SvgIcon extends FC<SvgIconConstituentValues> {} 
  • abstract the properties of an svg via parametrization with SvgIcon as follows
export const ArIcon: SvgIcon = ({ width = '8.0556vw', height = '8.0556vw', strokeColor = `stroke-current`, strokeWidth = '2', fillColor = 'none', fillColor2 = `fill-primary`, rotateCenter = 0, className = ` antialiased w-svgIcon max-w-svgIcon`, className2 = ` stroke-current`, className3 = ` fill-primary` }): JSX.Element => { return ( <svg width={width} height={height} viewBox='0 0 65 65' fill={fillColor} xmlns=' className={className} transform={`rotate(${rotateCenter}, 65, 65)`} > <circle cx='32.5' cy='32.5' r='31.5' stroke={strokeColor} strokeWidth={strokeWidth} className={className2} /> <path d='M30.116 39H32.816L27.956 26.238H25.076L20.18 39H22.808L23.87 36.084H29.054L30.116 39ZM26.462 28.992L28.226 33.816H24.698L26.462 28.992ZM40.7482 39H43.5202L40.7842 33.78C42.4582 33.294 43.5022 31.944 43.5022 30.162C43.5022 27.948 41.9182 26.238 39.4342 26.238H34.4482V39H36.9502V34.086H38.2462L40.7482 39ZM36.9502 31.944V28.398H38.9662C40.2262 28.398 40.9642 29.1 40.9642 30.18C40.9642 31.224 40.2262 31.944 38.9662 31.944H36.9502Z' fill={fillColor2} className={className3} /> </svg> ); }; 
  • As you can see, there are three separate className parameters (1, 2, 3) abstracted: (1) className for <svg>...</svg>, property JSX.IntrinsicElements.svg: SVGProps<SVGSVGElement>; (2) className2 for <circle /> property JSX.IntrinsicElements.circle: SVGProps<SVGCircleElement>; (3) className3 for <path /> property JSX.IntrinsicElements.path: SVGProps.

  • Notice that const ArIcon: SvgIcon = ({ ... }): JSX.Element => {...} is indeed a JSX.Element. Therefore, the <svg></svg> itself and any children (circles, paths, etc) are all JSX.IntrinsicElements, each allowed to have its own unique className. These className calls were added manually to the svg, as was the transform call (to rotate the icon inline elsewhere).

  • the JSX Attribute className of JSX.IntrinsicElements is defined as follows

SVGAttributes<T>.className?: string | undefined 
  • Each JSX.IntrinsicElement is entitled to a className property. Have 100 paths and a circle within an svg? you can have 102 classNames that can be parameterized via abstraction.

  • Now for the best part. The following is from a file in my portfolio, I tinkered with abstracting svg parameters to make it play nicely with Dark mode toggling (use-dark-mode) and screen-width dependent icon rendering (@artsy/fresnel). You can import this Icon globally and call parameters inline within each JSX.Element without any props passing

import { ArIcon } from 'components/svg-icons'; import Link from 'next/link'; import { Media } from 'components/window-width'; import { Fragment } from 'react'; import DarkMode from 'components/lead-dark-mode'; const ArIconConditional = (): JSX.Element => { const arIconXs: JSX.Element = ( <Media at='xs'> <Link href='/'> <a className='container block pl-portfolio pt-portfolio justify-between mx-auto w-full min-w-full ' id='top' aria-label='top' > <ArIcon width='18vw' height='18vw' className='transition-all transform translate-y-90' className2='transition-all duration-1000 delay-200 transform' className3='text-secondary fill-secondary' /> </a> </Link> </Media> ); const arIconSm: JSX.Element = ( <Media at='sm'> <Link href='/'> <a className='container block pl-portfolio pt-portfolio justify-between mx-auto w-full min-w-full ' id='top' aria-label='top' > <ArIcon width='15vw' height='15vw' className='' className2='' className3='' /> </a> </Link> </Media> ); const arIconMd: JSX.Element = ( <Media at='md'> <Link href='/'> <a className='container block pl-portfolio pt-portfolio justify-between mx-auto w-full min-w-full ' id='top' aria-label='top' > <ArIcon width='12.5vw' height='12.5vw' className='' className2='' className3='' /> </a> </Link> </Media> ); const arIconDesktop: JSX.Element = ( <Media greaterThan='md'> <Link href='/'> <a className='container block pl-portfolio pt-portfolio justify-between mx-auto w-full min-w-full ' id='top' aria-label='top' > <ArIcon width='10vw' height='10vw' className='' className2='' className3='' /> </a> </Link> </Media> ); const ArIconsCoalesced = (): JSX.Element => ( <Fragment> <div className='relative block justify-between lg:w-auto lg:static lg:block lg:justify-start transition-all w-full min-w-full col-span-5'> {arIconXs} {arIconSm} {arIconMd} {arIconDesktop} </div> </Fragment> ); return ( <Fragment> <div className='select-none relative z-1 justify-between pt-portfolioDivider navbar-expand-lg grid grid-cols-6 min-w-full w-full container overflow-y-hidden overflow-x-hidden transform'> <ArIconsCoalesced /> <div className='pt-portfolio'> <DarkMode /> </div> </div> </Fragment> ); }; export default ArIconConditional; 
  • This project uses tailwindcss and React's Next.js framework. That said, what if I wanted the JSX.IntrinsicElement circle encompassing the Icon to pulse on mobile only? add tailwind's animate-pulse to className2 as follows
// ... const arIconXs: JSX.Element = ( <Media at='xs'> <Link href='/'> <a className='container block pl-portfolio pt-portfolio justify-between mx-auto w-full min-w-full ' id='top' aria-label='top' > <ArIcon width='18vw' height='18vw' className='transition-all transform translate-y-90' className2='transition-all duration-1000 delay-200 transform animate-pulse' className3='text-secondary fill-secondary' /> </a> </Link> </Media> ); // ... 
  • The fill-primary call is a css variable defined for both .dark-mode and .light-mode css classes which are then passed to :root and activated on the toggling of darkMode by the client (onChange={darkMode.toggle}).

  • So, onClick={darkMode.enable} triggers the icon to change its fillColor and strokeColor values as a function of css variables. Utilizing React's FC to parameterize props via abstraction produces a truly remarkable degree of granular control. Customizing SVGs using inline calls at the JSX.Element level globally has never been so seamless.

  • darkMode.disable

darkMode.disable

  • darkMode.enable

darkMode.enable

  • Check out my recent DEV post if hacking the react-fontawesome library using typescript and next to create custom fontawesome SVG Icons that persist to production and are unchanged by library version updates piques your interest.

Cheers

2

This worked in the case of using Next.js

declare module "*.svg" { import {ReactElement, SVGProps} from "react"; const ReactComponent: (props: SVGProps<SVGElement>) => ReactElement; export {ReactComponent} } 
1

This one worked for me:

declare module '*.svg' { const content: React.ElementType<React.ComponentPropsWithRef<'svg'>>; export default content; } 

Gabriels answer lacks ref attribute.

Since NextJS was overriding my custom.d.ts exported *.svg module, I ended up importing all svg files into one Icons.ts file, and typing them as follows:

import { ReactElement, SVGProps } from 'react'; import Rotate from './360.svg'; import Clock from './clock.svg'; import Close from './close.svg'; import Discord from './discord.svg'; import Github from './github.svg'; import Hamburger from './hamburger.svg'; import Location from './location.svg'; import Logo from './logo.svg'; import MapPin from './map-pin.svg'; import Medium from './medium.svg'; const importedIcons = { Rotate, Clock, Close, Discord, Github, Hamburger, Location, Logo, MapPin, Medium, }; type IconName = keyof typeof importedIcons; type ReactComponent = (props: SVGProps<SVGElement>) => ReactElement; export default importedIcons as Record<IconName, ReactComponent>; 

Then when I want to use an icon, I can import it as follows, and intellisense picks up svg prop values.

import Icons from '~assets/icons/Icons'; ... <Icons.Rotate fill="green" stroke="purple" height={10} /> ... 

Not the default configuration, but it works!

Solution for next > 11 (resolving declare module '*.svg' not working)

You can add static type definitions in a *.d.ts file (as suggested in Gabrie's answer):

// e.g. global.d.ts declare module '*.svg' { import type { ReactElement, SVGProps } from 'react'; const content: (props: SVGProps<SVGElement>) => ReactElement; export default content; } 

The problem with this is that next adds its own image static types which overrides those you define. To disable these image static types forced by next, add images.disableStaticImages in your next.config.js like this:

const nextConfig = { // ..... images: { disableStaticImages: true, }, // ..... } module.exports = nextConfig; 

and run yarn dev to apply

Hexagon.tsx

import * as React from "react"; const Hexagon= (props:any) => ( <svg xmlns="" xmlnsXlink="" x="0px" y="0px" viewBox="0 0 40 40" style={{ enableBackground: "new 0 0 512 512", }} xmlSpace="preserve" width={props.size ? props.size : 40} height={props.size ? props.size : 40} fill={"currentColor"} {...props} > <path points="25.045,128 256,0 486.955,128 486.955,384 256,512 25.045,384 " d="M1.957 10L20 0L38.043 10L38.043 30L20 40L1.957 30Z" /> </svg> ); export default Hexagon; 

Then import your component and use

<Hexagon /> {/* this one will be default size (40) and default to whatever is the currentColor */} 

or

<Hexagon size={100} style={{ color: "green" }} /> {/* this one will be larger and green */} 

Your Answer

Sign up or log in

Sign up using Google Sign up using Facebook Sign up using Email and Password

Post as a guest

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

You Might Also Like