Skip to Content

WordPress, safely outputting SVGs without fighting PHPCS

Created on
No updates
Approx ~9 minutes reading time for 1,760 words.

Introduction

If you’ve ever tried storing SVG‘s in Advanced Custom Fields (ACF) or any other custom fields, then outputting them in a theme, you’ll know the pain. WordPress’s escaping functions strip the markup, PHPCS displays warnings and your beautiful SVG becomes sad, inert text.

This post walks through how to safely output SVG markup in WordPress without suppressing PHPCS, without security risks and without giving up and Base64 encoding everything.

The problem: WordPress escapes too well

WordPress has a solid set of escaping functions, great for normal HTML, not so great for inline SVG.

Typical escapes like;

esc_html( $svg );
esc_attr( $svg );
wp_kses_post( $svg );

…strip tags, flatten markup, or just refuse to render the SVG at all. And if you attempt to placate PHPCS with;

// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $svg;

…it works, but you’ve just silenced security tooling and introduced inconsistency in your codebase.

There’s a better way.

The fix: A custom barrd_kses_svg() escaping function

SVG‘s can be sanitised properly, we just need to teach WordPress which tags and attributes are safe. The answer is a small helper function that uses wp_kses() under the hood with a custom list of allowed SVG elements. That way;

  • PHPCS is happy.
  • Security stays intact.
  • SVG‘s render correctly.
  • Templates stay tidy.

The function

I keep this in inc/svg-escaping.php

<?php
/**
 * SVG Escaping Helper Function
 *
 * @package barrd
 */

if ( ! function_exists( 'barrd_kses_svg' ) ) {
  /**
   * Sanitise SVG output for safe display.
   *
   * This function allows SVG markup whilst stripping potentially harmful content.
   * Use this instead of phpcs:ignore comments when outputting SVG from ACF fields.
   *
   * @param string $svg The SVG markup to sanitise.
   * @return string Sanitised SVG markup.
   */
  function barrd_kses_svg( $svg ) {
    $kses_defaults = wp_kses_allowed_html( 'post' );

    $svg_args = [
      'svg'      => [
        'class'           => true,
        'aria-hidden'     => true,
        'aria-labelledby' => true,
        'role'            => true,
        'xmlns'           => true,
        'width'           => true,
        'height'          => true,
        'viewbox'         => true,
        'fill'            => true,
        'stroke'          => true,
        'stroke-width'    => true,
        'stroke-linecap'  => true,
        'stroke-linejoin' => true,
      ],
      'g'       => [
        'fill'            => true,
        'stroke'          => true,
        'stroke-width'    => true,
        'stroke-linecap'  => true,
        'stroke-linejoin' => true,
        'transform'       => true,
      ],
      'title'    => [ 'id' => true ],
      'desc'     => [ 'id' => true ],
      'path'    => [
        'd'               => true,
        'fill'            => true,
        'stroke'          => true,
        'stroke-width'    => true,
        'stroke-linecap'  => true,
        'stroke-linejoin' => true,
        'transform'       => true,
      ],
      'rect'    => [
        'x'            => true,
        'y'            => true,
        'width'        => true,
        'height'       => true,
        'fill'         => true,
        'stroke'       => true,
        'stroke-width' => true,
        'rx'           => true,
        'ry'           => true,
      ],
      'circle'  => [
        'cx'           => true,
        'cy'           => true,
        'r'            => true,
        'fill'         => true,
        'stroke'       => true,
        'stroke-width' => true,
      ],
      'ellipse' => [
        'cx'           => true,
        'cy'           => true,
        'rx'           => true,
        'ry'           => true,
        'fill'         => true,
        'stroke'       => true,
        'stroke-width' => true,
      ],
      'line'    => [
        'x1'           => true,
        'x2'           => true,
        'y1'           => true,
        'y2'           => true,
        'stroke'       => true,
        'stroke-width' => true,
      ],
      'polyline' => [
        'points'       => true,
        'fill'         => true,
        'stroke'       => true,
        'stroke-width' => true,
      ],
      'polygon'  => [
        'points'       => true,
        'fill'         => true,
        'stroke'       => true,
        'stroke-width' => true,
      ],
      'text'     => [
        'x'           => true,
        'y'           => true,
        'fill'        => true,
        'font-size'   => true,
        'font-family' => true,
        'text-anchor' => true,
      ],
      'defs'     => [],
      'style'    => [ 'type' => true ],
      'use'      => [
        'xlink:href' => true,
        'href'       => true,
      ],
    ];

    $allowed_tags = array_merge( $kses_defaults, $svg_args );

    return wp_kses( $svg, $allowed_tags );
  }
}

Once this exists, you can safely output SVG‘s without silencing PHPCS.

echo barrd_kses_svg( get_field( 'svg' ) );

Telling PHPCS about your function

PHPCS doesn’t automatically know your shiny new function is an escaping function, but it’s easy to update it.

Add this to .phpcs.xml (or equivalent file.)

<rule ref="WordPress.Security.EscapeOutput">
  <properties>
    <property name="customEscapingFunctions" type="array">
      <element value="barrd_kses_svg"/>
    </property>
  </properties>
</rule>

That’s it. No more // phpcs:ignore all over your templates.

Real world usage: Social Icons

Here’s a trimmed example of what this looks like in a template;

<a href="<?php echo esc_url( $data['github_url'] ); ?>" target="_blank" rel="noopener">
  <?php echo barrd_kses_svg( $data['github_svg'] ); ?>
  <span>GitHub</span>
</a>

Nice and clean. Escaped where necessary. SVGs properly sanitised. PHPCS with nary a red squiggly line. Perfect.

Bonus: Inline SVG for UI components

You can also pass literal strings;

<button>
  <?php echo barrd_kses_svg( '<svg width="24" height="24"><path d="..." /></svg>' ); ?>
  Save Changes
</button>

Great for simple icons you don’t want to store in a field.

Common “gotchas”

• SVG doesn’t render
You probably forgot to actually echo the return value.

• Some attributes are missing
Just extend the $svg_args array. SVG is big and flexible, so you can allow what you need. More details below.

• PHPCS still complains
Check;

  • The function name matches exactly.
  • PHPCS cache is cleared.
  • You’re including the WordPress standard (WPCS.)
  • No other ruleset is overriding your config.

Extending the function

Need gradients? Filters? Whatever weirdness your designer came up with? Just add them;

...
'linearGradient' => [
  'id' => true,
  'x1' => true,
  'x2' => true,
  'y1' => true,
  'y2' => true,
],
'stop' => [
  'offset'       => true,
  'stop-color'   => true,
  'stop-opacity' => true,
],
...
You can even create specialised variants for different contexts if you’re feeling fancy. 🙃

Final thoughts

This little helper function has made my templates cleaner, my PHPCS runs quieter and my SVG‘s… actually display. It keeps security intact, follows WordPress best practices, and scales nicely across a theme or plugin.

If you work with ACF or inline icons, I genuinely recommend dropping this into your toolbox*, it’s one of those tiny things that pays off every time you reach for a SVG.

*Remember to change barrd to whatever you’d like to use in terms of function naming and definitely for @package.

// End of Article

Article Information

Dave A.K.A. 'barrd'

Dave is a Bristol based Scottish Expat who has 20+ years experience of web development. Loves playing guitar, reading books, watching Sci-Fi and tinkering with tech.

About Dave A.K.A. 'barrd'

Image for Dave A.K.A. 'barrd'
Dave is a Bristol based Scottish Expat who has 20+ years experience of web development. Loves playing guitar, reading books, watching Sci-Fi and tinkering with tech.

Read more about Dave