Architecture
Kempt is a Manifest V3 extension with no framework and no background worker. esbuild bundles the TypeScript, and tests run on node:test.
Principles
Section titled “Principles”- CSS before JS. A visual change is a stylesheet rule. JS runs only to add elements, move elements, read page data or change navigation.
- The core knows nothing about features. A feature is one folder plus one line in a list.
- Every feature can be turned off and fully reversed. When it’s off, none of its CSS applies and none of its JS runs.
- No global observers and no polling. Route changes come from YouTube’s
yt-navigate-finishevent. DOM waits disconnect as soon as they resolve or time out (10 s by default); pass the narrowest root you can. - YouTube selectors have one home each. JS selectors live in
src/core/selectors.ts; CSS selectors live in the feature’sstyle.css.
Two scripts, two worlds
Section titled “Two scripts, two worlds”The manifest injects two scripts into every YouTube page, both at document_start:
| Script | World | Does |
|---|---|---|
content.js + content.css |
Isolated | Reads settings, sets <html> attributes, routes, runs features |
main-world.js |
YouTube’s page | Answers bridge calls that need YouTube’s own objects |
Project layout
Section titled “Project layout”src/ content.ts entry: settings -> html attributes -> router -> runner main-world.ts entry in the page world: a 20-line dispatcher core/ feature.ts Feature and Option types, popup groups, the runner router.ts routeOf(), onRoute(), shortsId() settings.ts chrome.storage settings and their localStorage mirror bridge.ts call(): the isolated-world side of the page bridge dom.ts waitFor, keep, el, observe selectors.ts every YouTube selector used from JS icon.ts Figma icons from src/icons/ as CSS masks i18n.ts YouTube's UI language and per-feature string tables page/ youtube.ts page-world helpers: ytcfg, Innertube, walking YouTube's data core.ts page-world handlers shared by features (stamping, navigate) theme/tokens.css --kyt-* design tokens and overrides of YouTube's --yt-spec-* variables features/ index.ts the only list of features (also the popup order) page.ts the only list of page-world handlers <id>/index.ts the feature <id>/style.css its CSS <id>/page.ts optional: its page-world handlers sidebar/nav.ts YouTube's guide sections -> Kempt's sidebar layout (pure) timeline/dates.ts relative upload times and History day headers -> dated groups (pure) timeline/filter.ts Subscriptions toolbar: item kind and search match (pure) font/faces.ts generated by scripts/fetch-fonts.mjs popup/ renders itself from features/index.ts color.ts accent color picker: swatches, hue/saturation wheel, brightness, hextest/*.test.tsThe feature contract
Section titled “The feature contract”export interface Feature { id: string; // also the CSS gate: html[kyt-<id>] label: string; // shown in the popup hint?: string; // popup tooltip group: Group; // popup section: look, navigation, watch, feeds icon?: string; // file name in src/icons/ without .svg defaultOn: boolean; routes?: Route[]; // omit = every page options?: Record<string, Option>; // popup controls run?(ctx: Ctx): void | Promise<void>; // omit for CSS-only features}
export interface Ctx { signal: AbortSignal; // aborted when the feature turns off or leaves its routes option<T>(key: string): T; // live option value call: typeof call; // typed call into the page world}Rules for run():
- Pass
{ signal: ctx.signal }to every listener, observer and wait. - On abort, remove what you added and put back what you moved.
- To react to a new video on the watch page, listen for
kyt:navigatewith the same signal.
The runner
Section titled “The runner”On startup, on every settings change and on every route change:
for each feature: enabled = settings[id] ?? defaultOn html.toggleAttribute(`kyt-${id}`, enabled) // turns its CSS on or off for each option: boolean -> html[kyt-<id>-<key>] while true choice -> html[kyt-<id>-<key>="<value>"] cssVar -> html.style.setProperty(cssVar, value) while enabled shouldRun = enabled && run && (no routes || routes includes route) if running and not shouldRun: abort its controller if not running and shouldRun: new AbortController, run(ctx)One AbortController per running feature is the whole lifecycle. Every listener and observer the feature made hangs off its signal, so turning a feature off can’t leak.
CSS gating
Section titled “CSS gating”Every rule in a feature’s style.css starts with html[kyt-<id>]:
html[kyt-subscribe-red] ytd-subscribe-button-renderer button:not([subscribed]) { ... }All feature CSS ships as one content.css, which the manifest injects before YouTube paints. Toggling a feature flips one attribute, so nothing gets injected at runtime and nothing flashes. test/structure.test.ts fails the build if a rule is missing its gate.
chrome.storage answers asynchronously, too late for the first paint. So settings.ts mirrors the whole settings object to localStorage['kempt-yt'], and content.ts reads it synchronously at document_start. Storage stays the source of truth.
Router
Section titled “Router”routeOf(url) is a pure, unit-tested function: /watch is watch, /feed/subscriptions is subscriptions, and so on. The router listens to yt-navigate-finish, which YouTube fires on every in-page navigation, and to popstate. It then dispatches kyt:navigate with { route }. Shorts, search and playlists are other; shorts code uses shortsId().
DOM helpers
Section titled “DOM helpers”waitFor(sel, { root, signal, timeout = 10_000 }): Promise<Element | null>One MutationObserver on root only. It disconnects on a match, an abort or a timeout, and resolves null on abort or timeout (timeout: Infinity waits until abort). On timeout it logs kyt: "<selector>" not found after <ms>ms, the first warning when YouTube changes its markup.
keep(el, root, mount, signal, init?) // re-runs mount() when YouTube re-renders under root; removes el on abortel(tag, cls?, ...children) // createElement + class + childrenobserve(target, init, fn, signal) // MutationObserver, disconnected on abortObservers that repair YouTube’s re-renders write inside the observer callback itself. That runs before the next paint; a requestAnimationFrame delay would paint the unrepaired page for one frame, which shows as flicker.
Design tokens
Section titled “Design tokens”src/theme/tokens.css defines every color Kempt uses as a --kyt-* variable, with a light and a dark value. The accent feature points YouTube’s own --yt-spec-* variables at them. A custom accent is one variable on <html>, and every tint is derived from it with color-mix().