Site themes
The extension ships the 134 styles of
catppuccin/userstyles (MIT, its
licence in extension/userstyles/LICENSE) unchanged, and compiles them against
the live caelestia scheme instead of a Catppuccin flavour.
Why compile, not rewrite to CSS variables
The styles are LESS, and they do a lot with concrete colours at compile time:
fade(), darken(), mix(), red(), hue(), colours escaped into inline
SVG data URLs, and CSS filter chains per colour. A var() can’t be faded at
compile time or put inside a data URL, so a one-off conversion to variables
would break most of them. Compiling each style with the scheme’s real colours
keeps every one of them working. All 134 compile, in about 27 ms each
(measured 2026-09-27).
How a page gets themed
content.jsruns atdocument_startin every frame and asks the background for its theme, handing over the CSS it applied last time (none, at first).- The background builds the CSS: the scheme’s
--caelestia-*variables, plus every enabled style whose URL rules match the page. - A style is compiled the first time a matching page asks, then cached until the scheme or the accent changes.
- The background removes the old CSS from that frame and inserts the new
with
scripting.insertCSS, and the content script keeps what it got. - When
schemeorsettingschange in storage, every content script asks again. That’s the live switch.
The content script, not the background, remembers what was injected into its
frame. The background can be suspended between two switches, and a restarted
background has no record of what it inserted; removeCSS needs the exact CSS
that was inserted.
insertCSS rather than a <style> element, because a page’s
Content-Security-Policy can block inline styles but not an extension’s
inserted sheet.
Overrides
The styles are applied as upstream wrote them; anything beyond that is the
user’s, in settings.sites.overrides[id] (see
Theming your own sites):
domains, when (a CSS selector) and css. The extension ships no fixes of
its own; recipes live in the docs.
domainsare extradomainrules. A style matched through one, or throughwhen, contributes all its@-moz-documentbodies (cssFor(…, all)), because its own rules don’t name that page.whenneeds the parsed document, whichdocument_startdoesn’t have. The content script asks once atdocument_startas always, then again atDOMContentLoaded, sending the ids whose selector matches asdetected. So a page themed only throughwhenshows the unthemed page until its DOM is parsed.cssgoes after the style’s CSS, so it wins at equal specificity.
Sites of the user’s own are settings.sites.custom ({ id, name }, ids
custom-…), with an override under the same id. They have no style, so
their domains and when are the only ways they match, and their css is
all they add, after every bundled style’s.
The palette swap
userstyles.ts libFor() rewrites the vendored lib.less before compiling:
- The
@catppuccinmap gets the scheme’s colours under both@latteand@mocha. caelestia’s scheme carries all 26 Catppuccin colour names. @accentbecomes a scheme colour,primaryby default (Settings, Websites, Accent).lightFlavoranddarkFlavorare both set tolattewhen the scheme is light andmochawhen it’s dark. The flavour then only decides the styles’if(@flavor = latte, …)branches, which pick between colours made for a light or a dark palette.
@-moz-document
Stylus understands @-moz-document; web pages don’t. So after compiling, the
extension splits the CSS into its @-moz-document blocks and injects only the
bodies of the blocks whose rules match the page, with Stylus’s semantics
(domain matches subdomains, regexp must match the whole URL). Some styles
build their rules with LESS (syncthing’s come from an @var), so the vendoring
script reads them from a compiled copy into index.json; that tells the
background which styles a page wants before it compiles any.
Checking the styles
tests/sites-firefox.mjs loads a page for every style in a headless Firefox
with the extension, logged out, and reports whether the background behind the
middle of the page became a scheme colour. It’s slow and depends on the live
sites, so it’s run by hand (see Development). A
failure is a lead, not a verdict: a landing page that differs from the
logged-in site, a page whose middle is an image, or a style that only covers
some pages all show up as failures.
Known gaps
- The
@<colour>-filtervalues are Catppuccin’s own precomputed filter chains, so the icons a few styles tint with a CSS filter come out in Catppuccin’s shade rather than the scheme’s. Fixing it needs a solver that finds a filter chain for an arbitrary colour. - A style’s own options (
@varcheckboxes like YouTube’s logo) are fixed at their defaults. There’s no UI for them yet.