Can you control how an SVG's stroke-width is drawn?
Asked Answered
P

14

284

Currently building a browser-based SVG application. Within this app, various shapes can be styled and positioned by the user, including rectangles.

When I apply a stroke-width to an SVG rect element of say 1px, the stroke is applied to the rect’s offset and inset in different ways by different browsers. This is proving to be troublesome, especially when I try to calculate the outer width and visual position of a rectangle and position it next to other elements.

For example:

  • Firefox adds 1px inset (bottom and left), and 1px offset (top and right)
  • Chrome adds 1px inset (top and left), and 1px offset (bottom and right)

My only solution so far would be to draw the actual borders myself (probably with the path tool) and position the borders behind the stroked element. But this solution is an unpleasant workaround, and I’d prefer not to go down this road if possible.

So my question is, can you control how an SVG’s stroke-width is drawn on elements?

Polio answered 30/8, 2011 at 9:49 Comment(4)
there are filter hacks you can use to achieve this - but it's not a great solutionShew
There is the paint-order parameter, where you can specify, that the fill should be rendered on top of the stroke, so you will get the "outside alignment", see jsfiddle.net/hne0kyLg/1Melliemelliferous
Found a way to do this using css 'outline-' attributes: codepen.io/badcat/pen/YVzmYY. Not sure what's the support for this across browsers, but might be useful.Worsted
SVG 2 also introduces new paint-order property (SVG 2 implementation seems to be in progress in Chrome).Stoll
D
504

No, you cannot specify whether the stroke is drawn inside or outside an element. I made a proposal to the SVG working group for this functionality in 2003, but it received no support (or discussion).

SVG proposed stroke-location example, from phrogz.net/SVG/stroke-location.svg

As I noted in the proposal,

  • you can achieve the same visual result as "inside" by doubling your stroke width and then using a clipping path to clip the object to itself, and
  • you can achieve the same visual result as 'outside' by doubling the stroke width and then overlaying a no-stroke copy of the object on top of itself.

Edit: This answer may be wrong in the future. It should be possible to achieve these results using SVG Vector Effects, by combining veStrokePath with veIntersect (for 'inside') or with veExclude (for 'outside). However, Vector Effects are still a working draft module with no implementations that I can yet find.

Edit 2: The SVG 2 draft specification includes a stroke-alignment property (with center|inside|outside possible values). This property may make it into UAs eventually.

Edit 3: Amusingly and dissapointingly, the SVG working group has removed stroke-alignment from SVG 2. You can see some of the concerns described after the prose here.

Detain answered 1/9, 2011 at 16:26 Comment(9)
Thought that might be the case. To solve this I've written a wrapper function for getBBox() called getStrokedBBox(). This wrapper returns the BBox according to how the browser applies strokes to the inset and offset of a shape. It's not perfect (need to keep checking against latest browser versions) but it does accurately provide a shapes outer width for now.Polio
@Detain maybe we'll see it before 10 years have passed. svgwg.org/svg2-draft/painting.html#SpecifyingStrokePaint annotationPrisoner
It's finally here! I, for one, have really been urging for this property to arrive.Spritsail
I created a svg-contour script for tracing contour of any SVGGeometryElement which can be used as a workaround of stroke-alignment, for anyone interested you can find a description hereAscender
Is there any page where I can upvote for the proposal? Thanks. Seems ridiculous it is not supported.Annihilator
Read the discussion you linked to. How about add support for rectangles and ellipse for now and then add support for paths after the details for paths have been worked out?Lawsuit
Are there any browser specific tags that support these? For example, -moz-stroke-alignment?Lawsuit
Here is a codepen with an example of the clipping mask: codepen.io/gotjoshua/pen/vYYpdxXImpeachable
It may not have made it into SVG 2, but it hasn’t been dropped completely. Perhaps we need to volunteer to help resolve the issues which have been identified in the draft. svgwg.org/specs/strokes/#SpecifyingStrokeAlignmentWeisburgh
L
80

I found an easy way, which has a few restrictions, but worked for me:

  • define the shape in defs
  • define a clip path referencing the shape
  • use it and double the stroke with as the outside is clipped

Here a working example:

<svg width="240" height="240" viewBox="0 0 1024 1024">
<defs>
	<path id="ld" d="M256,0 L0,512 L384,512 L128,1024 L1024,384 L640,384 L896,0 L256,0 Z"/>
	<clipPath id="clip">
		<use xlink:href="#ld"/>
	</clipPath>
</defs>
<g>
	<use xlink:href="#ld" stroke="#0081C6" stroke-width="160" fill="#00D2B8" clip-path="url(#clip)"/>
</g>
</svg>
Ligament answered 23/8, 2015 at 1:48 Comment(7)
The best answer I foundInnocence
Clever solution. ThanksFerminafermion
This should be accepted as answer. Using <defs> and <use> is the most elegant solution currently available.Attack
Nice solution. How would you reverse it to make an outside stroke?Malfunction
Why the top-level <use>? Why not just put the path and clip path in directly? This works just fine: <svg width="240" height="240" viewBox="0 0 1024 1024"> <path id="ld" d="M256,0 L0,512 L384,512 L128,1024 L1024,384 L640,384 L896,0 L256,0 Z" clip-path="url(#clip)" stroke="#0081C6" stroke-width="160" fill="#00d2b8"/> <clipPath id="clip"> <use xlink:href="#ld"/> </clipPath> </svg>. (Yes, this is completely reasonable and correct SVG and will render correctly everywhere. Fear no recursion.)Kermes
This works the best!! should be marked as solution. Also, @ChrisMorgan thank you, yours is a bit cleaner than the original and works just as well!Bench
It doesn't work when saved to a standalone file. You should either either add xmlns:xlink="http://www.w3.org/1999/xlink" into <svg ...> or replace xlink:href with href.Pumphrey
S
78

UPDATE: The stroke-alignment attribute was on April 1st, 2015 moved to a completely new spec called SVG Strokes.

As of the SVG 2.0 Editor’s Draft of February 26th, 2015 (and possibly since February 13th), the stroke-alignment property is present with the values inner, center (default) and outer.

It seems to work the same way as the stroke-location property proposed by @Phrogz and the later stroke-position suggestion. This property has been planned since at least 2011, but apart from an annotation that said

SVG 2 shall include a way to specify stroke position

, it has never been detailed in the spec as it was deferred - until now, it seems.

No browser support this property, or, as far as I know, any of the new SVG 2 features, yet, but hopefully they will soon as the spec matures. This has been a property I personally have been urging to have, and I'm really happy that it's finally there in the spec.

There seems to be some issues as to how to the property should behave on open paths as well as loops. These issues will, most probably, prolong implementations across browsers. However, I will update this answer with new information as browsers begin to support this property.

Spritsail answered 28/2, 2015 at 22:10 Comment(1)
stroke-alignment is specified in SVG Strokes, a W3C Working Draft. Meanwhile, the SVG 2 W3C Editor’s Draft says that a stroke positioning property shall be in the SVG spec at svgwg.org/svg2-draft/painting.html#SpecifyingStrokePaint, but that spec has already reached W3C Candidate Recommendation status and no such property is in the spec aside from a link to the stroke-position proposal, making it seem like that won't be the case.Crimp
A
70

You can use CSS to style the order of stroke and fills. That is, stroke first and then fill second, and get the desired effect.

MDN on paint-order: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/paint-order

CSS code:

paint-order: stroke;
Agma answered 9/8, 2018 at 11:36 Comment(5)
This is perfect! Thanks for sharingWhatever
The linked documentation doesn't seem to indicate whether the stroke is inside, outside, or centered? Could you clarify how to control where the stroke is drawn? Thanks.Wundt
The stroke is centred, just as it always was—but if you have a solid fill, the effect of adjusting the paint order to paint stroke first is that the inner half of the stroke is painted over, which has the same effect as drawing a half-as-thick stroke outside.Kermes
It's a good workaround as long as the fill is solid. If it's transparent, then the inner half of the stroke becomes visible, just like with the standard order.Pomatum
How can I make it stroke inside? It made a stroke outside.Shorter
P
8

Here's a function that will calculate how many pixels you need to add - using the given stroke - to the top, right, bottom and left, all based on the browser:

var getStrokeOffsets = function(stroke){

        var strokeFloor =       Math.floor(stroke / 2),                                                                 // max offset
            strokeCeil =        Math.ceil(stroke / 2);                                                                  // min offset

        if($.browser.mozilla){                                                                                          // Mozilla offsets

            return {
                bottom:     strokeFloor,
                left:       strokeFloor,
                top:        strokeCeil,
                right:      strokeCeil
            };

        }else if($.browser.webkit){                                                                                     // WebKit offsets

            return {
                bottom:     strokeCeil,
                left:       strokeFloor,
                top:        strokeFloor,
                right:      strokeCeil
            };

        }else{                                                                                                          // default offsets

            return {
                bottom:     strokeCeil,
                left:       strokeCeil,
                top:        strokeCeil,
                right:      strokeCeil
            };

        }

    };
Polio answered 2/9, 2011 at 11:37 Comment(0)
O
6

As people above have noted you'll either have to recalculate an offset to the stroke's path coordinates or double its width and then mask one side or the other, because not only does SVG not natively support Illustrator's stroke alignment, but PostScript doesn't either.

The specification for strokes in Adobe's PostScript Manual 2nd edition states: "4.5.1 Stroking: The stroke operator draws a line of some thickness along the current path. For each straight or curved segment in the path, stroke draws a line that is centered on the segment with sides parallel to the segment." (emphasis theirs)

The rest of the specification has no attributes for offsetting the line's position. When Illustrator lets you align inside or outside, it's recalculating the actual path's offset (because it's still computationally cheaper than overprinting then masking). The path coordinates in the .ai document are reference, not what gets rastered or exported to a final format.

Because Inkscape's native format is spec SVG, it can't offer a feature the spec lacks.

Ocular answered 4/11, 2014 at 20:59 Comment(0)
C
4

Here is a work around for inner bordered rect using symbol and use.

Example: https://jsbin.com/yopemiwame/edit?html,output

SVG:

<svg>
  <symbol id="inner-border-rect">
    <rect class="inner-border" width="100%" height="100%" style="fill:rgb(0,255,255);stroke-width:10;stroke:rgb(0,0,0)">
  </symbol>
  ...
  <use xlink:href="#inner-border-rect" x="?" y="?" width="?" height="?">
</svg>

Note: Make sure to replace the ? in use with real values.

Background: The reason why this works is because symbol establishes a new viewport by replacing symbol with svg and creating an element in the shadow DOM. This svg of the shadow DOM is then linked into your current SVG element. Note that svgs can be nested and every svg creates a new viewport, which clips everything that overlaps, including the overlapping border. For a much more detailed overview of whats going on read this fantastic article by Sara Soueidan.

Confirmatory answered 30/7, 2015 at 14:8 Comment(0)
P
4

Update 2023: The current draft renamed the attribute to stroke-align

Browser Support 2023:

See caniuse

This CSS property is not supported in any modern browser, nor are there any known plans to support it.

Polyfill-like helper function

Based on the previous approaches to combine paint-order, mask and clip-path.
(As suggested by @Xavier Ho @Jorg Janke)

//emulateStrokeAlign();

function emulateStrokeAlign() {
  let supportsSvgStrokeAlign = CSS.supports("stroke-align", "outer") ?
    true :
    CSS.supports("stroke-alignment", "outer") ?
    true :
    false;

  console.log("supportsSvgStrokeAlign", supportsSvgStrokeAlign);

  if (!supportsSvgStrokeAlign) {
    let ns = "http://www.w3.org/2000/svg";
    let strokeAlignmentEls = document.querySelectorAll(
      "*[stroke-alignment], *[stroke-align]"
    );
    strokeAlignmentEls.forEach((el, s) => {
      let svg = el.closest("svg");
      // set auto ids to prevent non-unique mask ids
      let svgID = svg.id ? svg.id : "svg_" + s;
      svg.id = svgID;

      //create <defs> if not previously appended
      let defs = svg.querySelector("defs");
      if (!defs) {
        defs = document.createElementNS(ns, "defs");
        svg.insertBefore(defs, svg.children[0]);
      }

      let style = window.getComputedStyle(el);
      let strokeWidth = parseFloat(style.strokeWidth);
      let strokeAlignment = el.getAttribute("stroke-alignment") ?
        el.getAttribute("stroke-alignment") :
        el.getAttribute("stroke-align");
      el.removeAttribute("stroke-align");
      el.removeAttribute("stroke-alignment");
      el.setAttribute("data-stroke-align", strokeAlignment);
      let maskClipId = `mask-${svgID}-${s}`;

      if (strokeAlignment === "outer") {
        // create mask
        let mask = document.createElementNS(ns, "mask");
        mask.id = maskClipId;
        let maskEl = el.cloneNode();
        mask.appendChild(maskEl);
        defs.appendChild(mask);
        maskEl.setAttribute("fill", "#000");
        mask.setAttribute("maskUnits", "userSpaceOnUse");
        maskEl.setAttribute("stroke", "#fff");
        maskEl.removeAttribute("stroke-opacity");
        maskEl.removeAttribute("id");
        maskEl.setAttribute("paint-order", "stroke");
        maskEl.style.strokeWidth = strokeWidth * 2;

        // clone stroke
        let cloneStroke = el.cloneNode();
        cloneStroke.style.fill = "none";
        cloneStroke.style.strokeWidth = strokeWidth * 2;
        cloneStroke.removeAttribute("id");
        cloneStroke.removeAttribute("stroke-alignment");
        cloneStroke.classList.add("cloneStrokeOuter");
        cloneStroke.setAttribute("mask", `url(#${maskClipId})`);
        el.parentNode.insertBefore(cloneStroke, el.nextElementSibling);
        //remove stroke from original element
        el.style.stroke = "none";
      }

      if (strokeAlignment === "inner") {
        //create clipPath
        let clipPathEl = el.cloneNode();
        let clipPath = document.createElementNS(ns, "clipPath");
        clipPath.id = maskClipId;
        defs.appendChild(clipPath);
        clipPathEl.removeAttribute("id");
        clipPath.appendChild(clipPathEl);
        el.setAttribute("clip-path", `url(#${maskClipId})`);
        el.style.strokeWidth = strokeWidth * 2;
      }
    });
  }
}
body {
  margin: 2em;
}

svg {
  width: 100%;
  height: auto;
  overflow: visible;
  border: 1px solid #ccc;
}

body {
  margin: 2em;
}

svg {
  height: 20em;
  overflow: visible;
  border: 1px solid #ccc;
}
<p><button onclick="emulateStrokeAlign()">Emulate stroke align</button></p>

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 380 120">
  <g id="myGroup" style="fill:rgb(45, 130, 255); stroke:#000; stroke-width:10; stroke-opacity:1;">
    <rect id="el1" stroke-alignment="outer" x="10" y="10" width="100" height="100" />
    <rect id="el2" x="140" y="10" width="100" height="100" />
    <rect id="el3" stroke-alignment="inner" x="270" y="10" width="100" height="100" />
  </g>
</svg>

<svg viewBox="0 0 12 6" xmlns="http://www.w3.org/2000/svg" stroke-width="0.5">
  <path d="M1,5 a2,2 0,0,0 2,-3 a3,3 0 0 1 2,3.5z" fill="blue" stroke-align="outer" stroke="red" stroke-opacity="0.5" stroke-linecap="butt" />
  <path d="M7,5 a2,2 0,0,0 2,-3 a3,3 0 0 1 2,3.5z" fill="blue" stroke-align="inner" stroke="red" stroke-opacity="0.5" />
</svg>

Hardcoded offset via paper.js offset glenzli's plugin

This approach will actually grow/shrink your <path> elements to get the desired stroke position (using the default middle stroke-alignment).

const canvas = document.createElement("canvas");
canvas.style.display='none';
document.body.appendChild(canvas);
//const canvas = document.querySelector("canvas");
paper.setup(canvas);

let strokeEls = document.querySelectorAll("*[stroke-alignment]");
strokeEls.forEach((el,i) => {
  let type = el.nodeName;
  let style = window.getComputedStyle(el);
  let strokeWidth = parseFloat(style.strokeWidth);
  let strokeAlignment = el.getAttribute('stroke-alignment');
  let offset = strokeAlignment==='outer' ? strokeWidth/2 : (strokeAlignment==='inner' ? strokeWidth / -2 : 0); 
  // convert primitive
  if(type!=='path'){
    el = convertPrimitiveToPath(el);
  }
  let d = el.getAttribute("d");
  let polyPath = new paper.Path(el.getAttribute("d"));
  let dOffset = offset ? PaperOffset.offset(polyPath, offset)
    .exportSVG()
    .getAttribute("d") : d;
  el.setAttribute("d", dOffset);
});
body{
  margin:2em;
}

svg{
  width:100%;
  overflow:visible;
  border:1px solid #ccc;
}
<svg viewBox="0 0 12 6" xmlns="http://www.w3.org/2000/svg" stroke-width="0.5">
  <path d="M1,5 a2,2 0,0,0 2,-3 a3,3 0 0 1 2,3.5" stroke="black" fill="none" stroke-linejoin="miter"/>
  <path d="M1,5 a2,2 0,0,0 2,-3 a3,3 0 0 1 2,3.5" fill="none" stroke-linejoin="miter" stroke-alignment="outer" stroke="red" stroke-opacity="0.5" />
  <path d="M7,5 a2,2 0,0,0 2,-3 a3,3 0 0 1 2,3.5" stroke="black" fill="none" stroke-linejoin="round" />
  <path d="M7,5 a2,2 0,0,0 2,-3 a3,3 0 0 1 2,3.5" fill="none" stroke-linejoin="round" stroke-alignment="inner" stroke="red" stroke-opacity="0.5" />
</svg>

<script src="https://unpkg.com/[email protected]/dist/paper-full.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/paperjs-offset.js"></script>

However, the library struggles with complex shapes.

Prowess answered 9/2, 2023 at 20:16 Comment(0)
S
2

I don’t know how helpful will that be but in my case I just created another circle with border only and placed it “inside” the other shape.

Springclean answered 8/11, 2016 at 15:34 Comment(0)
S
1

A (dirty) possible solution is by using patterns,

here is an example with an inside stroked triangle :

https://jsfiddle.net/qr3p7php/5/

<style>
#triangle1{
  fill: #0F0;
  fill-opacity: 0.3;
  stroke: #000;
  stroke-opacity: 0.5;
  stroke-width: 20;
}
#triangle2{
  stroke: #f00;
  stroke-opacity: 1;
  stroke-width: 1;
}    
</style>

<svg height="210" width="400" >
    <pattern id="fagl" patternUnits="objectBoundingBox" width="2" height="1" x="-50%">
        <path id="triangle1" d="M150 0 L75 200 L225 200 Z">
    </pattern>    
    <path id="triangle2" d="M150 0 L75 200 L225 200 Z" fill="url(#fagl)"/>
</svg>
Spikenard answered 3/6, 2015 at 15:46 Comment(0)
R
0

The solution from Xavier Ho of doubling the width of the stroke and changing the paint-order is brilliant, although only works if the fill is a solid color, with no transparency.

I have developed other approach, more complicated but works for any fill. It also works in ellipses or paths (with the later there are some corner cases with strange behaviour, for example open paths that crosses theirselves, but not much).

The trick is to display the shape in two layers. One without stroke (only fill), and another one only with stroke at double width (transparent fill) and passed through a mask that shows the whole shape, but hides the original shape without stroke.

  <svg width="240" height="240" viewBox="0 0 1024 1024">
  <defs>
    <path id="ld" d="M256,0 L0,512 L384,512 L128,1024 L1024,384 L640,384 L896,0 L256,0 Z"/>
    <mask id="mask">
      <use xlink:href="#ld" stroke="#FFFFFF" stroke-width="160" fill="#FFFFFF"/>
      <use xlink:href="#ld" fill="#000000"/>
    </mask>
  </defs>
  <g>
    <use xlink:href="#ld" fill="#00D2B8"/>
    <use xlink:href="#ld" stroke="#0081C6" stroke-width="160" fill="red" mask="url(#mask)"/>
  </g>
  </svg>
Ragi answered 24/4, 2020 at 8:54 Comment(0)
S
0

The easiest way I found is to add clip-path into circle

Add clip-path="circle()"

<circle id="circle" clip-path="circle()" cx="100" cy="100" r="100" fill="none" stroke="currentColor" stroke-width="5" />

Then the stroke-width="5" will magically become inner 5px stroke with absolute 100px radius.

Steatopygia answered 8/5, 2021 at 3:18 Comment(1)
Nope, it doesn't work: svgviewer.dev/s/sZPB9ffHAten
C
0

This worked for me:

.btn {
 border: 1px solid black;
 box-shadow: inset 0 0 0 1px black;
}
Courtesan answered 7/2, 2023 at 12:31 Comment(1)
You can't apply css properties border or box-shadow to svg elements like <rect>, <path>, <circle> etc.Prowess
E
0

I've read the answers in this topic since I was looking for a solution myself. In my case I couldn't edit the SVG inline so I needed to draw the stroke with external CSS. Doing this will make the stroke not fully visible because it's drawn on the outside of the path. The path becomes bigger than the viewbox so it will be hidden.

A simple fix for this is to put overflow: visible; on the svg itself. You can add a padding with half the size as the stroke-width to make it the original size.

svg {
  width: 80px;
  height: 80px;
  
  fill: transparent;
  
  & > * {
    stroke: black;
    stroke-width: 10px;
  }
}

svg.stroke-visible {
  overflow: visible;
  padding: 5px; //Half the stroke-width
}
Stroke not fully visible:
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="800" height="800" version="1.0" viewBox="0 0 64 64">
<path d="M62.799 23.737a3.941 3.941 0 0 0-3.139-2.642l-16.969-2.593-7.622-16.237a3.938 3.938 0 0 0-7.13 0l-7.623 16.238-16.969 2.593a3.937 3.937 0 0 0-2.222 6.642l12.392 12.707-2.935 17.977a3.94 3.94 0 0 0 5.797 4.082l15.126-8.365 15.126 8.365a3.94 3.94 0 0 0 5.796-4.082l-2.935-17.977 12.393-12.707a3.942 3.942 0 0 0 .914-4.001z"/>
 </svg>
 
 Stroke fully visible:
 
 <svg class="stroke-visible" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="800" height="800" version="1.0" viewBox="0 0 64 64">
<path d="M62.799 23.737a3.941 3.941 0 0 0-3.139-2.642l-16.969-2.593-7.622-16.237a3.938 3.938 0 0 0-7.13 0l-7.623 16.238-16.969 2.593a3.937 3.937 0 0 0-2.222 6.642l12.392 12.707-2.935 17.977a3.94 3.94 0 0 0 5.797 4.082l15.126-8.365 15.126 8.365a3.94 3.94 0 0 0 5.796-4.082l-2.935-17.977 12.393-12.707a3.942 3.942 0 0 0 .914-4.001z"/>
 </svg>
Esmaria answered 5/7, 2023 at 19:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.