Skip to content

Theming

One set of design tokens (named colors, sizes, and radii) covers every surface: popup, sidepanel, options, welcome, and the on-page UIs. To rebrand, swap one color scale; no component edits needed.

The scales live in apps/extension/assets/tailwind.css (the @theme block). The stock Tailwind palette is disabled (--color-*: initial), so raw palette utilities (bg-blue-600, text-gray-500) don’t compile, and lint bans them too. Component code references intent, never hue:

scale role
neutral the only gray: surfaces, borders, text
accent brand + every primary action; swap this scale to rebrand
success paid/active states, confirmations
warning past-due, cautions
danger destructive actions, errors

To rebrand, replace the eleven --color-accent-* oklch values in the @theme block with your brand’s scale (Tailwind v4’s palette reference is a good source of ready-made scales):

/* apps/extension/assets/tailwind.css: swap these for your brand */
@theme {
--color-accent-50: oklch(0.97 0.014 254.604);
--color-accent-100: oklch(0.932 0.032 255.585);
/* … 200–900 … */
--color-accent-950: oklch(0.282 0.091 267.935);
}

Every button, link, ring, and wall across every surface follows.

The pairing convention for tinted chips and banners: {scale}-50 background / {scale}-800 text / {scale}-200 border in light mode; {scale}-950 / {scale}-200 / {scale}-800 in dark (see BroadcastBanner for the reference implementation).

Dark mode is class strategy (@custom-variant dark), driven by the settings store: auto follows the OS; light/dark override it. Style both themes at authoring time, dark: variants throughout, and check every new component in both themes before shipping.

Content-script shadow UIs get the theme class on their shadow wrapper, never from the host page: the host page’s classes must not decide your theme (mountShadowUi handles this).

  • InterVariable, bundled locally in assets/fonts/; no CDN fonts, per the remote-code hygiene rule. Weights 100–900 in one variable file.
  • Headings are semibold (set in the base layer); body is regular.
  • Numbers always get tabular-nums (prices, credit counts, timers) so digits don’t jiggle.
  • Scale in practice: text-base headings inside surfaces, text-sm body, text-xs secondary/meta. Popup surfaces are dense; avoid anything above text-lg outside the welcome/options pages.
radius used for
rounded-lg controls: buttons, inputs, selects
rounded-xl cards, panels, option rows
rounded-full pills and avatars only

Fades and small translates only, 150–200 ms, with a hard cap at 300 ms and no spring or bounce curves. Extension surfaces open and close constantly; motion that draws attention twice a minute is noise. The one sanctioned entrance: animate-in fade-in slide-in-from-bottom-4 on transient chrome (status bar, toasts).

4 px grid (the Tailwind default). Surfaces: p-4 sections, space-y-4 between blocks, gap-2/gap-3 inside rows. Popup min-width is min-w-90 (360 px); the options content column is max-w-xl.

Primitives come from @extensionstart/ui: Button, Card, Input, Badge, Skeleton, Dialog, Toast (CVA variants over Base UI). Never hand-roll a <button> or badge in app code; extend via className, merged with cn(). Focus styles are built into the primitives (focus-visible:outline-2 outline-accent-600); custom interactive elements must match.

Two practical notes:

  • New Tailwind class sources outside the extension app need an @source line in assets/tailwind.css (that’s how the workspace packages’ classes are picked up).
  • In shadow UIs, rem is converted to px at build (rem would resolve against the host page’s root font size); details in the UI-on-web-pages guide.