Black feature image showing “CURSOR + SVG” title, a red arrow pointing to a Cursor error banner that says “Unsupported image type: supported formats are jpeg, png, gif, or webp,” with a geometric Cursor-style icon on the left and a flame icon on the right.

Cursor SVG Unsupported Image Type: Fix for Vue

If you’re hitting the Cursor SVG unsupported image type error, it’s not your SVGs, it’s the chat uploader. Treat SVGs as code inside the workspace and, if needed, batch-convert them into Vue icon components with a tiny Node script. Here’s the workflow.


The error and why it happens

You drop an .svg into Cursor chat and get:

Unsupported image type: supported formats are jpeg, png, gif, or webp.

This happens because Cursor’s chat layer only accepts raster images. SVGs are text, but the chat UI does not treat them that way. Inside the actual project workspace, SVGs are just files and Cursor can read or modify them like any other code.

Key point: do not upload SVGs into chat. Keep them as files in your project folder and reference them from there.


What “workspace” means in Cursor

Your workspace is the local folder you opened in Cursor. It is the same concept as a project in VS Code.

  • Open Cursor → Open Folder → pick your project root, for example ~/Projects/afitpilot-lite
  • The left sidebar is your file tree
  • Anything inside that tree is accessible to the AI tools, Plans, and the editor

Example structure:

afitpilot-lite/
  converted/
    logo.svg
    bolt.svg
  src/
    components/
      icons/
  package.json
  vite.config.js

A reliable workflow for SVG icons in Vue

Option A: use a Cursor plan that reads files from the workspace

This can work if the SVGs are already inside your repo. In Plan mode, tell Cursor to:

  • Read .svg files from /converted
  • Generate Vue SFCs into /src/components/icons
  • Bind size and color props
  • Create an index.ts barrel

If Plans are fussy or your repo is large, use Option B.

Option B: a tiny Node script that always works

Create a script that converts every .svg into a Vue 3 SFC with size and color props. You can still ask Cursor to tweak the outputs later, because they are just Vue files.

1) Add scripts/convert-svgs.js:

// scripts/convert-svgs.js
import { promises as fs } from "fs";
import path from "path";

const SRC_DIR = path.resolve("converted");
const OUT_DIR = path.resolve("src/components/icons");

async function ensureDir(p) {
  await fs.mkdir(p, { recursive: true });
}

function pascalCase(name) {
  return name
    .replace(/\.[^/.]+$/, "")
    .replace(/[-_ ]+([a-zA-Z0-9])/g, (_, c) => c.toUpperCase())
    .replace(/^[a-z]/, c => c.toUpperCase()) + "Icon";
}

function sanitizeSvg(svg) {
  svg = svg
    .replace(/\s(width|height)="[^"]*"/g, "")
    .replace(/\s(xmlns|id|class)="[^"]*"/g, "");

  if (!/viewBox="/i.test(svg)) {
    svg = svg.replace(
      /<svg\b([^>]*)>/i,
      `<svg$1 viewBox="0 0 24 24">`
    );
  }

  svg = svg
    .replace(/fill="(?!none)[^"]*"/gi, `:fill="color"`)
    .replace(/stroke="(?!none)[^"]*"/gi, `:stroke="color"`);

  svg = svg.replace(
    /<svg\b([^>]*)>/i,
    (m, attrs) => {
      const hasFillBinding = /(:fill="color"|fill="none")/i.test(attrs);
      const widthBound = /:width="/.test(attrs);
      const heightBound = /:height="/.test(attrs);
      let updated = attrs;
      if (!hasFillBinding) updated += ` fill="none"`;
      if (!widthBound) updated += ` :width="size"`;
      if (!heightBound) updated += ` :height="size"`;
      return `<svg${updated}>`;
    }
  );

  return svg;
}

function toVueSFC(svg) {
  return `<template>
${svg}
</template>

<script setup>
defineProps({
  size: { type: [Number, String], default: 24 },
  color: { type: String, default: 'currentColor' }
})
</script>
`;
}

async function* walk(dir) {
  for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
    const res = path.resolve(dir, entry.name);
    if (entry.isDirectory()) yield* walk(res);
    else if (entry.isFile() && res.toLowerCase().endsWith(".svg")) yield res;
  }
}

async function main() {
  await ensureDir(OUT_DIR);
  const created = [];

  for await (const file of walk(SRC_DIR)) {
    const base = path.basename(file, ".svg");
    const compName = pascalCase(base);
    const outFile = path.join(OUT_DIR, `${compName}.vue`);

    const raw = await fs.readFile(file, "utf8");
    const inner = sanitizeSvg(raw);
    const sfc = toVueSFC(inner);
    await fs.writeFile(outFile, sfc, "utf8");
    created.push(compName);
  }

  const indexContent = created
    .map(n => `export { default as ${n} } from './${n}.vue';`)
    .join("\n") + "\n";
  await fs.writeFile(path.join(OUT_DIR, "index.ts"), indexContent, "utf8");

  console.log(`Converted ${created.length} icons to ${OUT_DIR}`);
}

main().catch(err => {
  console.error(err);
  process.exit(1);
});

2) package.json scripts:

{
  "type": "module",
  "scripts": {
    "convert:svgs": "node scripts/convert-svgs.js"
  }
}

3) Run it:

mkdir -p src/components/icons
npm run convert:svgs

You now have Vue components like LogoIcon.vue, BoltIcon.vue, plus an index.ts barrel.


Using the generated icons

<script setup>
import { LogoIcon, BoltIcon } from '@/components/icons'
</script>

<template>
  <LogoIcon size="32" />
  <BoltIcon :size="48" color="#0EA5E9" />
</template>

If you prefer file imports:

<script setup>
import LogoIcon from '@/components/icons/LogoIcon.vue'
</script>

<template>
  <LogoIcon size="24" color="currentColor" />
</template>

Optional: Vite SVG loader for <img src>

If you want to import raw .svg as URLs for <img>:

npm i -D vite-svg-loader
// vite.config.js
import svgLoader from 'vite-svg-loader'
export default {
  plugins: [svgLoader()]
}

Then:

<script setup>
import LogoUrl from '@/assets/logo.svg'
</script>

<template>
  <img :src="LogoUrl" alt="Logo">
</template>

Common pitfalls

  • Dragging SVGs into chat triggers the error. Keep them in the file tree.
  • Wrong folder. Ensure converted/ is inside the workspace root you opened.
  • Missing type: module in package.json if you use import in Node.
  • Hardcoded fills. The sanitizer replaces fills and strokes with color binding. If you need multicolor icons, adapt the sanitizer to skip some paths.

A ready to paste Cursor plan block

If you still want to drive this through Cursor Plans after the files are in your repo, paste:

Read all .svg files from /converted in the current workspace. 
For each file:
- Create a Vue 3 SFC in /src/components/icons named <BaseName>Icon.vue.
- Wrap the SVG inside <template>.
- Add <script setup> with props: size [Number|String] default 24, color String default 'currentColor'.
- Bind :width="size" and :height="size" on the root <svg>.
- Replace static fill and stroke values with :fill="color" or :stroke="color" unless the value is "none".
- Remove width, height, xmlns, id, class attributes from the root <svg>. Keep viewBox.
- Generate src/components/icons/index.ts exporting all icons.

FAQ

Why not keep SVGs as inline <svg> in components manually?
You can. The script just saves time when you have dozens or hundreds to convert.

What about React projects?
Swap the output template to JSX and props. The same approach works.

Can I keep multicolor icons?
Yes. Modify the sanitizer to skip fills on specific paths or only replace root-level attributes.

Do I need the Vite loader?
Only if you want URL imports for <img>. For interactive icons with props, SFCs are better.


Conclusion

The error is a UI limitation, not a blocker. Treat SVGs as code inside the workspace, not images in chat. If Plans are temperamental, the Node script is a clean, repeatable solution. Once converted, your icons are first-class Vue components with size and color control that drop into any app.

If you want, I can adapt the script for multicolor icons, auto-generate Storybook stories, or produce React variants.

Find my other blog posts:

Resources:

For deep thinkers, creators, and curious minds. One post. Zero noise.

We don’t spam! Read our privacy policy for more info.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *