SVG Support

SVG elements are supported in the following scenarios:

  • Inline in the DOM
  • As a background-image
  • As the source of <img> elements
  • As a border via border-image-source

Support overview

The supported feature set is a subset of the SVG 2.0 specification. SVG elements can be styled and animated using standard CSS.

The most notable parts of the specification that are not supported are:

  • Document structure
    • No conditional processing
    • No metadata
    • No WAI-ARIA support
  • Text
    • Only basic <text> support. No additional text-related properties or elements.
    • <tspan> elements are ignored; their text is treated as part of the parent <text>.
  • Embedded content
    • No extensibility support (no <foreignObject>).
  • Painting
    • No vector-effects
    • No <marker> elements
    • No paint-order
    • No color-interpolation
    • No rendering hints
    • No will-change
  • Paint servers
    • No pattern support.
  • Scripting and interactivity
    • Not supported.
  • Linking
    • No <a> elements
    • No <view> elements
  • Multimedia
    • Not supported.
  • Animation
    • No SMIL animation. CSS/Web animations are supported.

Element-specific notes

  • <solidcolor> is deprecated in SVG, but still supported.
  • SVG elements cannot use mask: url(#...) and clip-path: url(#...) at the same time. If both are specified, clipping (clip-path) takes precedence.

SVG attributes vs. CSS properties with the same name

Several SVG attributes share names with CSS properties but have different parsing rules.

The primary difference is unit handling:

  • In CSS, properties like width, height, and font-size must include units (px, em, etc.).
  • In SVG attributes, these values may be unitless.

transform and transform-origin also differ between their CSS and SVG variants.

This does not typically affect styling, but it becomes relevant when animating these “duplicated” properties in CSS keyframes. See the Animation section below.

Animation

Standard CSS animations are supported for inline SVGs.

There are two important limitations to be aware of:

  • Several properties have the same names in CSS and SVG but follow different rules.

    For example:

    • CSS width must include units
    • SVG width may not include units

    For consistency with the animation system, keyframe animations for such properties must include units even when the SVG attribute normally does not.

    In the example below, y (an SVG-only attribute) remains unitless, while width requires px:

@keyframes moving-rect {
  0% {
    y: 0;
    width: 85px;
  }
  100% {
    y: 100;
    width: 125px;
  }
}
  • Interpolation between two paths containing elliptical arc commands (A / a) is not guaranteed to work, even if the commands appear compatible. Elliptical arcs are internally converted to up to four quadratic Bezier curves, so two arcs that look similar may end up with different internal representations.

    Paths can only be interpolated if their internal command sequences match.

    For example:

    • M 10,30 A 20,20 0,0,1 50,30
    • M 10,30 A 40,20 0,0,1 50,30

    cannot be interpolated with each other because one arc expands to two Bezier curves, while the other becomes a single curve.

    When animating paths, avoid elliptical arcs when possible and use quadratic or cubic curves.

Performance optimizations

SVGs are internally cached when any of the following conditions is true:

  • coh-use-aa-geometry is disabled (default: enabled)
  • The SVG content size does not exceed 1024x1024, and for non-inline SVG usage, background-repeat is not set to no-repeat

SVGs are re-tessellated and redrawn when:

  • Their content rectangle changes (e.g., due to animation)
  • A style change invalidates part of the SVG tree

Inline SVGs are never cached in GPU textures. They follow the standard DOM redraw rules: they are redrawn only when a region of their content becomes dirty.

Example:

<!DOCTYPE html>
<html>
	<style>
		.cached1 {
			width: 300px;
			height: 300px;
			background-image: url("cohtml.svg");
		}
		.cached2 {
			width: 2048px;
			height: 2048px;
			background-image: url("cohtml.svg"); /* 800x600 content size */
		}
		.cached3 {
			width: 300px;
			height: 300px;
		}
		.cached4 {
			width: 300px;
			height: 300px;
			background-repeat: no-repeat;
		}
		.non-cached1 {
			width: 300px;
			height: 300px;
			background-image: url("cohtml.svg");
			background-repeat: no-repeat;
		}
		.non-cached2 {
			width: 300px;
			height: 300px;
			background-image: url("cohtml-large.svg"); /* 2048x2048 content size */
		}
	</style>
	<body>
		<!-- Cached, since the content rectangle of `cohtml.svg` does not exceed 1024x1024. The element size is irrelevant. -->
		<div class="cached1"></div>
		<div class="cached2"></div>

		<!-- Inline images are also cached. The `background-repeat` property is ignored here. -->
		<img class="cached3" src="cohtml.svg">
		<img class="cached4" src="cohtml.svg">

		<!-- Not cached, since the `background-repeat` property is set to `no-repeat`. -->
		<div class="non-cached1"></div>

		<!-- Not cached, since the content rectangle of `cohtml-large.svg` exceeds 1024x1024. The element size is irrelevant. -->
		<div class="non-cached2"></div>
	</body>
</html>

Edge cases

Relative Units in SVG Intrinsics and Media Queries

While the SVG specification states that only absolute units should be used to compute intrinsic dimensions, modern browsers often allow font-relative units (like rem). This introduces complex edge cases, particularly when an SVG is used as a background image.

Consider the following SVG, where width depends on font-size, but font-size depends on the viewport width via a media query:

<svg width="10rem" height="10rem" font-size="10px" viewBox="0 0 50 50" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
    <style>
        @media (min-width: 100px) and (max-width: 100px) {
            svg {
                font-size: 50px;
            }
        }
    </style>
    <rect x="0" y="0" width="1rem" height="1rem" fill="rgba(0, 255, 0, 0.5)"/>
    <rect x="0" y="0" width="50" height="50" fill="rgba(0, 0, 255, 0.5)" />
</svg>

To render this, Gameface performs the following steps:

  1. Parse: Load the SVG and construct the DOM nodes.
  2. Initial Style Resolution: Resolve styles for the nodes, ignoring viewport-dependent media queries (since the viewport size is not yet known).
  3. Intrinsic Calculation: Compute the SVG’s intrinsic dimensions based on these initial styles.
  4. Box Determination: Calculate the final draw box on the page using the intrinsics and the page CSS.
  5. Contextual Resolution: Apply the calculated draw box as the viewport context. This activates or deactivates viewport-based media queries.
  6. Final Layout: Re-calculate layout for the SVG content based on the new context.
  7. Draw: Render the content.

In the example above, activating the media query changes the font-size, which changes the intrinsics (Step 3). If the engine were to re-evaluate the intrinsics based on this new state (e.g. --redraw-all), the draw box would change, disabling the media query, and creating an infinite layout loop.

Currently, Gameface does not automatically break this infinite loop. We rely on users to avoid constructing circular dependencies between intrinsic sizing and media queries.