You drop a crisp, hand-optimized SVG into the WordPress media library. Instead of a sharp icon or a responsive illustration, you get a broken image thumbnail, a security error, or a blank space where your graphic should be. The frustration is real, especially when you know the file is clean. The problem isn’t your SVG—it’s how WordPress handles it by default. I’m Jonas Venn, and I build systems that treat the front end as a precision instrument. This article walks through the exact reasons SVG uploads fail and gives you direct, code-level fixes that don’t rely on plugins.
The Core Problem: WordPress Treats SVGs as a Security Risk
WordPress doesn’t block SVGs out of spite. Scalable Vector Graphics are XML-based, which means they can contain inline scripts, event handlers, and external references. A malicious SVG can execute JavaScript, load remote resources, or embed hidden tracking pixels. The WordPress core team decided that the safest default is to reject the file type entirely. When you try to upload an SVG, the wp_check_filetype_and_ext function runs a MIME type check. If the file doesn’t pass the allowed list, you get the classic “Sorry, this file type is not permitted for security reasons” message.
This isn’t a bug. It’s a deliberate design choice. But for developers and site builders who need crisp, resolution-independent graphics, it’s a roadblock. The typical workaround is a plugin like Safe SVG or SVG Support. Those work, but they add another dependency, another update surface, and often more code than you actually need. If you’re comfortable editing your theme’s functions.php or building a small custom plugin, you can solve this with a few precise lines of code.

Step 1: Allow the SVG MIME Type
The first barrier is WordPress’s MIME type whitelist. You need to tell WordPress that image/svg+xml is an acceptable file type. This is done by hooking into the upload_mimes filter. Add this to your theme’s functions.php or, better, a site-specific plugin:
add_filter( 'upload_mimes', 'jooom_add_svg_mime' );
function jooom_add_svg_mime( $mimes ) {
$mimes['svg'] = 'image/svg+xml';
return $mimes;
}
This snippet registers the .svg extension with the correct MIME type. After adding this, you can upload an SVG, and WordPress will accept the file. But you’ll immediately hit the next wall: the media library shows a generic placeholder instead of a preview, and the file might still not display correctly in the block editor. That’s because WordPress also needs to handle the file’s display and sanitization.
Step 2: Fix the Media Library Preview
WordPress generates thumbnail previews for raster images using the GD or Imagick library. SVGs are vector files, so these libraries can’t process them. The result is a broken image icon in the grid view. To fix this, you need to tell WordPress to use the SVG itself as the thumbnail. Hook into wp_prepare_attachment_for_js and wp_get_attachment_image_src to override the default behavior.
add_filter( 'wp_prepare_attachment_for_js', 'jooom_fix_svg_media_library', 10, 3 );
function jooom_fix_svg_media_library( $response, $attachment, $meta ) {
if ( $response['mime'] === 'image/svg+xml' ) {
$response['sizes'] = array(
'full' => array(
'url' => $response['url'],
'width' => $response['width'],
'height' => $response['height'],
'orientation' => $response['orientation'],
),
);
$response['icon'] = $response['url'];
}
return $response;
}
add_filter( 'wp_get_attachment_image_src', 'jooom_fix_svg_thumbnail', 10, 4 );
function jooom_fix_svg_thumbnail( $image, $attachment_id, $size, $icon ) {
if ( get_post_mime_type( $attachment_id ) === 'image/svg+xml' ) {
$image[0] = wp_get_attachment_url( $attachment_id );
$image[1] = $image[2] = null;
}
return $image;
}
These two functions ensure that the media library shows the actual SVG as the preview and that any calls to wp_get_attachment_image_src return the original file. The width and height are set to null because SVGs are inherently responsive; forcing dimensions can distort them. This approach keeps your SVGs fluid in the admin and on the front end.

Step 3: Sanitize SVGs on Upload to Strip Malicious Code
Allowing SVGs without sanitization is like leaving your front door unlocked. Even if you trust the source, a designer might unknowingly upload an SVG exported from a compromised tool. The fix is to strip out scripts, event handlers, and other risky elements at the point of upload. PHP’s DOMDocument class makes this straightforward. We’ll hook into wp_handle_upload_prefilter to clean the file before WordPress saves it.
add_filter( 'wp_handle_upload_prefilter', 'jooom_sanitize_svg' );
function jooom_sanitize_svg( $file ) {
if ( $file['type'] !== 'image/svg+xml' ) {
return $file;
}
$dom = new DOMDocument();
$dom->loadXML( file_get_contents( $file['tmp_name'] ), LIBXML_NOENT | LIBXML_DTDLOAD );
$allowed_tags = array( 'svg', 'g', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'ellipse', 'text', 'tspan', 'defs', 'use', 'clipPath', 'mask', 'linearGradient', 'radialGradient', 'stop', 'pattern', 'image', 'title', 'desc' );
$allowed_attributes = array( 'id', 'class', 'xmlns', 'viewBox', 'width', 'height', 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'r', 'rx', 'ry', 'd', 'points', 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', 'stroke-dasharray', 'opacity', 'transform', 'href', 'xlink:href', 'clip-path', 'mask', 'style' );
$elements = $dom->getElementsByTagName( '*' );
for ( $i = $elements->length - 1; $i >= 0; $i-- ) {
$element = $elements->item( $i );
if ( ! in_array( $element->tagName, $allowed_tags ) ) {
$element->parentNode->removeChild( $element );
continue;
}
for ( $j = $element->attributes->length - 1; $j >= 0; $j-- ) {
$attribute = $element->attributes->item( $j );
if ( ! in_array( $attribute->name, $allowed_attributes ) ) {
$element->removeAttribute( $attribute->name );
}
}
}
file_put_contents( $file['tmp_name'], $dom->saveXML() );
return $file;
}
This code defines a strict whitelist of allowed SVG elements and attributes. Anything not on the list gets stripped. The LIBXML_NOENT | LIBXML_DTDLOAD flags prevent XML External Entity (XXE) attacks during parsing. After cleaning, the sanitized XML is written back to the temporary file. This step is non-negotiable if you’re allowing SVGs on a site where multiple users have upload access.
Step 4: Ensure SVGs Display Correctly in the Block Editor
Even with the MIME type allowed and the media library fixed, the block editor can still choke on SVGs. The Image block expects a raster image with defined dimensions. When you insert an SVG, the block might set width and height to zero or fail to render the preview. The solution is to add a small CSS snippet that forces the editor to respect the SVG’s intrinsic aspect ratio and to output the raw SVG markup when appropriate.
Add this to your theme’s admin styles via admin_enqueue_scripts:
add_action( 'admin_enqueue_scripts', 'jooom_svg_admin_styles' );
function jooom_svg_admin_styles() {
echo '<style>
.components-responsive-wrapper__content[src$=".svg"] {
width: 100% !important;
height: auto !important;
}
.wp-block-image img[src$=".svg"] {
width: 100%;
height: auto;
}
</style>';
}
This targets SVG images inside the editor’s responsive wrapper and the image block, forcing them to scale properly. For more control, you can also filter the image block’s output on the front end to inline the SVG code, which allows CSS styling of individual SVG elements. That’s a more advanced technique, but the CSS fix above solves the immediate display issue.

Step 5: Handle SVG Dimensions and Responsiveness
SVGs don’t have a fixed pixel size like PNGs. They can scale infinitely, but WordPress and many themes try to assign explicit width and height attributes. This can lead to SVGs being displayed at 0x0 or stretched disproportionately. The earlier thumbnail fix sets dimensions to null, which helps, but you should also ensure that the img tag itself doesn’t get hardcoded dimensions from the theme or editor.
Add this filter to remove width and height attributes from SVG img tags on the front end:
add_filter( 'wp_get_attachment_image_attributes', 'jooom_remove_svg_dimensions', 10, 3 );
function jooom_remove_svg_dimensions( $attr, $attachment, $size ) {
if ( get_post_mime_type( $attachment->ID ) === 'image/svg+xml' ) {
unset( $attr['width'] );
unset( $attr['height'] );
$attr['class'] = isset( $attr['class'] ) ? $attr['class'] . ' svg-image' : 'svg-image';
}
return $attr;
}
This strips the width and height attributes and adds a svg-image class for targeted CSS. In your theme’s stylesheet, you can then set .svg-image { width: 100%; height: auto; } to make SVGs fill their container while maintaining aspect ratio. This is especially useful for logos and icons that need to adapt to different screen sizes.
Why a Code Solution Beats a Plugin for System Builders
Plugins are convenient, but they introduce variables you can’t always control. A plugin might be abandoned, conflict with a custom theme, or load assets on every page when you only need SVGs in a specific post type. By writing your own SVG support, you keep the logic transparent and minimal. You know exactly what’s allowed and what’s stripped. This matters when you’re building a site that will be handed off to a client or maintained by a team—you can document the exact behavior instead of pointing to a third-party plugin’s changelog.
There’s also a performance angle. Many SVG plugins add a settings page, enqueue scripts, or run sanitization on every page load. The code above only fires during upload and when preparing attachment data. It’s lean and event-driven. If you’re already maintaining a custom theme or a mu-plugin for client sites, this fits right in. You can even wrap the entire solution in a single class and conditionally load it only for users with the upload_files capability.
When SVGs Still Break: Debugging Common Edge Cases
Sometimes the file uploads but renders as a blank square. Here are the most common culprits and how to diagnose them:
1. The SVG Uses Inline Styles That Conflict with Theme CSS
If your SVG contains <style> blocks with generic class names like .st0, they can be overridden by your theme’s stylesheet. Open the SVG in a text editor and look for <style> tags. Either remove them and use presentation attributes instead, or namespace the classes. The sanitization function above doesn’t strip <style> tags because they’re valid SVG, but you can add 'style' to the disallowed tags list if you prefer to handle styling externally.
2. The SVG Contains External References
Elements like <image href="..."> or <use href="..."> can point to external resources. If those resources are blocked by a Content Security Policy or are unavailable, the SVG will break. Inspect the SVG code for any xlink:href or href attributes pointing to external URLs. Replace them with embedded data URIs or remove them if they’re not essential.
3. Server MIME Type Configuration
Even if WordPress accepts the file, your web server might not serve it with the correct Content-Type: image/svg+xml header. This is rare on modern hosts but can happen. Check the network tab in your browser’s developer tools when loading the SVG directly. If the MIME type is wrong, add AddType image/svg+xml .svg to your .htaccess file or contact your host.
FAQ
Is it safe to allow SVG uploads on a client site?
Yes, if you implement sanitization. The code in Step 3 strips scripts and event handlers, which are the primary attack vectors. For high-security sites, you can also restrict SVG uploads to administrators only by wrapping the MIME type filter in a capability check: if ( current_user_can( 'manage_options' ) ). This limits the risk to trusted users.
Why does my SVG look blurry in the media library even after the fix?
The media library grid view uses a fixed thumbnail size. If your SVG has a complex design with thin lines, it might appear aliased when scaled down. This is a rendering issue, not a code problem. The SVG itself is still sharp at its native size. You can improve the preview by ensuring your SVG has a clear viewBox and uses stroke widths that hold up at small sizes.
Can I use this approach on a multisite network?
Yes, but with caution. On a multisite, the upload_mimes filter applies network-wide unless you conditionally load it per site. You can place the code in a mu-plugin and use get_current_blog_id() to enable SVG support only for specific sites. The sanitization function should always run regardless, to protect the entire network.
What if I need to allow more SVG elements for complex illustrations?
The allowed tags and attributes arrays in the sanitization function are a starting point. You can add elements like <filter>, <feGaussianBlur>, or <animate> if your use case requires them. Be aware that <animate> and <script> are high-risk. Only whitelist what you absolutely need, and test thoroughly.
Building a Reliable SVG Workflow
Once SVGs are working, treat them like any other asset in your build pipeline. Optimize them with tools like SVGO before uploading. Keep a master folder of source SVGs outside WordPress so you can re-upload if needed. If you’re using a version control system for your theme, consider storing critical SVGs directly in the theme and referencing them via get_template_directory_uri() instead of the media library. This bypasses the upload process entirely and gives you full control over the files.
For sites that rely heavily on SVGs—icon systems, data visualizations, interactive maps—this code-based approach scales better than a plugin. You can extend the sanitization to log rejected elements, add a custom capability for SVG uploads, or integrate with a CDN that serves SVGs with immutable caching headers. The foundation is solid, and the code is yours to adapt.
If you’re dealing with other WordPress quirks, like a fresh install that returns nothing but a blank page, the same systematic mindset applies. Check your permalinks, your .htaccess, and your reading settings before reaching for a plugin. I’ve written about that exact scenario in What to Fix First When a New WordPress Site Says Nothing Found. The principle is the same: understand the system, then write the smallest possible fix.
SVGs are a core part of modern web design. WordPress’s default stance is conservative, but with a few well-placed hooks, you can unlock their full potential without compromising security or performance. The code above has been running on client sites for years without issues. Copy it, adapt it, and get back to building.








