Engineering
How to Extract Brand Colors From SVG Logos
An SVG logo is text, so reading its colors should be trivial. Six things about real-world SVGs make it anything but, starting with currentColor.

An SVG is a text file with the colors written in it. Compared to sampling pixels out of a PNG, reading fill="#1F6FEB" out of some markup should be the easy case. In practice SVG logos are the single most common source of wrong brand colors, and the reasons are specific and fixable.
Here are the six that account for nearly all of it.
#1. currentColor renders as black
This is the big one. A large share of modern logos, especially anything that came out of an icon system or a component library, are authored like this:
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" d="M4 16 L16 4 L28 16 L16 28 Z" />
</svg>currentColor is not a color. It is a reference to the computed color property of the element the SVG is sitting inside. On the live page it inherits the brand color from a CSS rule several levels up the tree. Pull the file out on its own and hand it to a rasterizer, and there is no inherited color to resolve against, so it falls back to the initial value: black.
The result is a monochrome palette, confidently returned, for a logo that is bright blue on screen. It is worse than a failure because it looks like a success.
The fix is to detect it rather than to try to render through it:
const isCssDependent = (svg) =>
/currentcolor/i.test(svg) ||
/fill\s*=\s*["']inherit["']/i.test(svg) ||
/\bfill\s*=\s*["']\s*["']/i.test(svg)Once detected you have two honest options. Skip that candidate for color purposes and fall back to another asset, such as the favicon or a raster version of the same mark. Or resolve the color from the live page, by reading the computed color of the element the SVG was found in before you detach it. What you must not do is rasterize it and report the result.
#2. Gradient stops cover almost no pixels
Gradients are declared once in <defs> and referenced by ID:
<defs>
<linearGradient id="g">
<stop offset="0%" stop-color="#7C3AED" />
<stop offset="100%" stop-color="#22D3EE" />
</linearGradient>
</defs>
<circle cx="16" cy="16" r="12" fill="url(#g)" />The two colors a designer would name are #7C3AED and #22D3EE. Rasterize this and count pixels, and neither of them wins anything. Every pixel in that circle is a different interpolated blend, the endpoints exist on a single row each, and the most frequent value is some purple-cyan midpoint that appears in no brand guide anywhere.
Pixel frequency analysis structurally cannot find a gradient's endpoints. The markup can, trivially, which is the argument for parsing the text as well as rasterizing rather than choosing between them.
#3. Colors hide in five different places
If you are scanning markup, scanning for fill= is not enough. Colors in real SVGs appear as:
- Presentation attributes:
fill,stroke,stop-color,flood-color,lighting-color - Inline styles:
style="fill: #1F6FEB; stroke: rgb(0 0 0)" - A
<style>block inside the SVG, with class selectors applied to elements - CSS custom properties defined in that same block and referenced by
var() - Nested
<svg>or<image>elements carrying a raster payload as a data URI
The first two are a straightforward pair of regexes over attribute and style forms. The third and fourth need at least a shallow understanding of the embedded stylesheet. The fifth is a raster image wearing an SVG hat, and the only way to read it is to decode the data URI and treat it as the image it is.
Whatever you scan for, you also need to reject the values that are not colors: none, transparent, inherit, currentColor, and anything starting with url(, which is a paint server reference rather than a color.
#4. Not every declared color is a visible color
This is where naive markup parsing goes wrong in the opposite direction from naive rasterization. These elements all carry fills that never reach a viewer:
<mask id="m"><rect fill="#FFFFFF" width="32" height="32" /></mask>
<clipPath id="c"><path fill="#FF0000" d="..." /></clipPath>
<path fill="#00FF00" opacity="0" d="..." />
<g display="none"><path fill="#FF00FF" d="..." /></g>Inside a <mask>, color is not color, it is a luminance channel controlling transparency. The white rectangle is saying "show everything", not "the brand is white". A <clipPath> child's fill is ignored entirely; only its geometry is used. And elements at zero opacity or display: none are frequently leftovers from the designer's working file, exported and forgotten.
A markup pass that counts all of these hands back colors from a file that never displayed them. This is the strongest argument for reconciling the two methods: markup parsing finds colors that rasterizing misses, and rasterizing confirms that a color parsed from markup actually appears on screen. Colors found by both are the ones you trust most.
#5. Rasterizing is full of small traps
When you do rasterize, several things silently produce a blank or wrong image.
Missing namespace. Markup lifted out of a page with outerHTML often has no xmlns attribute, because the HTML parser supplied the SVG namespace implicitly and it was never serialized. Almost every standalone rasterizer treats a namespace free <svg> as an unknown element and returns an empty canvas. Add it back before handing the string over:
const ensureNamespace = (svg) =>
/\sxmlns\s*=/.test(svg)
? svg
: svg.replace(/^<svg\b/i, '<svg xmlns="http://www.w3.org/2000/svg"')No intrinsic size. An SVG with only a viewBox and no width or height has no intrinsic dimensions. Rasterizers disagree about what to do: some default to 100x100, some to the viewBox, some fail. Set an explicit render size instead of finding out which one you have.
Renderer differences. librsvg, resvg and a headless browser do not implement the same subset. Filters, <foreignObject> and some blend modes are the usual disagreements. If you rasterize server side and compare against what a browser shows, expect the mismatches to cluster in those features.
External references. <use href="sprite.svg#logo"> and <image href="..."> point outside the file. Detached from its origin those resolve to nothing, and you get a correctly rendered picture of an empty box.
#6. Dark mode variants live in the same file
The neat trick of shipping one logo that adapts to the page theme is now common, and it means a single file contains two palettes:
<style>
.mark { fill: #0B1220; }
@media (prefers-color-scheme: dark) {
.mark { fill: #F8FAFC; }
}
</style>Both fills are real brand colors in the sense that both get shown to real users. Which one you extract depends entirely on the color scheme your renderer emulates, and most default to light without telling you. If the distinction matters for your use case, emulate both and keep the results labelled rather than merging them into one undifferentiated list.
#Putting it together
The approach that survives all six is not markup parsing or rasterization but both, with the results reconciled:
- Screen the file first. If it depends on inherited color, do not use it as a color source. Resolve from the page context or move to the next candidate.
- Parse the markup for attributes, inline styles and gradient stops, skipping anything inside masks, clip paths, hidden elements and paint server references.
- Rasterize at a fixed size with the namespace repaired, and count pixel frequencies while ignoring transparent pixels.
- Reconcile. Markup supplies the gradient endpoints and small accents that pixel counting cannot see. Pixels supply the evidence of what is actually visible and in what proportion.
- Deduplicate perceptually. Both passes produce near-identical variants of the same color, and both feed the same cluster. Merge them in a perceptually uniform space rather than in RGB, for the reasons in The Complete Guide to Accurate Brand Color Palettes.
A note on hex parsing while you are writing the markup pass: SVG accepts four hex forms, #RGB, #RGBA, #RRGGBB and #RRGGBBAA, plus rgb(), hsl(), their alpha variants, and the 148 CSS named colors. A regex that only handles six digit hex will quietly skip a meaningful fraction of real logos, and #0FA is not an unusual thing to find in a hand-optimized file.
The payoff for getting this right is that SVG stops being the awkward case and becomes the best one. It is the only format where the brand's actual chosen values are present as declared numbers rather than inferred from a grid of pixels. You can see the results on real companies in the brand color directory.
Do you want API access?
Access our API to integrate color, brand & screenshot extraction into your app:
Brand Logos
Site Assets & Screenshot
Color Extraction & Grouping
Categorization & Company Details


