Fixing SVG url() in coexistence with „base“ for Safari

Fixing SVG url() in coexistence with „base“ for Safari

The url() function is a mightly tool if used with SVG definitions, since you can reference an element by ID with url(#element-id). This can be used to set e.g. the mask or fill attribute. Given the following code for example:

<svg height="35" width="350">
  <defs>
    <linearGradient x1="0%" y1="0%" x2="0%" y2="100%" id="myGradient">
      <stop offset="0" stop-color="white"></stop>
      <stop offset="1" stop-color="black"></stop>
    </linearGradient>
    <mask id="myMask">
      <rect x="0" y="0" width="100%" height="100%" fill="url(#myGradient)" />
    </mask>
  </defs>
  
  <text x="0" y="30" fill="red" font-size="30px" mask="url(#myMask)">This SVG text is masked!</text>
</svg>

This code generates a text which is masked vertically with a linear gradient from visible/opaque (white) to unvisible/transparent (black):

The code works, unless you use the <base> element in your HTML. The <base> element defines how relative URLs will get resolved on your webpage. Many applications written with SPA frameworks like Angular use it to define resource access independent from the hosting location, e.g. by setting <base href="/">. Unfortunately, this influences resolving url() statements as well, since IDs will be resolved relative to the given <base> href and not to the current document where the element is defined. Chrome, Chromium Edge and Firefox don’t have a problem here, but Safari just doesn’t find the ID and thus does not apply the mask. In other scenarios, using fill to reference gradients just gives a black color in Safari. The visual error is different, but the cause (and solution) is exactly the same.

But help is underway! Instead of just using url(#element-id), we can prepend the current window.location, thus making the URL absolute, which fixes our problem. And since we want to apply this fix dynamically, we can include the following JavaScript snippet in a central place, which fixes the URLs for all fill and mask attributes in our SVG:

/**
 * Fixes references to SVG IDs.
 * Safari won't display SVG masks/fills e.g. referenced with
 * `url(#id)` when the <base> tag is on the page.
 *
 * More info:
 * - http://stackoverflow.com/a/18265336/796152
 * - http://www.w3.org/TR/SVG/linking.html
 */
export function fixSvgUrls() {
    function fixForAttribute(attrib) {
        const baseUrl = window.location.href;

        /**
         * Find all svg elements with the given attribute, e.g. for `mask`.
         * See: http://stackoverflow.com/a/23047888/796152
         */
        [].slice.call(document.querySelectorAll(`svg [${attrib}]`))
            // filter out all elements whose attribute doesn't start with `url(#`
            .filter((element: SVGElement) => element.getAttribute(attrib).indexOf('url(#') === 0)
            // prepend `window.location` to the attrib's url() value, in order to make it an absolute IRI
            .forEach((element: SVGElement) => {
                const maskId = element.getAttribute(attrib).replace('url(', '').replace(')', '');
                element.setAttribute(attrib, `url(${baseUrl + maskId})`);
            });
    }

    // this fixes the URL IDs for 'fill' and 'mask'; if you need others, add them here
    fixForAttribute('fill');
    fixForAttribute('mask');
}

Just call this code after initialization of your HTML page (by using document.addEventListener("DOMContentLoaded", fixSvgUrls)) or in ngOnInit() in your Angular application or what else and it will fix your SVG.

Ich bin freiberuflicher Senior Full-Stack Web-Entwickler (Angular, TypeScript, C#/.NET) im Raum Frankfurt/Main. Mit Leidenschaft für Software-Design, Clean Code, moderne Technologien und agile Vorgehensmodelle.

2 Kommentare

  1. Mike Jones 2 Jahren vor

    Hey Matthias, i have been having problems with this bug on Safari with svg linear and radial gradients for a while now. Since your solution apply to Angular, i was wondering if you have a quick work-around solution for a vanilla javascript website using wordpress and barba ? I have been trying to adapt your solution to it but i fail miserably.

    Cheers and thank you.

  2. Autor

    Hi Mike, the solution is not Angular-specific, in fact it is vanilla JavaScript (perhaps besides „svgElement: SVGElement“, where you should remove the typing), you just need a Hook where it will be called. In plain JavaScript this could be done with document.addEventListener(„DOMContentLoaded“, fixSvgUrls). I don’t know about the quirks of WordPress/Barba in this setting, but it should be definitely possible.

Eine Antwort hinterlassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

*

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.