Chrome themes are the easiest way to recolour the browser — and the most thoroughly misunderstood part of Chrome customisation. Almost every guide online shows the same ten pretty themes, explains how to click “Add to Chrome”, and stops exactly where it gets interesting.
This article does it differently. We didn’t research, we measured: downloaded 661 entries from the Chrome Web Store, unpacked them, took their manifests apart, and pushed the colours of 506 real themes through a WCAG contrast calculator. Then we built our own theme — and systematically tried to break it, to find out what Chrome actually validates.
Two results up front, and both are uncomfortable. First: in 36.3% of themes, at least one text-on-background combination falls below a contrast ratio of 3.0 — in 46 themes it is exactly 1.00, meaning literally invisible text. Second: a complete, working Chrome theme comes to 619 bytes. One single file. No build step, no framework, not a line of JavaScript.
The short version
| Question | Answer (measured 3 Sep 2026) |
|---|---|
| How many colours can a Chrome theme set? | 26 overwritable colour keys |
| For comparison: Firefox | 42 — Chrome gives noticeably fewer controls |
| How large is a theme? | Our complete theme: 619 bytes |
| Smallest theme in the corpus | 2,879 bytes (4 colours, no image) |
| Largest theme in the corpus | 27.3 MB — a factor of 9,470 |
| Do I need an image? | No — 14% of themes use none at all |
| Do I need programming skills? | No — one manifest.json, nothing else |
| Does a theme need permissions? | No — and that is precisely the security argument |
| How many themes have contrast problems? | 50.2% below AA, 36.3% below 3.0 |
| Themes with invisible text (contrast 1.00) | 46 |
| Does Chrome validate colour values? | No — only type and count, never the value |
| Is every “theme” search result a theme? | No — 23.4% are extensions |
manifest.json evaluated. Contrast calculated per WCAG 2.1 (relative luminance); alpha values were composited against the respective background rather than discarded. Colour keys counted from chrome/browser/themes/browser_theme_pack.cc and chrome/common/extensions/manifest_handlers/theme_handler.cc in chromium/chromium. Loading behaviour tested with Google Chrome 149.0.7827.102, Chrome for Testing 151.0.7922.34 and Chromium 152.0.7977.64.
What a Chrome theme actually is
A Chrome theme is an extension — technically speaking. Same format, same store, same directory. The only difference: instead of code, it contains a theme block in the manifest.
That sounds like a formality, but it is the most important sentence in this article. Because in Chromium’s source, theme is its own extension type:
"theme": {
"channel": "stable",
"extension_types": ["theme"]
}
A package is either a theme or an extension. It cannot be both. And from that follows the property that makes themes by far the safest category in the entire Web Store: a theme cannot request permissions. No storage, no tabs, no access to the pages you visit. It has no background process, no content script, no way to read anything at all.
We didn’t take that on faith, we counted. Of the 506 real themes in the corpus, exactly four declare anything permission-shaped at all — and in all four cases these are dead entries that Chrome simply ignores for a theme-type package. In practice: installing a theme means installing a colour table.

The finding nobody was looking for: one in four “themes” isn’t one
We assembled our corpus through theme searches in the store — “theme”, “dark theme”, “minimal theme”, “catppuccin”, “amoled”, 74 queries in total. Then we opened every package and checked whether there was a theme block in it at all.
Of 661 hits, 155 are not themes — 23.4%. They are ordinary extensions that surface in theme searches because they carry “Theme” or “Dark Mode” in their name.
That would be a curiosity if the difference were cosmetic. It isn’t. Together, these 155 extensions request exactly what a real theme technically cannot:
| Permission | Number of non-themes |
|---|---|
storage | 111 |
scripting | 43 |
activeTab | 39 |
tabs | 32 |
| Access to all websites | 31 |
alarms | 22 |
unlimitedStorage | 16 |
topSites | 13 |
31 search results for “theme” want to read every website you visit. Among them entries simply called “Dark Mode”, “High Contrast” or “night theme for web” — names that sound exactly like the harmless thing a theme would be.
Some of these are entirely legitimate: a dark-mode recolourer must be able to read pages in order to recolour them. That isn’t an accusation. The point is different: the search results page does not distinguish the two, and the store tile looks identical either way. A user searching for “dark theme” and clicking the first pretty result can go from “changes the colour of my tab bar” to “reads every page I open” in a single click, with nothing on the surface changing.
The reliable distinction needs no expertise: Chrome shows no permission prompt when installing a real theme. If one appears — “This extension can read and change your data on all websites” — it is not a theme, whatever the name says. If you want to dig deeper, the same information sits on the store page under “Privacy practices”.
If you want to know which browser you’re setting up in the first place, our comparison of open source web browsers covers the larger picture.
The 26 colours — and how we know it’s 26
Most guides count Chrome theme colours by copying from other guides. We looked in the source instead. browser_theme_pack.cc holds the authoritative table, with a remarkably honest comment underneath:
// Strings used by themes to identify colors in the JSON.
constexpr StringToIdTable<TP::OverwritableByUserThemeProperty>
kOverwritableColorTable[] = {
{"frame", TP::COLOR_FRAME_ACTIVE},
{"toolbar", TP::COLOR_TOOLBAR},
{"tab_text", TP::COLOR_TAB_FOREGROUND_ACTIVE_FRAME_ACTIVE},
// ... 23 more
// /!\ If you make any changes here, you must also increment
// kThemePackVersion above, or else themes will display incorrectly.
};
Counted out, that’s 26 overwritable colour keys. Next to it sits a second table with 11 colours Chrome computes itself and a theme cannot set — including the loading spinner and the window control buttons. On top of that: 6 tints, 15 image slots and 3 display properties.
For comparison: Firefox exposes 42 colour keys. Chrome gives markedly less control — our Firefox themes article shows the same architecture with noticeably more knobs.
What actually gets used in practice is far less again. Counted across all 506 themes:
| Colour key | used by | share |
|---|---|---|
toolbar | 502 | 99.2% |
frame | 496 | 98.0% |
tab_text | 494 | 97.6% |
bookmark_text | 475 | 93.9% |
tab_background_text | 462 | 91.3% |
ntp_background | 461 | 91.1% |
ntp_text | 454 | 89.7% |
button_background | 366 | 72.3% |
ntp_link | 307 | 60.7% |
frame_inactive | 161 | 31.8% |
omnibox_text | 133 | 26.3% |
background_tab_incognito_inactive | 32 | 6.3% |
The median is 9 colours per theme, the minimum 2, the maximum 42 — where the themes above 26 only get there by setting keys that don’t exist. More on that shortly.
Practical consequence: seven colours cover over 90% of what real themes do. If you’re building a theme, you don’t need 26 lines.
The experiment: what does Chrome actually validate?
This is where it gets interesting. We built 15 themes, each with one deliberately planted defect, and loaded each one individually into Chrome for Testing 151. Afterwards we inspected the profile to see whether the theme had really been registered as the active theme — not whether the browser had printed a success message.
| # | Manipulation | Result |
|---|---|---|
| C1 | Colour values far outside 0–255 ([999, -40, 300]) | ✅ accepted |
| C2 | Hex string instead of RGB list ("#1a1b26") | ❌ rejected |
| C3 | Invented colour keys (tolbar, totally_made_up) | ✅ accepted |
| C4 | Only two values instead of three ([26, 27]) | ❌ rejected |
| C5 | Floats instead of integers ([26.7, 27.2, 38.9]) | ❌ rejected |
| C6 | White on white — invisible text | ✅ accepted |
| C7 | Reference to a non-existent image file | ❌ rejected |
| C8 | Unknown image slot, file missing | ❌ rejected |
| C9 | Nonsense property values ("diagonal", 7) | ✅ accepted |
| C10 | Fourth alpha value | ✅ accepted (correct) |
| C11 | Unknown image slot, file present | ✅ accepted |
| C12 | Valid image slot, file present | ✅ accepted (correct) |
| C13 | Out-of-range tints + invented tint | ✅ accepted |
| C14 | Exclusively negative colour values | ✅ accepted |
| C15 | Alpha values above 1 and below 0 | ✅ accepted |
The pattern is unambiguous, and it’s spelled out in the source. When loading colours, theme_handler.cc checks exactly three things:
// Validate that the colors are RGB or RGBA lists.
if (!value.is_list()) { *error = errors::kInvalidThemeColors; return false; }
// There must be either 3 items (RGB), or 4 (RGBA).
if (!(color_list.size() == 3 || color_list.size() == 4)) { ... }
// The first three items (RGB), must be ints:
if (!(color_list[0].is_int() && ...)) { ... }
Is it a list? Does it have 3 or 4 entries? Are the first three integers? That was the entire validation. The value range is never checked. The key names are never compared against the table. Whether the result is readable interests no one.
The validation is a statement about shape, not about content. A theme with [999, -40, 300] loads cleanly; Chrome silently clamps the values to the valid range later. A theme with the typo tolbar loads just as cleanly — the line simply isn’t found when the colour table is assembled, and disappears without a sound.

One instructive special case was C8 versus C11. C8 (unknown image slot) was rejected — at first glance that looked like Chrome does validate image names after all. The obvious conclusion would have been wrong. C11 is the same unknown slot, only with an actually present file next to it — and it loads without complaint. So C8 didn’t fail because of the name, it failed because of the missing file. A test with two simultaneous defects doesn’t tell you which of the two produced the result. Only the second run separated them.
Worth noting is what Chrome does hard-fail on: missing image files (C7, C8). That is stricter than the VS Code packer vsce, which in our test of VS Code themes silently packaged a reference to a file that didn’t exist.
The proof in the wild: 21% of themes contain keys that don’t exist
If Chrome silently ignores unknown colour keys, then the store must contain themes doing exactly that — without anyone ever noticing. And that is precisely the case.
108 of the 506 themes (21.3%) set at least one colour Chrome doesn’t know. The most common:
| Invented/obsolete key | Themes |
|---|---|
ntp_section | 64 |
ntp_section_text | 45 |
ntp_section_link | 44 |
control_background | 33 |
ntp_link_underline | 27 |
ntp_section_link_underline | 19 |
ntp_attribution_text | 5 |
toolbar_top_separator | 4 |
This is neither coincidence nor pure sloppiness: ntp_section and relatives were once real keys for the old “New Tab” page. They were removed from Chrome, but nobody told the theme authors — because there is nothing that could tell them. No error, no warning, no store rejection. The line stays in the manifest, keeps looking right, and has done nothing for years.
On top of that: 5 themes with an invalid image slot, 6 with invented properties, and 3 with duplicate JSON keys — in “Green theme”, ntp_text and toolbar each appear twice in the same object with different values. JSON doesn’t formally define that unambiguously; in practice the last one wins. The author sees the first colour in the editor and the second one in the browser.
The measurement that matters: contrast
Now for the part that actually affects users. For every theme we computed the colour pairs that genuinely sit on top of each other in the browser — tab text on toolbar, bookmark text on toolbar, inactive tab text on frame, and the New Tab page colours. Calculated per WCAG 2.1, with alpha values composited against the background.
| Colour pair | measured | Median | below 4.5 (AA) | below 3.0 | Minimum |
|---|---|---|---|---|---|
| Tab text on toolbar | 490 | 9.27 | 21.2% | 49 | 1.00 |
| Bookmark text on toolbar | 474 | 8.50 | 24.3% | 63 | 1.00 |
| Inactive tab text on frame | 454 | 6.07 | 36.8% | 120 | 1.00 |
| NTP text on NTP background | 440 | 12.82 | 21.4% | 75 | 1.00 |
| NTP link on NTP background | 298 | 11.41 | 30.2% | 58 | 1.00 |
| Omnibox text on omnibox background | 124 | 10.76 | 6.5% | 1 | 2.40 |
Summarised across all pairs: 50.2% of themes have at least one pair below AA, 36.3% below 3.0 — and 46 themes have at least one pair at exactly 1.00.
A contrast ratio of 1.00 means: identical colour. The text isn’t hard to read, it is not there.

Because that is a strong claim, we recomputed the extreme cases by hand instead of trusting the script:
- “Acrylic Watercolor”:
toolbar=[255,255,255],tab_text=[255,255,255]. White on white. Store rating: 4.77 stars. - “Pink Leopard Hearts”:
toolbar,bookmark_textandtab_background_textare all three[231,92,173]. The entire bookmark text vanishes into the background. Rating: 4.82 stars across 55 votes. - “Jungle Gem – High Contrast Dark Theme”:
toolbar=[0,0,0],tab_text=[0,0,0]. Black on black — in a theme that carries “High Contrast” in its name.
That last case isn’t an outlier but a pattern we had already found with VS Code: “High Contrast” in the name is a promise without cover. Both themes in the corpus carrying that phrase fail — one at 1.00, the other at 2.19.
And the finding that stings most: 17 themes with contrast below 3.0 simultaneously hold a rating of 4.5 stars or better across at least 50 votes. Among them:
| Theme | worst pair | Rating | Votes |
|---|---|---|---|
| Retro Robots Theme | 1.00 | 4.80 | 3,513 |
| Black & white theme | 1.00 | 4.78 | 3,122 |
| Red Fox Snow Theme | 1.00 | 4.71 | 1,593 |
| Anime Angel Beats! Theme | 1.00 | 4.77 | 663 |
| Cute Pixel Pastel Easter Egg | 1.00 | 4.68 | 210 |
3,513 people rated a theme with invisible text an average of 4.8 out of 5. That is not a criticism of those people. It is the evidence that star ratings structurally cannot find this class of defect: a theme with unreadable inactive tab text looks great in the store screenshot, and in daily use you take the vanished label for a design decision rather than a bug.
Size: the factor of 368
The spread in the corpus is absurd. The smallest theme (“Baby Blue”) is 2,879 bytes and consists of four colours:
{
"frame": [181, 211, 252],
"frame_inactive": [227, 237, 252],
"toolbar": [210, 227, 252],
"omnibox_background": [255, 255, 255]
}
The largest (“Christmas decorations”) is 27.3 MB — a factor of 9,470. The difference has exactly one cause: images.
| Group | Count | Median size |
|---|---|---|
| Themes without any image | 71 (14.0%) | 3,913 bytes |
| Themes with an image | 435 (86.0%) | 1,440,405 bytes |
A factor of 368. A pure colour theme is, at the median, smaller than an average icon; an image theme is, at the median, larger than a music file. If you want to keep your theme small, you have exactly one decision to make — and you make it in the first minute.

Incidentally: 194 of the 506 themes (38.3%) still run on Manifest V2, and 18 have no manifest_version at all. For extensions that would be a death sentence by now — themes, however, are practically unaffected by the MV3 migration because they contain no code to migrate. A theme from 2012 works today, unchanged.
Building your own Chrome theme
This is the easiest part of the article. You need one folder and one file.
{
"manifest_version": 3,
"name": "GetMind Cosmic",
"version": "1.0",
"theme": {
"colors": {
"frame": [26, 27, 38],
"frame_inactive": [36, 37, 48],
"toolbar": [36, 40, 59],
"tab_text": [192, 202, 245],
"tab_background_text":[138, 145, 178],
"bookmark_text": [192, 202, 245],
"toolbar_text": [192, 202, 245],
"omnibox_background": [26, 27, 38],
"omnibox_text": [192, 202, 245],
"ntp_background": [26, 27, 38],
"ntp_text": [192, 202, 245],
"ntp_link": [122, 162, 247]
},
"properties": {
"ntp_logo_alternate": 1
}
}
}
That is the complete theme: 619 bytes, one file, twelve colours. No image, no code, no permission.
Installing:
- Open
chrome://extensions - Turn on Developer mode in the top right
- Load unpacked → select the folder
The command-line route --load-extension=/path appears in many guides. It no longer works in current Chrome versions. We tested it in Chrome 149 and the log says, verbatim:
--load-extension is not allowed in Google Chrome, ignoring.
The process starts anyway and exits with code 0 — there is no visible error whatsoever. The theme simply isn’t there. In Chrome for Testing and Chromium the flag still works, which is why all our loading tests run there.
Important note on the theme ID: we loaded the same manifest from two different folders and got two different IDs (kaofli… and kfdlee…). For an unpacked extension, Chrome derives the ID from the path, not from the contents. Move your theme, and as far as Chrome is concerned it’s a different theme.

Checking your own colours — because Chrome doesn’t
Since Chrome demonstrably doesn’t check the content, you have to do it yourself. The calculation is short enough to write in a few lines:
def _lin(c):
c = c / 255.0
return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
def luminance(rgb):
r, g, b = rgb[:3]
return 0.2126 * _lin(r) + 0.7152 * _lin(g) + 0.0722 * _lin(b)
def contrast(foreground, background):
a, b = luminance(foreground), luminance(background)
if a < b:
a, b = b, a
return (a + 0.05) / (b + 0.05)
# Our theme, checked:
contrast([192, 202, 245], [36, 40, 59]) # tab text on toolbar → 9.49
contrast([138, 145, 178], [26, 27, 38]) # inactive tab on frame → 6.81
contrast([122, 162, 247], [26, 27, 38]) # NTP link on NTP → 7.29
We validated this implementation against known reference values before trusting it with 506 themes: white on black must yield exactly 21.00, #767676 on white exactly 4.54 (the official AA borderline case). Both match to the second decimal. A measuring instrument you haven’t held against a known value only measures your own conviction.
The three pairs you must check — nothing more is needed:
tab_textagainsttoolbar(active tab)tab_background_textagainstframe(inactive tabs — the failure rate is highest here at 36.8%)ntp_textagainstntp_background(New Tab)
Target value 4.5 for normal text. If you land below it, you should at least know — exactly as with contrast in code editors, where the same blind spot happened to hit the comment colour.
Chromium forks: same manifest, different ID
We additionally loaded our unmodified theme into Chromium 152. It works without any adjustment — same manifest.json, same colours, no compatibility switch. The same applies to Brave, Edge, Vivaldi and Opera, which all sit on the same codebase.
Two caveats worth knowing:
- The ID differs between browsers, because it is path-dependent. Irrelevant to users, not to scripts.
- The store differs. Edge has its own add-ons store, and Vivaldi ships its own themes using a completely different (and considerably more powerful) mechanism. A Chrome theme runs everywhere, but in some forks it isn’t the obvious solution.
If you’re interested in the differences between browsers beyond colours, our open source browser comparison covers engine, privacy and update policy.
Publishing to the Chrome Web Store
If you only want to use your theme yourself, you’re done above. For publishing:
- Create a developer account — a one-off fee of 5 US dollars.
- Zip the folder. No special tooling needed; the store accepts an ordinary ZIP.
- Include an icon. Formally optional, practically indispensable — otherwise the store shows a placeholder.
- Upload and wait for review. Themes are usually reviewed faster than extensions, because there is no code to analyse.
What the review does not do: it checks neither contrast nor colour keys. The 108 themes with invalid keys and the 46 with invisible text all passed that review.
For unpacked extensions the ID comes from the path — for uploaded ones it comes from the signing key the store assigns. That ID is permanent and cannot be changed.
Limits: what a Chrome theme cannot do
This is where the honesty of many guides ends. A theme can:
- colour the window frame, the tab strip and the toolbar
- set background images in frame, toolbar and New Tab page
- define the colours of the New Tab page
A theme cannot:
- change websites (that requires a content script — so, a real extension with page access)
- recolour the context menu, the settings pages or
chrome://pages - change fonts or spacing
- affect corner radii, tab shapes or layout
- force a dark surface in dialogs
That last point is the most common disappointment: a dark theme makes the tab bar dark, but many menus stay light because they follow the operating system mode. And Firefox’s userChrome.css, which lets you rebuild practically any detail in Firefox, has no counterpart in Chrome. If you genuinely want to intervene deeply, Chrome is the wrong address — one of the few points where Firefox clearly allows more.
Frequently asked questions
Is a Chrome theme dangerous?
A real theme is the safest category in the store: it technically cannot request permissions, has no background process and no content script. Of 506 themes examined, practically none had genuine data access. Caution is warranted with search results that merely sound like themes — 23.4% of our hits were extensions, 31 of which wanted to read every website.
How do I tell whether a “theme” really is a theme?
By the permission prompt: a real theme triggers none. If Chrome asks for access to websites during installation, it is an extension — regardless of the name. Second check: the “Privacy practices” section on the store page.
How many colours can a Chrome theme set?
26 overwritable colour keys, plus 6 tints, 15 image slots and 3 display properties. A further 11 colours are computed by Chrome itself and cannot be overwritten. In practice, a typical theme uses only 9 of them.
Does Chrome check whether my theme colours are readable?
No. Chrome only checks whether a colour value is a list of 3 or 4 entries and whether the first three are integers. Value range, key names and readability are never checked. Our white-on-white test loaded without complaint, as did values like [999, -40, 300].
Why doesn’t one of my theme colours work?
Most likely the key is spelled differently than you think — a typo, or a key from an older Chrome version. Chrome ignores unknown keys silently, with no error message. In 21.3% of store themes at least one such dead key sits in the manifest, most commonly ntp_section.
How large does a Chrome theme have to be?
As small as you like. Our complete theme is 619 bytes, the smallest in the store 2,879 bytes. Themes without an image have a median size of 3,913 bytes, themes with one 1.44 MB — a factor of 368.
Do I need programming skills for a Chrome theme?
No. A theme contains no code at all, just a manifest.json with colour values. If you can read JSON you can build a theme; there is no build process.
Why doesn’t --load-extension work for me?
Because Google Chrome now ignores that flag — the log says verbatim “—load-extension is not allowed in Google Chrome”. The process still starts without an error. Use chrome://extensions → Developer mode → “Load unpacked” instead, or reach for Chrome for Testing or Chromium.
Does my Chrome theme also run in Brave, Edge or Vivaldi?
Yes, unchanged — all are Chromium-based. We verified the same manifest in Chromium 152. The extension ID differs, though, and Edge and Vivaldi have their own stores and partly their own theme mechanisms.
Can a theme change how websites look?
No. A theme colours the browser interface only. Websites require a content script — and therefore a real extension with access to the pages you visit. That is precisely the difference that became visible in the 31 search results with full access.
What is Chrome’s equivalent of userChrome.css?
There isn’t one. Firefox allows deep intervention in the browser interface via userChrome.css; Chrome offers only the 26 colour keys and the image slots. If you want to change tab shapes, spacing or fonts, a Chrome theme will not get you there.
Conclusion
Chrome themes are simultaneously simpler and less well validated than their reputation suggests.
Simpler, because a complete theme consists of a single file of 619 bytes. No build, no framework, no code. If you have half an hour, you have your own theme.
Less well validated, because Chrome checks only the shape of the values and never their content. That isn’t speculation — it is written in theme_handler.cc and reproduced across 15 loading tests: white on white, values beyond 255, invented keys — all accepted. The consequences sit in the store: 21.3% of themes carry dead colour keys, 36.3% have at least one barely readable colour pair, and 46 have one that is entirely invisible.
And because the most popular of these themes hold excellent ratings — 4.80 stars across 3,513 votes for a theme with invisible text — it is clear that neither the automated review nor the community finds this class of defect. The only reviewer left is the author.
Three contrast calculations, ten lines of code, once before publishing. That is the entire difference between a theme that looks pretty and one you can actually use.
