Skip to content

SVGR strips px from CSS variables in inline styles #1020

@HondaYt

Description

@HondaYt

🐛 Bug Report

When converting inline styles, SVGR strips the px unit from CSS custom property values (e.g., --s: 20px becomes --s: 20). React does not auto-append px to custom properties, so rendering breaks.

To Reproduce

  1. Run (version pinned for reproducibility):
npx @svgr/cli@8.1.0 --no-svgo <<< '<svg style="--s: 20px;" viewBox="0 0 20 20"><path d="M0 0"/></svg>'
  1. Observe output:
import * as React from "react";
const SvgComponent = (props) => (
  <svg style={{ "--s": 20 }} viewBox="0 0 20 20" {...props}><path d="M0 0" /></svg>
);
export default SvgComponent;

Expected output

import * as React from "react";
const SvgComponent = (props) => (
  <svg style={{ "--s": "20px" }} viewBox="0 0 20 20" {...props}><path d="M0 0" /></svg>
);
export default SvgComponent;

Why This Happens

In packages/hast-util-to-babel-ast/src/stringToObjectStyle.ts, the formatValue function converts numeric values (and px-suffixed values) to numbers for all properties:

const formatValue = (value: string) => {
  if (isNumeric(value)) return t.numericLiteral(Number(value))
  if (isConvertiblePixelValue(value))
    return t.numericLiteral(Number(trimEnd(value, 'px')))  // ← strips px even for CSS custom properties
  return t.stringLiteral(value)
}

This optimization is safe for standard CSS properties because React auto-appends px to numeric values (e.g., width: 20width: 20px). However, React treats CSS custom property values as raw strings (no unit inference), so the unit is lost.

The fix in #582 (cbdb47f) correctly handled key formatting for --* properties, but its test case used --index: 1 (a unitless number), so this edge case wasn't caught.

Suggested Fix

This makes custom property values always stay as strings. That changes --index: 1 from a number to "1", but that is consistent with how CSS custom properties are interpreted (string values passed through as-is).

Pass the key to formatValue and preserve string values for CSS custom properties:

const formatValue = (value: string, key: string) => {
  // CSS custom properties: always preserve original string value
  if (VAR_REGEX.test(key)) return t.stringLiteral(value)

  if (isNumeric(value)) return t.numericLiteral(Number(value))
  if (isConvertiblePixelValue(value))
    return t.numericLiteral(Number(trimEnd(value, 'px')))
  return t.stringLiteral(value)
}

Update the call site in stringToObjectStyle:

const property = t.objectProperty(formatKey(key), formatValue(value, key))

Suggested Test Cases

// packages/hast-util-to-babel-ast/src/index.test.ts

it('preserves px unit in CSS custom property values', () => {
  expect(stringToObjectStyle('--size: 20px')).toMatchInlineSnapshot(`
    {
      "--size": "20px",
    }
  `)
})

it('preserves other units in CSS custom property values', () => {
  expect(stringToObjectStyle('--spacing: 1.5rem; --width: 100%')).toMatchInlineSnapshot(`
    {
      "--spacing": "1.5rem",
      "--width": "100%",
    }
  `)
})

it('preserves complex CSS custom property values', () => {
  expect(stringToObjectStyle('--gradient: linear-gradient(red, blue)')).toMatchInlineSnapshot(`
    {
      "--gradient": "linear-gradient(red, blue)",
    }
  `)
})

Note: In the PR, I covered the regression at the transform (integration) level rather than adding direct stringToObjectStyle unit tests. This still verifies that custom property values (including units) are preserved in the generated output.

Workaround

Note: This quick workaround only handles integer px values (e.g., 20px). It does not cover decimals, negatives, or other units.

Wrap CSS custom property values in calc() to bypass the /^\\d+px$/ regex:

// svg-css-var-fix-loader.js (use before @svgr/webpack)
module.exports = function(source) {
  return source.replace(
    /style="([^"]*)"/g,
    (match, styleContent) => {
      const fixed = styleContent.replace(
        /(--[\\w-]+:\\s*)(\\d+px)/g,
        '$1calc($2)'
      )
      return `style="${fixed}"`
    }
  )
}

Related

Link to repl or repo (highly encouraged)

Not available. CLI reproduction included above.

Run npx envinfo --system --binaries --npmPackages @svgr/core,@svgr/cli,@svgr/webpack,@svgr/rollup --markdown --clipboard

Paste the results here:

System:
 - OS: Linux 6.12 Amazon Linux 2023
 - CPU: (11) arm64 unknown
 - Memory: 4.15 GB / 7.65 GB
 - Container: Yes
 - Shell: 5.2.15 - /bin/bash
Binaries:
 - Node: 24.13.0 - /home/node/.nvm/versions/node/v24.13.0/bin/node
 - npm: 11.7.0 - /home/node/.nvm/versions/node/v24.13.0/bin/npm
npmPackages:
 - @svgr/core: not installed
 - @svgr/cli: not installed
 - @svgr/webpack: ^8.1.0 => 8.1.0
 - @svgr/rollup: not installed

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions