[{"data":1,"prerenderedAt":106},["ShallowReactive",2],{"blog-extract-brand-colors-from-modern-css":3},{"post":4,"related":49,"previous":93,"next":104},{"slug":5,"title":6,"description":7,"excerpt":8,"date":9,"updated":10,"status":11,"author":12,"authorTitle":10,"category":13,"categoryName":14,"categoryAccent":15,"tags":16,"image":20,"cover":21,"imageAlt":10,"featured":22,"readingMinutes":23,"wordCount":24,"headings":25,"html":46,"canonicalUrl":10,"sourceFile":47,"path":48},"extract-brand-colors-from-modern-css","How to Extract Brand Colors From Modern CSS","Regex over stylesheets used to be a fine way to collect a site's colors. CSS Color 4 and 5 ended that. Here is what to read instead of source text.","For about fifteen years you could collect every color a website used with three regular expressions. One for hex, one for rgb() and rgba(), one for hsl() and hsla(). Between them they matched…","2026-08-21T09:00:00.000Z","","published","Colorize","engineering","Engineering","#22D3EE",[17,18,19],"css","brand colors","color theory","/social/blog/extract-brand-colors-from-modern-css.png","/social/blog/extract-brand-colors-from-modern-css-cover.png",false,5,1168,[26,30,33,37,40,43],{"id":27,"text":28,"level":29},"what-actually-changed","What actually changed",2,{"id":31,"text":32,"level":29},"read-computed-values-not-source-text","Read computed values, not source text",{"id":34,"text":35,"level":36},"the-custom-property-trap","The custom property trap",3,{"id":38,"text":39,"level":29},"computed-values-are-not-all-srgb-any-more","Computed values are not all sRGB any more",{"id":41,"text":42,"level":29},"both-color-schemes-are-real","Both color schemes are real",{"id":44,"text":45,"level":29},"what-to-collect","What to collect","\u003Cp>For about fifteen years you could collect every color a website used with three regular expressions. One for hex, one for \u003Ccode>rgb()\u003C/code> and \u003Ccode>rgba()\u003C/code>, one for \u003Ccode>hsl()\u003C/code> and \u003Ccode>hsla()\u003C/code>. Between them they matched essentially every color anyone wrote.\u003C/p>\n\u003Cp>That stopped being true. A stylesheet written this year can define its entire palette without producing a single match against any of those patterns:\u003C/p>\n\u003Cdiv class=\"code-block\">\u003Cspan class=\"code-lang\" aria-hidden=\"true\">css\u003C/span>\u003Cpre>\u003Ccode class=\"language-css\">@property --brand {\n  syntax: '&lt;color&gt;';\n  inherits: true;\n  initial-value: oklch(0.55 0.21 264);\n}\n\n:root {\n  --brand-hover: oklch(from var(--brand) calc(l - 0.08) c h);\n  --brand-subtle: color-mix(in oklch, var(--brand) 12%, white);\n  --surface: light-dark(oklch(0.99 0 0), oklch(0.16 0.01 264));\n  --accent: color(display-p3 0.9 0.35 0.1);\n}\u003C/code>\u003C/pre>\u003C/div>\n\u003Cp>Five colors, zero hex codes, zero \u003Ccode>rgb()\u003C/code>. A scraper built on pattern matching reports that this site has no colors.\u003C/p>\n\u003Ch2 id=\"what-actually-changed\">\u003Ca class=\"heading-anchor\" href=\"#what-actually-changed\" aria-label=\"Link to this section\">#\u003C/a>What actually changed\u003C/h2>\n\u003Cp>Three specs landed in browsers in close succession, and each broke a different assumption.\u003C/p>\n\u003Cp>\u003Cstrong>CSS Color 4\u003C/strong> added color spaces beyond sRGB. \u003Ccode>lab()\u003C/code>, \u003Ccode>lch()\u003C/code>, \u003Ccode>oklab()\u003C/code>, \u003Ccode>oklch()\u003C/code> and \u003Ccode>hwb()\u003C/code> are all now ordinary things to write, along with the \u003Ccode>color()\u003C/code> function for addressing a specific space such as \u003Ccode>display-p3\u003C/code> or \u003Ccode>rec2020\u003C/code>. \u003Ccode>oklch()\u003C/code> in particular has become the default choice for design systems, because its lightness axis is perceptually even, which makes generating a tint and shade ramp a matter of stepping one number.\u003C/p>\n\u003Cp>\u003Cstrong>CSS Color 5\u003C/strong> added colors that are computed from other colors. \u003Ccode>color-mix()\u003C/code> blends two colors in a named space. Relative color syntax, the \u003Ccode>oklch(from var(--brand) ...)\u003C/code> form, takes an existing color apart into components you can do arithmetic on. Neither has a literal value anywhere in the source.\u003C/p>\n\u003Cp>\u003Cstrong>\u003Ccode>light-dark()\u003C/code>\u003C/strong> puts two values in one declaration and picks between them based on the resolved \u003Ccode>color-scheme\u003C/code>. The source text contains both. The page shows one.\u003C/p>\n\u003Cp>The through line is that the relationship between what is written in the stylesheet and what appears on screen went from &quot;identical&quot; to &quot;the output of a small program&quot;. Pattern matching reads the program's source. You want its result.\u003C/p>\n\u003Ch2 id=\"read-computed-values-not-source-text\">\u003Ca class=\"heading-anchor\" href=\"#read-computed-values-not-source-text\" aria-label=\"Link to this section\">#\u003C/a>Read computed values, not source text\u003C/h2>\n\u003Cp>The browser already contains a complete, correct implementation of all of this. \u003Ccode>getComputedStyle\u003C/code> gives you the answer after cascade, inheritance, \u003Ccode>var()\u003C/code> substitution, relative color resolution, \u003Ccode>color-mix()\u003C/code> evaluation and \u003Ccode>light-dark()\u003C/code> selection have all been applied.\u003C/p>\n\u003Cdiv class=\"code-block\">\u003Cspan class=\"code-lang\" aria-hidden=\"true\">js\u003C/span>\u003Cpre>\u003Ccode class=\"language-js\">const el = document.querySelector('.cta')\ngetComputedStyle(el).backgroundColor\n// &quot;oklch(0.55 0.21 264)&quot;\u003C/code>\u003C/pre>\u003C/div>\n\u003Cp>This is not the trick it sounds like. It is the only way to get a correct answer for a declaration involving \u003Ccode>var()\u003C/code>, because resolving \u003Ccode>var()\u003C/code> yourself means reimplementing the cascade, and the cascade is not a thing you want to reimplement.\u003C/p>\n\u003Ch3 id=\"the-custom-property-trap\">\u003Ca class=\"heading-anchor\" href=\"#the-custom-property-trap\" aria-label=\"Link to this section\">#\u003C/a>The custom property trap\u003C/h3>\n\u003Cp>The obvious next step is to enumerate the custom properties directly, since a design system's palette is usually declared as tokens on \u003Ccode>:root\u003C/code>:\u003C/p>\n\u003Cdiv class=\"code-block\">\u003Cspan class=\"code-lang\" aria-hidden=\"true\">js\u003C/span>\u003Cpre>\u003Ccode class=\"language-js\">const root = getComputedStyle(document.documentElement)\nArray.from(root)\n  .filter((name) =&gt; name.startsWith('--'))\n  .map((name) =&gt; [name, root.getPropertyValue(name).trim()])\u003C/code>\u003C/pre>\u003C/div>\n\u003Cp>This works, and it is worth doing, because token names carry meaning that a bare hex code does not. But it has a sharp edge that catches people. An \u003Cstrong>unregistered\u003C/strong> custom property is substitution only. Its computed value is the token stream you wrote, not a resolved color. So this:\u003C/p>\n\u003Cdiv class=\"code-block\">\u003Cspan class=\"code-lang\" aria-hidden=\"true\">css\u003C/span>\u003Cpre>\u003Ccode class=\"language-css\">:root {\n  --brand-subtle: color-mix(in oklch, var(--brand) 12%, white);\n}\u003C/code>\u003C/pre>\u003C/div>\n\u003Cp>reads back as the literal string \u003Ccode>color-mix(in oklch, var(--brand) 12%, white)\u003C/code>. You have moved the problem, not solved it.\u003C/p>\n\u003Cp>Registering the property with \u003Ccode>@property\u003C/code> and \u003Ccode>syntax: '&lt;color&gt;'\u003C/code> changes that: registered properties compute to a real value, and read back resolved. But you do not control whether the site you are reading did that.\u003C/p>\n\u003Cp>The reliable way to resolve an arbitrary token is to make the browser use it as a color and then read what it used. Put it on a throwaway element:\u003C/p>\n\u003Cdiv class=\"code-block\">\u003Cspan class=\"code-lang\" aria-hidden=\"true\">js\u003C/span>\u003Cpre>\u003Ccode class=\"language-js\">const resolveColor = (value) =&gt; {\n  const probe = document.createElement('div')\n  probe.style.color = 'rgb(1, 2, 3)' // sentinel: survives an invalid value\n  probe.style.color = value\n  document.documentElement.append(probe)\n  const resolved = getComputedStyle(probe).color\n  probe.remove()\n  return resolved === 'rgb(1, 2, 3)' ? null : resolved\n}\n\nresolveColor('var(--brand-subtle)') // an actual color, whatever the token held\u003C/code>\u003C/pre>\u003C/div>\n\u003Cp>The sentinel matters. Assigning an invalid value to a style property is a no-op, so without a known starting value you cannot tell a failed assignment from a successful one that happened to produce the same color.\u003C/p>\n\u003Ch2 id=\"computed-values-are-not-all-srgb-any-more\">\u003Ca class=\"heading-anchor\" href=\"#computed-values-are-not-all-srgb-any-more\" aria-label=\"Link to this section\">#\u003C/a>Computed values are not all sRGB any more\u003C/h2>\n\u003Cp>The old comfortable assumption was that \u003Ccode>getComputedStyle\u003C/code> always hands back \u003Ccode>rgb(r, g, b)\u003C/code>. That was true while every color was an sRGB color. It is not true now. A color authored in a wider space serializes in that space, so you will receive strings like:\u003C/p>\n\u003Cdiv class=\"code-block\">\u003Cpre>\u003Ccode>oklch(0.55 0.21 264)\ncolor(display-p3 0.9 0.35 0.1)\nlab(52.2 40.1 59.9)\u003C/code>\u003C/pre>\u003C/div>\n\u003Cp>Two consequences follow. First, whatever parses your results needs to handle those forms, which means using a color library that implements CSS Color 4 rather than a hand-rolled \u003Ccode>rgb()\u003C/code> splitter.\u003C/p>\n\u003Cp>Second, and more awkward: some of those colors do not exist in sRGB. A saturated \u003Ccode>display-p3\u003C/code> red is outside the sRGB gamut, and there is no hex code that represents it. You have to decide what to do, and the two options are not equal.\u003C/p>\n\u003Cp>\u003Cstrong>Clipping\u003C/strong> each channel into range is what naive code does implicitly and it distorts hue, sometimes badly, because clamping one channel while the others stay put moves the color sideways rather than just desaturating it.\u003C/p>\n\u003Cp>\u003Cstrong>Gamut mapping\u003C/strong> as described in CSS Color 4 reduces chroma in OKLCH while holding lightness and hue, stepping down until the color fits. It is more work and it produces a color that still looks like the one you started with.\u003C/p>\n\u003Cp>If your output is hex, you are gamut mapping whether you admit it or not. Do it deliberately, and keep the original value alongside the mapped one so the information is not lost.\u003C/p>\n\u003Ch2 id=\"both-color-schemes-are-real\">\u003Ca class=\"heading-anchor\" href=\"#both-color-schemes-are-real\" aria-label=\"Link to this section\">#\u003C/a>Both color schemes are real\u003C/h2>\n\u003Cp>\u003Ccode>light-dark()\u003C/code> and \u003Ccode>prefers-color-scheme\u003C/code> mean a page has at least two palettes, and the one you capture depends on what your renderer emulates. Most headless setups default to light and do not mention it.\u003C/p>\n\u003Cp>If you are driving a browser, you can ask for each explicitly and capture twice:\u003C/p>\n\u003Cdiv class=\"code-block\">\u003Cspan class=\"code-lang\" aria-hidden=\"true\">js\u003C/span>\u003Cpre>\u003Ccode class=\"language-js\">await page.emulateMediaFeatures([\n  { name: 'prefers-color-scheme', value: 'dark' },\n])\u003C/code>\u003C/pre>\u003C/div>\n\u003Cp>Treat the two results as two labelled palettes rather than merging them. A brand's dark surface color is not a variant of its light surface color, it is a separate decision, and flattening them produces a palette containing both \u003Ccode>#FFFFFF\u003C/code> and \u003Ccode>#0B1220\u003C/code> with no explanation of why.\u003C/p>\n\u003Ch2 id=\"what-to-collect\">\u003Ca class=\"heading-anchor\" href=\"#what-to-collect\" aria-label=\"Link to this section\">#\u003C/a>What to collect\u003C/h2>\n\u003Cp>Given all of the above, a color pass over a live page that holds up in 2026 gathers four things:\u003C/p>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth scope=\"col\">Source\u003C/th>\u003Cth scope=\"col\">What it gives you\u003C/th>\u003Cth scope=\"col\">Why it matters\u003C/th>\u003C/tr>\u003C/thead>\u003Ctbody>\u003Ctr>\u003Ctd>Custom properties on \u003Ccode>:root\u003C/code>\u003C/td>\u003Ctd>Token names and values\u003C/td>\u003Ctd>Names describe a color's job\u003C/td>\u003C/tr>\u003Ctr>\u003Ctd>Computed styles on real elements\u003C/td>\u003Ctd>Colors as used, after everything resolves\u003C/td>\u003Ctd>The only trustworthy values\u003C/td>\u003C/tr>\u003Ctr>\u003Ctd>Both color schemes\u003C/td>\u003Ctd>Two labelled palettes\u003C/td>\u003Ctd>Neither one is the whole answer\u003C/td>\u003C/tr>\u003Ctr>\u003Ctd>Original color space\u003C/td>\u003Ctd>\u003Ccode>oklch\u003C/code>, \u003Ccode>display-p3\u003C/code> and friends\u003C/td>\u003Ctd>Hex silently discards this\u003C/td>\u003C/tr>\u003C/tbody>\u003C/table>\u003C/div>\n\u003Cp>Notice that &quot;the text of the stylesheet&quot; is not on the list. It is still worth reading for one narrow purpose, which is discovering token names that are declared but not currently in scope, such as a dark-only or logged-in-only theme block. As a source of color values it has been superseded.\u003C/p>\n\u003Caside class=\"callout\">\n  \u003Cstrong>The wider point.\u003C/strong> Every one of these features exists because\n  designers wanted to describe relationships between colors rather than list\n  them. A palette is now a system with rules, and a tool that reports a flat list\n  of hex codes is reporting the shadow the system casts, not the system.\n\u003C/aside>\n\u003Cp>Worth saying plainly: a color declared in CSS is still only part of a brand's palette. The colors in a logo are not in the stylesheet at all, and reading those means working from the image, which is a different problem with a different set of traps. \u003Ca href=\"/blog/extract-brand-colors-from-svg-logos\">How to Extract Brand Colors From SVG Logos\u003C/a> covers that side, and \u003Ca href=\"/blog/how-to-extract-brand-colors-from-any-website\">How to Extract Brand Colors From Any Website\u003C/a> covers when to reach for which.\u003C/p>","content/blog/extract-brand-colors-from-modern-css.md","/blog/extract-brand-colors-from-modern-css",[50,66,81],{"slug":51,"title":52,"description":53,"excerpt":54,"date":55,"updated":10,"status":11,"author":12,"authorTitle":10,"category":56,"categoryName":57,"categoryAccent":58,"tags":59,"image":61,"cover":62,"imageAlt":10,"featured":63,"readingMinutes":64,"path":65},"how-to-extract-brand-colors-from-any-website","How to Extract Brand Colors From Any Website","Four ways to pull the exact hex codes off a live site (DevTools, the CSS, a screenshot, and an extractor) and which one is right for each job.","You need the exact colors a site uses, and you need them as hex codes rather than as a hunch. Maybe you're building a pitch deck for a client, matching an integration to a partner's brand, or…","2026-08-23T09:00:00.000Z","guides","Guides","#818CF8",[18,60,17],"workflow","/social/blog/how-to-extract-brand-colors-from-any-website.png","/social/blog/how-to-extract-brand-colors-from-any-website-cover.png",true,4,"/blog/how-to-extract-brand-colors-from-any-website",{"slug":67,"title":68,"description":69,"excerpt":70,"date":71,"updated":10,"status":11,"author":12,"authorTitle":10,"category":72,"categoryName":73,"categoryAccent":74,"tags":75,"image":77,"cover":78,"imageAlt":10,"featured":22,"readingMinutes":79,"path":80},"extract-dominant-colors-from-a-logo","The Best Way to Extract Dominant Colors From a Logo","Median cut, octree and k-means were built to compress photographs. Point them at a logo and they return three whites and a gray. Here is what works instead.","Reach for a color extraction library and you will almost certainly get median cut, usually through ColorThief or one of its ports. Feed it a photograph and it does a genuinely good job. Feed it a logo…","2026-08-22T09:00:00.000Z","color-theory","Color Theory","#E879F9",[18,76,19],"logos","/social/blog/extract-dominant-colors-from-a-logo.png","/social/blog/extract-dominant-colors-from-a-logo-cover.png",6,"/blog/extract-dominant-colors-from-a-logo",{"slug":82,"title":83,"description":84,"excerpt":85,"date":86,"updated":10,"status":11,"author":12,"authorTitle":10,"category":72,"categoryName":73,"categoryAccent":74,"tags":87,"image":89,"cover":90,"imageAlt":10,"featured":22,"readingMinutes":91,"path":92},"accurate-brand-color-palettes","The Complete Guide to Accurate Brand Color Palettes","Near-duplicate shades are the most common flaw in an extracted palette. Here is how CIELAB and delta E collapse them into the colors a brand actually uses.","You point an extractor at a logo, ask for the brand palette, and get five colors back. Three of them are the same blue.","2026-08-19T09:00:00.000Z",[19,18,88],"palettes","/social/blog/accurate-brand-color-palettes.png","/social/blog/accurate-brand-color-palettes-cover.png",7,"/blog/accurate-brand-color-palettes",{"slug":94,"title":95,"description":96,"excerpt":97,"date":98,"updated":10,"status":11,"author":12,"authorTitle":10,"category":13,"categoryName":14,"categoryAccent":15,"tags":99,"image":101,"cover":102,"imageAlt":10,"featured":22,"readingMinutes":79,"path":103},"extract-brand-colors-from-svg-logos","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…","2026-08-20T09:00:00.000Z",[18,100,76],"svg","/social/blog/extract-brand-colors-from-svg-logos.png","/social/blog/extract-brand-colors-from-svg-logos-cover.png","/blog/extract-brand-colors-from-svg-logos",{"slug":67,"title":68,"description":69,"excerpt":70,"date":71,"updated":10,"status":11,"author":12,"authorTitle":10,"category":72,"categoryName":73,"categoryAccent":74,"tags":105,"image":77,"cover":78,"imageAlt":10,"featured":22,"readingMinutes":79,"path":80},[18,76,19],1787496473240]