YouTube quirks
Short comments in the source point here. Each entry is a constraint YouTube imposes, found by breaking something. Read this before you “fix” something that looks odd, and add an entry when a bug takes a real debugging round.
Page and scripts
Section titled “Page and scripts”- A content script can’t read Polymer’s
.dataon YouTube’s elements (Firefox Xray wrappers). Stamping, guide data and the comment count run in the page world. See Page bridge. - Trusted Types blocks
innerHTML. Build nodes withel()fromsrc/core/dom.ts. ytcfgexists atdocument_start, butLOGGED_INis set later.signedInwaits for a value; reading it early makes signed-in features skip without an error.--yt-spec-*variables are not defined at the page root. Feature CSS uses Kempt’s--kyt-*tokens.- Feature-local CSS variables must not reuse a theme token name. A popup rule read
var(--kyt-bg, transparent), but--kyt-bgis the theme background inherited from<html>, so every unmapped menu row drew a black square. - YouTube’s newer lockup classes are camelCase (
ytBadgeShapeThumbnailLive). A BEM-style[class*="thumbnail-live"]matches nothing. - Firefox 128 has no iterator helpers (they arrived in 131). Use arrays.
setTimeout(fn, Infinity)fires at once.waitForhas its own no-timeout path fortimeout: Infinity.
Re-renders and flicker
Section titled “Re-renders and flicker”- Signed in, YouTube redraws the masthead
#buttons(when Notifications arrives) and the watch action row (when Ask arrives, and on the Subscribe animation), and drops nodes it didn’t create. Anything Kempt inserts there goes throughkeep()insrc/core/dom.ts. keepandstamprun synchronously in the observer callback, before paint. ArequestAnimationFramedelay paints the re-rendered element unstamped for one frame.- Stamp channel tabs by URL slug, never by title. The mini guide’s root exists early, but its entries render later.
Masthead and search
Section titled “Masthead and search”- YouTube stacks its own hover layers: 10% white on the guide’s
#endpoint, a 1px rim on active rows, 20% white on the notifications button. Clear each on its own element. A broad#buttons > *:hoverrule also kills Kempt’s own Settings hover. - Old
yt-icon-buttonhovers throughyt-interaction; button-shape components hover differently. Cover both. - The search box gains a second magnifier on focus (Kempt hides it). The grey mic circle is the
#voice-search-buttonwrapper. - Setting
input.valuedoesn’t open search suggestions. Only real key events do (ext.mjs --type).
Sidebar
Section titled “Sidebar”- The selected background goes on
ytd-guide-entry-renderer[active]itself (YouTube’s squircle), not on the innertp-yt-paper-item. ytd-guide-rendererdoesn’t exist on watch pages, or when the sidebar starts collapsed (under about 1312px), until the drawer opens.guide()then falls back to one guide request per page load.- Signed-in guide data links only to
/feed/playlists, so the Playlists dropdown fetches the list on first open. The guide data has no Settings, Help or Send feedback entries. - Entries behind YouTube’s “Show more” (Memberships, for example) have no drawn icon.
ytIconwaits up to about 5 s for the hiddenyt-iconto render. - Masking flattens multicolour brand icons (Premium, Music, Kids). Kempt detects a fill other than
currentColorand draws those as<img>. grid-template-rows: 0frto1fron an empty list animates to the wrong height. Fill the list before opening it.- The Shorts entry has a
serviceEndpoint(seedlessreelWatchEndpoint, url/shorts/), not anavigationEndpoint. Sent throughyt-navigateit opens/shorts/with no sequence: nothing plays and scrolling does nothing. YouTube’s guide entryonTapstarts it (the URL becomes/shorts/<id>), sonavigateborrows that handler from a detachedytd-guide-entry-renderer.
- Inlining icons in
content.jsbroke: esbuild’sdataurlloader mangled"and#(icons drew as squares), and base64 grew the bundle. Icons are copied todist/iconsand loaded asweb_accessible_resourcesthrough CSSmask-image. - Masks use alpha only, so rewriting SVG
filltocurrentColordoes nothing useful, and it fills shapes meant to stay empty. - Card menu icon types, signed in: Not interested is
HIDE, Don’t recommend channel isREMOVE.
Watch tabs
Section titled “Watch tabs”- YouTube’s
updatePanelsLocation(theater, fullscreen, column changes) expects#panels,#chat-container,#playlistand#inline-panels(plus#shopping-timely-shelfand#persistent-panel-containerin#below) to be the first children of their parent, in order. Anything inserted inside that run makes it re-insert every panel on each call, which re-renders comments and reloads the chat. Kempt’s nodes go after that run (YT_PANELS), andstyle.cssrestores the visual order with flexorder. - Fullscreen with live chat expanded moves that panel run out of
#secondary-innerinto#panels-full-bleed-containerbeside the player. Anything anchored after the run (the watch tabs) moves with it, sowatch-tabshides its bar and boxes underytd-watch-flexy[fullscreen]and lets the chat show whatever tab is selected. #panelsmoves between#secondary-innerand#belowwith the layout, so every anchor goes through#panels. That makes the one-column layout work the same.- Videos (
#related) and Live chat (#chat-container) are only shown or hidden, so the chat iframe never reloads. The description and comments move into boxes that go back on abort. - Hidden tabs are
display: none. Comments and related videos load lazily when their tab is first shown. YouTube’s continuation loaders use anIntersectionObserverwith a null root, so infinite scroll still works inside Kempt’s scroll boxes; addoverscroll-behavior: contain. - Re-inserting the comments box re-renders every loaded comment, so Kempt moves nothing that is already in place.
- The comment count comes from the comments engagement panel’s
contextualInfo(short form, “2.4M”). It is stale for about a second after an in-page navigation. No panel means comments are off, so the tab shows its icon only. - The description carries an inline
min-width(about 381px) that Kempt overrides. On streams,#teaser-carouseland#comment-teaserare hidden while Watch tabs is on. - Theater toggles often leave
#primary’s width unchanged, so aResizeObserverwon’t fire. Re-align after thetheaterattribute changes, insiderequestAnimationFrame. - Theater with live chat:
updateChatLocationmoves#chat-containerinto#columnsand sets[fixed-panels](chatposition: fixedunder the masthead,#columnspadded by the sidebar width). Logged-out live chat to test with:/@LofiGirl/live. - Ask (AI) is a flexible item of the action row. The row drops flexible items from the end into the ⋯ menu as it narrows, so
frontmoves it first (dropped last),offremoves it from the row and the menu, andrestoreputs YouTube’s data back. Reassigningdatare-stamps the row. Ask’s item haspanelIdentifier: 'PAyouchat'or theSPARKicon. - HTML lowercases attribute names: observe
kyt-watch-tabs-aias, not-aiAs. - Live chat’s close button sits inside its same-origin iframe, out of reach of CSS. The iframe reloads with each stream, so the handler re-runs on every
load(which does not bubble: use capture). - Signed in, YouTube keeps a 6px right margin after the Share and Save icons, so Kempt’s
margin: 0needs!important.
Comment sort
Section titled “Comment sort”The hidden #sort-menu items still work when clicked. Find them by index, because the list re-renders, and sync the chips from aria-selected.
Progress bar
Section titled “Progress bar”#movie_player can hold two .ytp-progress-bar: the real one in .ytp-chrome-bottom, and a hidden 0x0 spare that comes first in DOM order. A bare .ytp-progress-bar selector matches the spare. Scope to #movie_player .ytp-chrome-bottom .ytp-progress-bar. The Home hover preview (#inline-preview-player) is a second player with no bar.
Timeline
Section titled “Timeline”- YouTube matches the Subscriptions grid’s children to its data by index whenever the row count changes (sidebar toggle, resize). Any Kempt node between YouTube’s items shifts every index, and YouTube then moves every card in the feed (a freeze of about a second). Kempt’s toolbar and headers sit after all of YouTube’s children and flex
orderdraws them in place. - Hiding items with CSS never shrinks YouTube’s work: infinite scroll keys off the loading row’s visibility. While filtering, most items are hidden, so the loading row never leaves the screen and the feed would load to its end.
style.csshides that row unless more matches are wanted. - YouTube orders streams by end time but labels them by start time (“Streamed 21 hr ago” between 14 and 15 hr). Grouping therefore only moves back in time: an item that looks newer stays in the current group, and so does one Kempt can’t read.
- Seconds to days give the upload day (YouTube rounds down, so “13 days ago” is still one day). Weeks, months and years only give a range, so they group by YouTube’s own wording.
- Feed ages are compact in the text (“hace 11 m” is months) and long in the
aria-label, so Kempt parses the label. The age regex is built fromIntl’s words, which match YouTube’s long form in the ten languages checked: “vor 2 Wochen”, “hace 2 semanas”, “2 週間前”, “2주 전”.
Home’s Shorts shelf draws only elementsPerRow of its items (one row, no Show more). YouTube resets that count on every width change and reflects it to the elements-per-row attribute, so Kempt sets it again on each change and on shelves the grid adds later.
Resizing a long rich grid is slow on plain YouTube too: each width change rewrites items-per-row, is-in-first-column and is-link-card-full-width on every loaded item, which caused frames of up to 2 s with about 900 Subscriptions items (scripts/resize-diag.js). Layout itself is cheap (a forced relayout of 450 items took 1 to 3 ms), so the cost is in YouTube’s own scripts, and CSS cannot reduce it. Rule out YouTube with the extension disabled before blaming a feature.
Firefox sizes the toolbar popup on its first layout. Rows render synchronously with defaults, so the panel gets its full height; input waits for storage.sync, and the list is inert until it answers. A popup that waited for storage opened near-empty and closed.