Skip to content

More modern approach #109

@artursopelnik

Description

@artursopelnik

I created a small node script that supports CSS variables, and I wanted to share it with you. With this script, you won’t need any third-party plugins and can use it directly in your project. Currently, it only works with the fill property, but in most cases, stroke is not needed.

--color-text: red;
--icon-arrow-down: svg-load("icons/arrow-down.svg", "var(--color-text)"); // use css variables here or directly the color for fill

select {
  background-position: center right 2rem;
  background-size: 2rem auto;
  background-repeat: no-repeat;
  background-image: var(--icon-arrow-down);
}

vite.config.ts

import postcssSvgLoadPlugin from "./postcss.svg-load-plugin"

...
  plugins: [stylelint()],
  css: {
    postcss: {
      plugins: [postcssSvgLoadPlugin()]
    }
  },
...

stylelint.config.mjs

...
rules: {
    "declaration-property-value-no-unknown": [
      true,
      {
        "ignoreProperties": {
          "background-image": ["/^svg-load/"]
        }
      }
    ]
}
...

postcss.svg-load-plugin.js

import fs from "fs/promises"
import path from "path"

const svgCache = {}

export default function postcssSvgLoadPlugin(options = {}) {
  return {
    postcssPlugin: "postcss-svg-load",
    async Once(root, { result }) {
      const cssFilePath = result.opts.from
      const baseDir = options.baseDir ?? (cssFilePath ? path.dirname(cssFilePath) : process.cwd())
      const cssVariables = {}

      root.walkRules((rule) => {
        if (rule.selector === ":root" || rule.selector.startsWith(":root,")) {
          rule.walkDecls((decl) => {
            if (decl.prop.startsWith("--")) {
              cssVariables[decl.prop] = decl.value.trim()
            }
          })
        }
      })

      const svgLoadRegex = /svg-load\(\s*["']([^"']+)["'](?:\s*,\s*["']([^"']*)["'])?\s*\)/g
      const processingPromises = []

      root.walkDecls((decl) => {
        let match
        const matches = []

        while ((match = svgLoadRegex.exec(decl.value)) !== null) {
          matches.push(match)
        }

        for (const match of matches) {
          const [fullMatch, svgRelativePath, colorParam] = match

          let color = null

          if (colorParam !== undefined && colorParam !== "") {
            const varMatch = colorParam.match(/^var\((--[^)]+)\)$/)
            if (varMatch) {
              const varName = varMatch[1]
              if (cssVariables[varName]) {
                color = cssVariables[varName]
              } else {
                result.warn(`CSS variable ${varName} not found`, { node: decl })
                continue
              }
            } else {
              color = colorParam
            }
          }

          const svgAbsolutePath = path.resolve(baseDir, svgRelativePath)
          const cacheKey = `${svgAbsolutePath}::${color ?? ""}`

          const promise = (async () => {
            if (svgCache[cacheKey]) {
              decl.value = decl.value.replace(fullMatch, svgCache[cacheKey])
              return
            }

            try {
              let svgContent = await fs.readFile(svgAbsolutePath, "utf8")

              if (color !== null) {
                if (/<svg[^>]*fill="[^"]*"[^>]*>/.test(svgContent)) {
                  svgContent = svgContent.replace(/(<svg[^>]*?)fill="[^"]*"([^>]*>)/, `$1fill="${color}"$2`)
                } else {
                  svgContent = svgContent.replace(/(<svg[^>]*)>/, `$1 fill="${color}">`)
                }
              }

              const encodedSvg = encodeURIComponent(svgContent).replace(/'/g, "%27").replace(/"/g, "%22")

              const dataUrl = `url("data:image/svg+xml,${encodedSvg}")`

              svgCache[cacheKey] = dataUrl

              decl.value = decl.value.replace(fullMatch, dataUrl)
            } catch {
              result.warn(`SVG file not found: ${svgAbsolutePath}`, { node: decl })
            }
          })()

          processingPromises.push(promise)
        }
      })

      await Promise.all(processingPromises)
    }
  }
}

postcssSvgLoadPlugin.postcss = true

#72 #94

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