Search code examples
reactjssvg

React: SVG dynamically add CSS by id or class


In React I have a svg file such as mySVG.svg

<svg width="50" height="50" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
  <rect id="Unit1" class="class1" x="0" y="0" width="10" height="10" fill="blue"/>
  <rect id="Unit2" class="class1" x="10" y="10" width="10" height="10" fill="blue"/>
  <rect id="Unit3" class="class2" x="20" y="20" width="10" height="10" fill="blue"/>
</svg>

Then, in the component file Example.tsx, I would like to dynamically set the svg's colors based on passed in props

import  { ReactComponent as mySvg} from 'assets/mySVG.svg';

export function Example(props:{highlighted:string}):ReactElement {

  if (props.highlighted === 'Unit1') {  
    return (
      <mySvg
        //set unit 1 fill color to red
      />
    );
  }

  return (
    <mySvg />
  );
}

How would I accomplish dynamically setting the fill color of the svg based on id and className of the svg paths?

Importing a static css file with the colors defined won't work because I need this to work dynamically.


Solution

  • Given the svg file mySVG.svg

    <svg width="50" height="50" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
      <rect id="Unit1" class="class1" x="0" y="0" width="10" height="10" fill="blue"/>
      <rect id="Unit2" class="class1" x="10" y="10" width="10" height="10" fill="blue"/>
      <rect id="Unit3" class="class2" x="20" y="20" width="10" height="10" fill="blue"/>
    </svg>
    

    I created a helper function to get the DOM of a given Ref getDOMNodeById.ts

    export type DOMNode = Element | ChildNode | Text | null;
    
    export function getDOMNodeById(componentDOMNode:DOMNode, id:string): DOMNode | void {
      if (componentDOMNode?.nodeType === Node.ELEMENT_NODE) {
        const element = componentDOMNode as Element;
    
        if (element.id === id) {
          return element;
        }
        else if (element.hasChildNodes()) {
          for (let i=0; i<element.childNodes.length; i++) {
            const childNode = element.childNodes[i];
            const result = getDOMNodeById(childNode, id);
            if (result) {
              return result;
            }
          }
        }
      }
    }
    
    export const isHTMLElement = (v:any): v is HTMLElement => {
        return v instanceof Element || v instanceof HTMLElement;
    }
    

    Tying it all together, add a Ref to the svg react component, call the helper function to grab the DOM of Ref, then mutate the returned Element Example.tsx

    import  { ReactComponent as mySvg} from 'assets/mySVG.svg';
    import { DOMNode, getDOMNodeById, isHTMLElement } from './getDOMNodeById';
    
    export function Example(props:{highlighted:string}):ReactElement {
      const svgRef = useRef(null);
      const svgDOM = ReactDOM.findDOMNode(svgRef.current);
    
      if (props.highlighted === 'Unit1') {  
        const unit = getDOMNodeById(svgDOM, 'Unit1');
        if (isHTMLElement(unit)) {
          unit.style.fill = 'red';
        }
      }
    
      return (
        <mySvg ref={svgRef} />
      );
    }