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
andcolor
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
inpackage.json
if you useimport
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:
- Nuxt HMR CSS Fix – How I Solved Hot Reload & Styling Issues with a Single-Port Setup
- Cursor Conversation Too Long — The Simple Prompt That Solves It
- Cursor Claude 3.7 Sonnet One-File Focus Issue: Why It Happens and How to Fix It
Leave a Reply