Your SVG files are valid. Your toolbar icons look fine. But some dropdown menus are suddenly missing their icons. Here's why that happens—and how to catch it before release.
Migrating from CSS sprites to SVG icons sounds straightforward.
You replace the old assets, update the components, check that the new icons render, and move on.
Everything looks fine until someone opens a dropdown menu.
The toolbar icons are there. The menu labels are visible. The actions still work. But the icons inside the menu have disappeared.
The strange part? The missing icons may already exist in your SVG sprite.
This isn't necessarily an asset problem. It can be a migration problem: one part of your application uses the new rendering system while another still expects the old one.
Let's look at a real-world example, reproduce the failure in React, and build a practical checklist for avoiding it.
1. A real migration bug: the icons existed, but the menus were blank
In August 2026, a contributor reported an issue in Euro-Office DocumentServer:
Dropdown menu items still render icons the old way — every menu icon is blank.
The application had been migrating from CSS/PNG sprites to SVG symbols.
Its toolbar buttons had already moved to the new SVG rendering approach, but dropdown menu items still generated the old CSS sprite markup.
The PNG sprite assets that those menus depended on were no longer being generated or shipped.
The result was predictable once the cause was identified: buttons displayed their icons, while dropdown menus showed empty spaces.
According to the issue report, 1,092 menu items declared icons across five editors. The report also found that the SVG symbols needed by the document editor's menus were largely already available.
The issue was subsequently closed.
What makes this case interesting isn't the missing artwork. It's the difference between migrating icon files and migrating the components that render them.
A migration can be complete at the asset level and still be incomplete at the interface level.
2. Reproducing the problem in a React application
To make the issue easier to understand, we built a small, independent React and TypeScript demonstration.
It contains a document-style interface with toolbar buttons and a dropdown menu.
The same actions—Copy, Paste, Save, and Download—appear in both places.
In the incomplete migration, the toolbar uses SVG symbols correctly. The dropdown still uses legacy CSS sprite references.
The result looks like this:
Before and after: the toolbar works in both versions, but the dropdown icons only appear after correcting the rendering code.
Notice that the menu items don't disappear. Their text and actions remain available.
Only the icons are missing.
This is why the problem can escape ordinary functional testing. A test that checks whether a menu opens or an action runs may still pass.
3. Why CSS sprites and SVG symbols aren't interchangeable
Consider how an application might have rendered icons before the migration.
The old CSS sprite approach
<span class="legacy-icon legacy-copy"></span>
.legacy-icon {
display: inline-block;
width: 20px;
height: 20px;
background-image: url("/icons.png");
background-repeat: no-repeat;
}
.legacy-copy {
background-position: -40px 0;
}
The icon depends on an external PNG image and CSS rules that select the correct position within that image.
If the PNG sprite or its CSS rules are removed, the element can still exist in the DOM.
But nothing draws the icon.
That's an important distinction: the markup hasn't disappeared, and the browser doesn't necessarily report a JavaScript error.
The new SVG symbol approach
An SVG sprite can define reusable symbols:
<svg
xmlns="http://www.w3.org/2000/svg"
style="position:absolute;width:0;height:0;overflow:hidden"
aria-hidden="true"
>
<symbol id="icon-copy" viewBox="0 0 24 24">
<rect
x="8" y="8" width="12" height="12"
rx="1"
fill="none"
stroke="currentColor"
stroke-width="2"
/>
<path
d="M16 8V4H4v12h4"
fill="none"
stroke="currentColor"
stroke-width="2"
/>
</symbol>
</svg>
A component can reference that symbol with <use>:
<svg class="icon" aria-hidden="true">
<use href="#icon-copy"></use>
</svg>
.icon {
width: 20px;
height: 20px;
color: inherit;
}
Here, the symbol is defined within the same document. An external SVG sprite can also be used, but the URL and symbol IDs must resolve correctly.
The difference is fundamental.
The CSS sprite approach relies on background images and positioning rules. The SVG symbol approach relies on valid symbol references and SVG rendering.
Removing the old sprite files doesn't automatically update the components that reference them.
4. What an incomplete migration looks like
Here's the problem in our demonstration.
The toolbar has been migrated:
function ToolbarButton() {
return (
<button type="button">
<svg
width="20"
height="20"
aria-hidden="true"
>
<use href="#icon-copy" />
</svg>
Copy
</button>
);
}
But the dropdown still renders a legacy element:
function MenuAction() {
return (
<button type="button">
<span className="legacy-icon legacy-copy" />
Copy
</button>
);
}
The toolbar doesn't need the old PNG sprites anymore.
The menu still does.
Once those assets and styles are removed, the menu icon becomes invisible.
In the screenshot, the toolbar icons are visible, but the dropdown entries have no icons.
There's no need to invent a missing asset or artificially hide anything to reproduce the failure.
The old markup simply no longer has a working rendering mechanism.
Fixing the menu
The immediate correction is to update the menu to reference the SVG symbol:
function MenuAction() {
return (
<button type="button">
<svg
width="20"
height="20"
aria-hidden="true"
>
<use href="#icon-copy" />
</svg>
Copy
</button>
);
}
The icon now renders through the same mechanism as the toolbar.
But manually fixing individual menus doesn't address the larger problem: how do you stop different components from implementing icon rendering differently?
5. Use one icon component across your interface
A shared icon component is one practical way to reduce inconsistencies.
In React, a simple implementation might look like this:
type IconName =
| "copy"
| "paste"
| "save"
| "download";
type IconProps = {
name: IconName;
size?: number;
};
function Icon({ name, size = 20 }: IconProps) {
return (
<svg
width={size}
height={size}
aria-hidden="true"
focusable="false"
>
<use href={`#icon-${name}`} />
</svg>
);
}
Both the toolbar and the menu can now render the same symbol through the same component:
<button type="button">
<Icon name="copy" />
Copy
</button>
This doesn't eliminate every possible icon bug.
An incorrect symbol ID, a missing sprite, or incompatible styling can still cause problems.
But it reduces the number of places where icon rendering behaviour must be maintained.
For icon-only controls, remember that the button itself still needs an accessible name—for example, through aria-label. The decorative SVG shouldn't be the only source of that name.
6. Finding legacy icon references before they reach production
After updating your main components, search the codebase for references to the old rendering system.
Don't limit the search to image files.
Old rendering logic may still exist in templates, dropdown components, CSS classes, or conditional code paths.
For example, with ripgrep:
rg -n 'background-image|background-position' src
rg -n 'legacy-icon|iconCls|menu-item-icon' src
rg -n '\.png' src
These searches are intentionally broad.
A match doesn't automatically indicate a migration bug. CSS backgrounds and PNG files may still be legitimate elsewhere in the application.
The goal is to identify code that still renders icons using a mechanism scheduled for removal.
A useful component inventory can help:
| Component | SVG migration | Visual verification |
|---|---|---|
| Toolbar buttons | Complete | Passed |
| Dropdown menus | Complete | Passed |
| Context menus | Needs review | Pending |
| Dialog buttons | Complete | Passed |
| Selected states | Needs review | Pending |
| Disabled states | Needs review | Pending |
The important thing is to inspect each type of component, not just the most visible one.
Dropdowns and context menus are particularly easy to overlook because they aren't always visible during normal page loading.
7. Test what users actually see
A successful build doesn't guarantee that icons are visible.
Neither does checking that an SVG element exists in the DOM.
For example, this markup is syntactically valid:
<svg>
<use href="#icon-does-not-exist"></use>
</svg>
But if the referenced symbol doesn't exist, it won't display the intended icon.
Likewise, a valid SVG reference can still produce an invisible icon because of color, sizing, clipping, or styling.
That's why icon migrations benefit from multiple kinds of verification.
Check the SVG references
Confirm that every symbol ID used by your components actually exists.
Be especially careful when icons are assembled dynamically from names or application state.
Check the rendered components
Inspect the real interface, including menus that only appear after interaction.
Verify that the expected SVG geometry is rendered, rather than checking only for the presence of an <svg> tag.
Compare screenshots
Capture representative interface states before and after the migration.
For example, with Playwright:
import { test, expect } from "@playwright/test";
test("shows the migrated menu", async ({ page }) => {
await page.goto("/");
await page
.getByRole("button", { name: "Actions" })
.click();
const menu = page.getByRole("menu");
await expect(menu).toBeVisible();
await expect(menu).toHaveScreenshot(
"actions-menu.png"
);
});
This example assumes the application's Actions control opens a properly identified menu and that a reviewed baseline screenshot has already been created.
Screenshot tests are particularly useful because they can reveal a missing icon even when the associated button or menu action continues to work.
Our demonstration was checked using Chromium and Playwright, including tests that inspect rendered icon geometry and screenshots.
That doesn't replace cross-browser testing in a production application, but it provides a useful starting point.
8. Don't forget themes and component states
Migrating to SVG symbols also changes how icon styling is managed.
For monochrome icons, currentColor can simplify theme support by allowing the artwork to inherit the surrounding text color.
For example:
.action {
color: #27364b;
}
.action svg {
width: 20px;
height: 20px;
}
If the referenced artwork uses stroke="currentColor" or fill="currentColor" appropriately, the icon can follow the component's color.
Dark mode can then adjust that color through the surrounding component or theme.
However, this only works when the SVG artwork and its styling are compatible with the chosen approach.
During verification, check more than the default light theme.
Include dark mode, hover, disabled, active, and selected states where relevant.
Also test smaller icon sizes. An icon that looks correct at 32 pixels may be difficult to distinguish at 16 pixels.
9. Prevent the old rendering system from coming back
Once the migration is complete, consider adding a lightweight automated check for obsolete markup.
For example:
import fs from "node:fs";
const files = [
"src/components/Toolbar.tsx",
"src/components/Dropdown.tsx",
];
let failed = false;
for (const file of files) {
const source = fs.readFileSync(file, "utf8");
if (source.includes("legacy-icon")) {
console.error(
`Legacy icon reference found in ${file}`
);
failed = true;
}
}
if (failed) {
process.exitCode = 1;
}
This intentionally simple example checks only the specified files.
A production implementation should cover all relevant source directories and account for the project's template and component patterns.
The purpose isn't to reject every use of CSS backgrounds.
It's to prevent a retired icon-rendering mechanism from being reintroduced accidentally.
Combine that source check with browser tests for components that use icons.
The two approaches catch different classes of problems.
10. A practical checklist for SVG icon migrations
Before marking the migration complete, verify the following:
- [ ] Every icon-rendering component has been identified.
- [ ] Obsolete CSS sprite references have been reviewed.
- [ ] All required SVG symbol IDs exist.
- [ ] Toolbar, dropdown, and context menu icons render correctly.
- [ ] Light and dark themes have been checked.
- [ ] Disabled, selected, and hover states remain readable.
- [ ] Representative screenshots have been compared.
- [ ] Automated checks can detect deprecated icon markup.
- [ ] The migration has been tested beyond the main toolbar.
When choosing or replacing icons during this process, consistency matters too.
Mixing unrelated icon families can introduce differences in stroke weight, proportions, and visual alignment—even when the migration itself works technically.
SVGicons.com can help developers explore and compare SVG icons when preparing a consistent set for an application.
Asset selection and rendering verification are separate responsibilities, and both deserve attention.
Final thoughts
A missing icon doesn't always mean a missing file.
Sometimes the artwork is available, the SVG sprite is valid, and most of the interface works perfectly.
The problem is that one component still expects an icon-rendering system that no longer exists.
That's why an icon migration shouldn't be considered complete until the actual interface has been verified.
Don't just check whether your SVG files migrated. Check whether every component migrated with them.
References
- Euro-Office DocumentServer — Issue #313 — real-world report of missing dropdown icons after an incomplete SVG sprite migration.
- SVGicons.com — SVG icon discovery and selection.
The React demonstration and screenshots described in this article are an independent educational reproduction, not screenshots from the Euro-Office application.



Top comments (0)