Elegant Outlines with SVG paint-order

Get fine-grained control over your design with an SVG 2 property implemented by many browsers.

spline_1

SVG rendering uses a painter’s model to describe how graphics are rendered to the screen. Like layers of paint on a wall, content on top obscures content below. The SVG specifications define which content gets painted over which other content. The different parts of each shape — the stroke, fill, and markers — each create layers of paint. Those shapes are then layered one on top of the other, in the order they are defined in the document.

Two new properties introduced by the SVG 2 specification, z-index and paint-order, allow you to change up the rendering rules.

Most web designers will be familiar with z-index, which has been supported in CSS layout for years. Unfortunately, it is not yet supported in any major web browser for SVG content. At present, the only solution is to arrange your markup (or the DOM created by scripts) so that elements are listed in the order you want them to be painted.

In contrast, the paint-order property has already been implemented in a number of web browsers. If you’re willing to make adjustments in your design according to browser support level, you can use the fine-tuned control in the latest browsers and replace it with a simpler effect in others. If you need the same appearance in all browsers, however, you can create something that looks like paint order control with SVG 1.1 code. This post describes why paint-order is useful, how to use it in the latest browsers, and how to fake it in the others.

Understanding the SVG paint properties

The shape elements in you SVG code define precise geometric curves using resolution-independent mathematical relationships. An SVG <line> is just the idea of a line, connecting two infinitesimal points; it has no thickness of its own. Text in SVG is also defined as geometric outlines, based on the vector curves in the font files.

When you include a shape or text element in an SVG without any style information, it is displayed as a solid black region exactly matching the dimensions you specify. This is the default fill value: solid black.

The fill property tells the SVG-rendering software what to do with that geometric shape. For every pixel on the screen — or ink spot on the paper — the software determines if that point is inside or outside of the shape. If it is inside, the software turns to the fill value to find out what to do next.

In the simple case (like the default black), the fill value is a color and all the points inside the shape get replaced by that color. In other cases, the fill value is an instruction to look up more complicated painting code. Where to look it up is indicated by a URL referencing the id of an SVG element representing the instructions (aka, a paint server, such as a gradient or pattern).

In addition to — or instead of — a fill, you can paint a shape by stroking it. In computer graphics, stroking a shape means drawing a line along its edge. Different programs have different interpretations of what that can mean.

In SVG (currently, anyway), stroking is implemented by generating a secondary shape extending outwards and inwards from the edges of the main shape. The stroke property is by default none, but it can be set to a color value or a paint server reference to create a visible stroke. The thickness of the stroke (set by the stroke-width property) is centered over the edge of the shape, half overlapping the fill region and half outside. Other stroke-related properties control the details of the generated shape, such as how it wraps around corners, or break the shape into a dashed line.

The stroke region is then painted using the same approach as for filling the main shape: the software scans across, and determines whether a point is inside or outside the stroke. If the point is inside, the software uses the painting instructions from the stroke property to assign a color.

The order of operations

When a shape has both fill and stroke paint, some pixels are included in both the fill area and the stroke region, and therefore have two different colors specified. As with all of SVG, the painter’s model applies: if both colors are opaque, the color of the layer on top replaces the color of the layer below.

But which layer is “on top”?

By default, the stroke is painted on top of the fill. This means that you can always see the full stroke width. It also means that if the stroke is partially-transparent, it will appear two-toned. The fill paint color will be visible under the inner half of the stroke region but not under the outer half.


Tip:

Stroke markers — symbols that display on the corners of custom shapes — are painted after the fill and stroke, in order from start to end of the path.


In SVG 1.1, the only way to draw a stroke underneath the fill is to separate it into two shapes: one with stroke only, and then the same shape duplicated in the same position (with a <use> element), filled but not stroked:

<g stroke="blue" fill="red">
    <g fill="none">
        <path id="shape" d="..." />
    </g>
    <use xlink:href="#shape" stroke="none" />
</g>

The above snippet makes extensive use of inherited styles. The <path> itself does not have any fill or stroke values directly set; it inherits from its surrounding. The overall stroke and fill values are set on the containing <g>; one or the other is then cancelled out on the nested group and the <use> element.

SVG 2 introduces the paint-order property to make this effect much easier to achieve. Its value is a list of whitespace-separated keywords (fill, stroke, and markers) that indicate the order in which the various parts of the shape should be painted. So the same effect could be created with a single element:

<path id="shape" d="..." stroke="blue" fill="red" 
      paint-order="stroke fill" />

Any paint layers you don’t specify in the paint-order property will be painted later (markers, in this case), in the same order they normally would be. This means that to swap fill and stroke, you only need to specify the stroke:

<path id="shape" d="..." stroke="blue" fill="red" 
      paint-order="stroke" />

The stroke will be painted first, then fill, and finally any markers. The entire fill region will always be visible, even where it overlaps the stroke.

The default value of paint-order (equivalent to fill stroke markers) can be explicitly set with the normal keyword.


WARNING:

At the time of writing, paint-order is supported in the latest Firefox (since version 31), Blink (since Chromium version 35), and WebKit (since March 2014) browsers. Internet Explorer and Edge, as well as older versions of the other browsers, use the default paint order.


The ability to control painting order is especially important with text. Text in SVG can be stroked just like shapes can, to create an outlined effect. However, all but the thinnest strokes tend to obscure the details of the letters.

By painting the fill region overtop of the stroke — in a contrasting color — you can reinforce the shape of the letters and restore legibility. paint-order-example uses paint-order and a thick stroke to create a crisp outline around heading text. paint-order-okay-figure shows the result in a supporting browser.

paint-order-example
Stroking without obscuring the finer details of text:

<svg xmlns="http://www.w3.org/2000/svg"
     viewBox="0 0 400 80" width="4in" height="0.8in"
     xml:lang="en">
    <title>Outlined text, using paint-order</title>
    <rect fill="navy" height="100%" width="100%" />
    <text x="50%" y="70" 
          text-anchor="middle"
          font-size="80" 
          font-family="sans-serif"
          fill="mediumBlue" 
          stroke="gold"
          stroke-width="7"
          paint-order="stroke"
          >Outlined</text>
</svg>

paint-order-okay-figure
Outlined text with strokes painted behind the fill:

paint-order-okay

Falling back gracefully

If you relied solely on paint-order to achieve this effect, your text would be a blocky mess on unsupporting browsers, as shown in paint-order-no-support-figure. Some fallback strategies are in order.

paint-order-no-support-figure
Outlined text with strokes painted using the default order:

paint-order-no-support

One solution is to use the CSS @supports conditional rule to only apply the outline effect if paint-order is supported. In other cases, use a different styling that provides legible text — if not the desired effect.

paint-order-supports-example provides a modified version of the code from paint-order-example; the styles have been moved from presentation attributes to a <style> block so that conditional CSS can be applied. The basic styles include a much narrower stroke when painting order cannot be controlled; the @supports block replaces this with the thick stroke and paint-order option.

The result looks the same as paint-order-okay-figure in browsers that support paint-order (all of which currently also support the @supports rule). paint-order-supports-figure shows how the revised code looks in other browsers.

paint-order-supports-example
Testing support before using paint-order:

<svg xmlns="http://www.w3.org/2000/svg"
     viewBox="0 0 400 80" width="4in" height="0.8in"
     xml:lang="en">
    <title>Using @supports to adjust paint-order effects</title>
    <style type="text/css">
        .outlined {
          text-anchor: middle;
          font-size: 80px; 
          font-family: sans-serif;
          fill: mediumBlue; 
          stroke: gold;

          /* fallback */
          stroke-width: 3;
        }

        @supports (paint-order: stroke) {
            .outlined {
              stroke-width: 7;
              paint-order: stroke;
            }
        }
    </style>
    <rect fill="navy" height="100%" width="100%" />
    <text x="50%" y="70" class="outlined"
          >Outlined</text>
</svg>

paint-order-supports-figure
Text with a narrower outline when paint-order is not supported:

paint-order-supports


Tip:

The stroke-width has been cut by more than half between paint-order-okay-figure and paint-order-supports-figure. However, the stroke only appears slightly narrower, because the inside half of the stroke is now visible on top of the fill.


If changing the appearance with @supports is not acceptable to you, the only alternative is to duplicate the elements to create one for stroke and one for fill. Depending on the way you are using your SVG, and how much control you have over its styling, you may be able to use a script to perform the conversion for you when necessary. Because paint-order is a new style property, browsers that do not support it will not include it within the style property of each element. You can therefore detect these browsers and generate the extra <use> elements as required.

paint-order-script-example provides a sample script which identifies elements by classname and performs the manipulations if required. The result (in a browser that does not support paint-order) is shown in paint-order-script-figure. Although it appears identical to paint-order-okay-figure, the underlying DOM structure is much more complex.

paint-order-script-example
Simulating paint-order with Multiple Elements:

<svg xmlns="http://www.w3.org/2000/svg"
     xmlns:xlink="http://www.w3.org/1999/xlink"
     viewBox="0 0 400 80" width="4in" height="0.8in"
     xml:lang="en">
    <title>Faking paint-order with JavaScript</title>
    <style type="text/css">
        .outlined {
          text-anchor: middle;
          font-size: 80px; 
          font-family: sans-serif;
          fill: mediumBlue; 
          stroke: gold;
          stroke-width: 7;
          paint-order: stroke;
        }
    </style>
    <rect fill="navy" height="100%" width="100%" />
    <text x="50%" y="70" class="outlined"
          >Outlined</text>
    <script><![CDATA[
(function(){
    var NS = {svg: "http://www.w3.org/2000/svg",
              xlink: "http://www.w3.org/1999/xlink"
             };
    var index = 10000;

    var t = document.getElementsByClassName("outlined");   //<1>
    if ( t && 
        (t[0].style["paint-order"] === undefined )){       //<2>
        Array.prototype.forEach.call(t, fakeOutline);      //<3>
    }

    function fakeOutline(el){
        el.id = el.id || "el-" + index++;                  //<4>

        var g1 = document.createElementNS(NS.svg, "g");    //<5>
        g1.setAttribute("class", el.getAttribute("class") );
        el.removeAttribute("class");
        el.parentNode.insertBefore(g1, el);

        var g2 = document.createElementNS(NS.svg, "g");    //<6>
        g2.style["fill"] = "none";
        g2.insertBefore(el, null);
        g1.insertBefore(g2, null);

        var u = document.createElementNS(NS.svg, "use");   //<7>
        u.setAttributeNS(NS.xlink, "href", "#" + el.id);
        u.style["stroke"] = "none";
        g1.insertBefore(u, null);
    }
})();
]]> </script>
</svg>

<1> The elements to modify are identified by a specific class name, "outlined", for easy access in the script.

<2> The style property of any element (here, the first element selected) can be examined to determine if it supports the paint-order property. A strict equality test (===) is used to distinguish an empty value (no inline style was set on the element) from an undefined value (the property name is not recognized).

<3> If the fallback is required, the fakeOutline() method is called for each element that had the class name. The forEach() array method is used to call the function as many times as needed. However, since the list returned by getElementsByClassName() is not a true JavaScript Array object, you cannot use t.forEach(fakeOutline). Instead, the forEach() function is extracted from the Array prototype and then invoked using its own call() method.

<4> The fakeOutline() function will duplicate the outlined element with a <use> element, so it will need a valid id value; if it doesn’t already have one, an arbitrary value is added with a unique index.

<5> The element is replaced by a group that is transferred all of its classes. This of course requires that all fill and stroke styles are assigned via class, and not by tag name or via presentation attributes. The insertBefore() method is used to ensure that the new group will have the same position in the DOM tree as the element it is replacing.

<6> A nested group will hold the original element, but will prevent it from inheriting the fill style.

<7> Finally, a <use> element duplicates the element, but cancels out the stroke style so that it only inherits fill styles. It is inserted into the main group as the last child (“before” nothing), so that it will be drawn on top of the version with no fill.

paint-order-script-figure
Text duplicated to mimic a stroke-first paint order:

paint-order-script

As you can tell, the script is rather convoluted for such a simple effect. A more generic fallback script — a complete polyfill for the property — would be even more complex, as you would need to account for all the different ways in which a style property can be applied to an element. Effectively, you need to recreate the work of the CSS parser, identifying all the style rules it discarded as invalid.

In most cases, if the final appearance is essential in all browsers, it is easier to create the layered stroke and fill copies of the object within your markup, directly creating the structure that would be generated by the script:

<g class="outlined">
    <g style="fill: none;">
        <text id="el-10000" x="50%" y="70">Outlined</text>
    </g>
    <use style="stroke: none;" xlink:href="#el-10000" />
</g>

Public domain spline image via Wikipedia.

tags: , , , ,

Get the O’Reilly Web Platform Newsletter

Stay informed. Receive weekly insight from industry insiders—plus exclusive content and offers.

  • Kay2Dan

    A big fan of SVG here & looking forward to SVG2 landing in browsers sooner than later. Is there any resource which points to all the spec features of SVG2 and their progress within browsers, kind of like http://www.caniuse.com/ ?