DEV3 Technologies home

Sanitised inline SVG icon helper

Load SVG icons from the theme folder, strip anything unsafe with wp_kses and a strict allow-list, and print them inline.

WordPressUtilitiessvgiconsksessecurity

Code

<?php
/**
 * Inline SVG icons from /assets/icons/<name>.svg
 *
 * @package Acme_Theme
 */

defined( 'ABSPATH' ) || exit;

/** Tags and attributes a plain icon needs. Anything else is removed. */
function acme_svg_allowed_html(): array {
	$common = array( 'class' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, 'transform' => true, 'opacity' => true );
	return array(
		'svg'      => $common + array( 'xmlns' => true, 'viewbox' => true, 'width' => true, 'height' => true, 'aria-hidden' => true, 'role' => true, 'focusable' => true ),
		'g'        => $common,
		'title'    => array(),
		'path'     => $common + array( 'd' => true, 'fill-rule' => true, 'clip-rule' => true ),
		'circle'   => $common + array( 'cx' => true, 'cy' => true, 'r' => true ),
		'rect'     => $common + array( 'x' => true, 'y' => true, 'width' => true, 'height' => true, 'rx' => true ),
		'line'     => $common + array( 'x1' => true, 'y1' => true, 'x2' => true, 'y2' => true ),
		'polyline' => $common + array( 'points' => true ),
	);
}

/**
 * Return a sanitised icon, or an empty string if it does not exist.
 *
 * @param string $name  File name without extension, [a-z0-9-] only.
 * @param string $label Accessible name; empty marks the icon decorative.
 */
function acme_icon( string $name, string $label = '' ): string {
	$name = sanitize_file_name( $name );
	if ( ! preg_match( '/^[a-z0-9-]+$/', $name ) ) {
		return '';
	}

	$cache_key = 'acme_icon_' . $name;
	$svg       = wp_cache_get( $cache_key, 'acme' );

	if ( false === $svg ) {
		$file = get_template_directory() . '/assets/icons/' . $name . '.svg';
		$raw  = is_readable( $file ) ? (string) file_get_contents( $file ) : '';
		$svg  = wp_kses( $raw, acme_svg_allowed_html() );
		wp_cache_set( $cache_key, $svg, 'acme' );
	}

	if ( '' === $svg ) {
		return '';
	}

	$attrs = '' === $label
		? ' aria-hidden="true" focusable="false"'
		: ' role="img" aria-label="' . esc_attr( $label ) . '"';

	return preg_replace( '/^<svg/', '<svg' . $attrs, $svg, 1 );
}

// Template use: echo acme_icon( 'arrow-right' );
// Output is already sanitised, so echo it directly.

How it works

What it solves

Inline SVG icons are crisp and styleable with currentColor, but an SVG is a document that can carry scripts and event handlers. Reading arbitrary files and echoing them is a cross-site scripting risk, so every icon should pass through a strict allow-list.

How it works

acme_icon() accepts a name, normalises it with sanitize_file_name, and insists on lowercase letters, digits and dashes so path traversal is impossible. The file is read from the theme and filtered by wp_kses with a short list of drawing elements and attributes; script, foreignObject and on* handlers are not on it and disappear. The result is cached in the object cache and given either aria-hidden or an accessible label.

The returned string can be echoed directly.

Adapting it

Extend the allow-list only with elements you actually use, such as defs and linearGradient. Make icons inherit colour by using currentColor in the files. Add a size parameter that sets width and height. To support uploads, apply the same sanitiser at upload time and never display unsanitised files.

Pitfalls

  • wp_kses lowercases attribute names, which is why viewbox is written that way in the list.
  • Do not allow style or href without extra care; they can carry URLs.
  • The object cache is per request without a persistent backend; that is fine.
  • Remove XML headers and comments from icon files.

How to use it

Save as inc/icons.php, require it from functions.php, and put files in assets/icons/.

Use echo acme_icon( 'arrow-right' ); for a decorative icon.

Use echo acme_icon( 'search', 'Search' ); when the icon stands alone and needs a name.

Licence: GPL-2.0-or-later. This snippet comes from WordPress code and is GPL-2.0-or-later.

Related snippets