DEV Community

Cover image for Dynamic Border-Image Recoloring with SVG Filters
Björn Crombach
Björn Crombach

Posted on

Dynamic Border-Image Recoloring with SVG Filters

border-image has been supported in CSS for many years and allows complex decorative borders without additional markup. It works particularly well for pixel-art, where alternatives like clip-path or box-shadow quickly become difficult to maintain as the design becomes more detailed. border-image keeps the complexity constant, regardless of the level of detail in the design because it is handled as an image.

However, border-image has one major limitation: it is static. If you want multiple states (hover, active, focus-visible, theme variants), you normally need multiple image assets. This will result in more HTTP requests, more data being transferred and more image files that need to be maintained and handled.

One solution I found was to use SVG filters to dynamically recolor border-image files by referencing the filter via url(). In practice the filter is applied to a pseudo-element that renders the border-image so that the filter does not affect the content inside the element.

border-image: url("fileName.png") 8 fill;
/* Re-color the border-image */
filter: url("#remap");
Enter fullscreen mode Exit fullscreen mode
<!-- Invisible SVG filter for color remapping -->
<svg
    xmlns="http://www.w3.org/2000/svg"
    style="position: absolute; width: 0; height: 0; overflow: hidden"
>
    <filter id="remap" color-interpolation-filters="sRGB">
    <feColorMatrix
        id="svg-matrix"
        type="matrix"
        values="
            1  0  0  0  0
            0  1  0  0  0
            0  0  1  0  0
            0  0  0  1  0
        "
    />
    </filter>
</svg>
Enter fullscreen mode Exit fullscreen mode

For this to work, we first need to create a specialised asset. Each region that should be able to receive a specific color must use a unique placeholder color. The filter then uses these placeholder colors as keys and remaps them to the desired output colors. Then we create multiple SVG filters that transform the image into each desired state. See the following demo to see this in action on a pixel art button.

Another use case for dynamic recoloring could be to support more color themes, or simply light and dark variants.

Recoloring

The first approach I took was to use the feColorMatrix filter to remap red, green, blue and black pixels to the target colors. While this worked, it was limited to only four colors.

The next approach was to replace feColorMatrix with feComponentTransfer to build a look-up table (LUT) for greyscale texture maps.

feComponentTransfer allows remapping individual color channels using lookup tables with up to 256 discrete slots while preserving the full transparency range. When applied to a grayscale asset, the filter effectively becomes a palette lookup: each gray value acts as an index into the color table and is mapped to a corresponding output color.

<!-- Invisible SVG filter for color remapping -->
<svg
    xmlns="http://www.w3.org/2000/svg"
    style="position: absolute; width: 0; height: 0; overflow: hidden"
>
    <filter id="palette-remap" color-interpolation-filters="sRGB">
    <feComponentTransfer>
        <feFuncR id="fr" type="discrete" tableValues="0 0 0 0 0 0 0 0 0 0" />
        <feFuncG id="fg" type="discrete" tableValues="0 0 0 0 0 0 0 0 0 0" />
        <feFuncB id="fb" type="discrete" tableValues="0 0 0 0 0 0 0 0 0 0" />
    </feComponentTransfer>
    </filter>
</svg>
Enter fullscreen mode Exit fullscreen mode

An even more powerful approach that came to mind was to utilise all channels, essentially trying something similar to channel packing, which is commonly used in game engines to combine multiple greyscale maps into one image for data transport.

To achieve this, we combine both techniques. feColorMatrix is used to isolate individual channels (R, G, B), while feComponentTransfer applies the lookup table that remaps the grayscale values to their target colors. This filter setup is repeated for each channel.

<svg
    xmlns="http://www.w3.org/2000/svg"
    style="position: absolute; width: 0; height: 0; overflow: hidden"
>
    <!-- R channel: extract + remap (7 slots, tableSize=7) -->
    <filter id="unpack-r" color-interpolation-filters="sRGB">
    <feColorMatrix
        type="matrix"
        values="
        1 0 0 0 0
        1 0 0 0 0
        1 0 0 0 0
        1 0 0 0 0
    "
    />
    <feComponentTransfer>
        <feFuncR id="ur-r" type="discrete" tableValues="0 0 0 0 0 0 0" />
        <feFuncG id="ur-g" type="discrete" tableValues="0 0 0 0 0 0 0" />
        <feFuncB id="ur-b" type="discrete" tableValues="0 0 0 0 0 0 0" />
        <feFuncA type="linear" slope="255" intercept="0" />
    </feComponentTransfer>
    </filter>

    <!-- ... Other channels -->
</svg>
Enter fullscreen mode Exit fullscreen mode

However, the alpha channel turned out to be problematic. Browsers internally premultiply RGB values with the alpha channel before the image reaches the SVG filter. This means that storing an independent texture map in the alpha channel corrupts the RGB data, making it unsuitable for channel packing.

Another limitation of this approach is that it cannot fully support transparency when independent texture maps are used in each channel, as can be seen in the demo below. To support basic visibility of pixels, I decided that #000 should be treated as fully transparent. This means that we lose one slot per channel (leaving us with 255 slots), but gain support for pixel visibility.

While this approach is by far the most complex, it reduces the number of requests and file size even further. The packed texture in the following demo is only 432 bytes, but it serves three border-images.

Conclusion

  • While the last approach is technically very interesting, I don't think it justifies the extra complexity and preparation needed to fully utilise it, unless you really want to optimise to the limit.
  • The first approach is fairly simple, but it lacks flexibility because it does not support enough colors.
  • In my opinion, the second approach, which uses the greyscale asset, is the winner, as it allows for a large number of color slots while keeping the concept simple.

SVG filters are GPU-accelerated in modern browsers. The filters used in these demos are particularly efficient because they operate per pixel and do not require sampling neighbouring pixels.

Top comments (0)