Styling guide

Synced from STYLING.md in the Better Lyrics repository when this site is built.

1. Introduction to CSS and Better Lyrics

CSS (Cascading Style Sheets) is a styling language used to describe the presentation of a document written in HTML or XML. It allows you to control the layout, colors, fonts, and other visual aspects of web pages.

The Better Lyrics CSS files are designed to enhance the lyrics viewing experience on YouTube Music™. They modify the appearance of the lyrics display, add animations, and adjust the layout to create a more immersive and user-friendly experience.

The extension lets you modify the CSS in real-time, so you can see the changes immediately.

If you’re new to CSS, don’t worry! This guide will walk you through the main components of the Better Lyrics CSS and explain how they work.

2. Understanding the CSS Structure

The Better Lyrics styling system consists of several modular CSS files, organized into directories:

  1. blyrics/ - Core lyrics styling, animations, and visual effects
    • index.css: The @import list the extension loads; everything below reaches the page through it
    • variables.css: Global custom properties
    • lyrics.css: Main lyrics container and line styles, translations, romanization
    • instrumental.css: Instrumental break styles
    • components.css: UI components like the loader, footer, and buttons
    • misc.css: Utility classes, and the --noto-sans-universal font stack
    • modal.css: In-page modal dialogs
    • responsive.css: Media queries for responsive design
    • picture-in-picture.css: The floating lyrics window
  2. ytmusic/ - YouTube Music interface modifications and layout adjustments
    • variables.css: YouTube Music specific variables
    • general.css: General interface overrides
    • fullscreen.css: Fullscreen mode styles
    • mobile.css: Mobile-specific adjustments
  3. themesong.css - Compatibility styles for the ThemeSong browser extension

Three of those are build outputs rather than source files. variables.css, lyrics.css and instrumental.css ship inside the @braccato/core package and are copied to blyrics/ when the extension is built, because they style the lyrics the renderer draws rather than the page around them. Edit them in that package; everything else under blyrics/ and ytmusic/ is source, in public/css/.

None of this changes anything a theme selects on. The class names, the custom properties and the served paths are all the same as before, so a theme written against the old layout keeps working.

Each file is organized into logical sections:

  1. Global variables (custom properties)
  2. Main container styles
  3. Individual lyric line styles
  4. Animation definitions
  5. YouTube Music layout modifications
  6. Responsive design rules
  7. Special styles for fullscreen mode

Each section uses CSS selectors to target specific HTML elements and apply styles to them.

3. Custom Properties (CSS Variables)

In blyrics/variables.css (shipped by @braccato/core), you’ll see a :root selector with custom properties that define the visual theme and behavior of Better Lyrics:

:root {
  --blyrics-ui-text-color: var(--blyrics-text-color, color(display-p3 1 1 1 / 1));
  --blyrics-glow-color: var(--blyrics-highlight-color, color(display-p3 1 1 1 / 0.5));
  --blyrics-default-font-family: Satoshi, var(--noto-sans-universal, "Noto Sans"), Avenir, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Open Sans, Helvetica Neue, sans-serif;
  /* ... more variables ... */
}

These custom properties allow for easy customization of colors, sizes, and other properties throughout the stylesheet.

Colors

VariableDefault ValueDescription
--blyrics-ui-text-colorcolor(display-p3 1 1 1 / 1) (white)Color of non-lyric UI text
--blyrics-glow-colorcolor(display-p3 1 1 1 / 0.5) (semi-transparent white)Color of glow effect
--blyrics-error-colorcolor(display-p3 0.992 0.882 0.882) (slightly red off-white)Color used in error conditions
--blyrics-lyric-active-colorcolor(display-p3 1 1 1 / var(--blyrics-active-opacity, 1))Color of active lyrics
--blyrics-lyric-inactive-colorcolor(from var(--blyrics-text-color, color(display-p3 1 1 1)) display-p3 r g b / var(--blyrics-inactive-opacity, 0.3))Color of inactive lyrics
--blyrics-footer-bg-colorhsla(0, 0%, 100%, 0.1)Background color for footer elements
--blyrics-footer-border-colorhsla(0, 0%, 100%, 0.1)Border color for footer elements
--blyrics-footer-text-color#aaaText color in footer
--blyrics-footer-link-color#fffLink color in footer
--blyrics-discord-hover-color#5865f2 (Discord Blurple)Hover color for Discord button

Typography

VariableDefault ValueDescription
--blyrics-font-family¹Unset; falls back to --blyrics-default-font-familyExplicit font family override for lyrics
--blyrics-font-size3remFont size for lyrics
--blyrics-font-weight700Font weight for lyrics
--blyrics-line-height1.333Line height for lyrics
--blyrics-translated-font-size2remFont size of translated/romanized lyrics
--blyrics-translated-font-weight600Font weight of translated/romanized lyrics
--blyrics-translated-font-familyUnset; uses --blyrics-font-family or the local default stackFont family of translated/romanized lyrics
--blyrics-translated-colorcolor(display-p3 1 1 1 / var(--blyrics-translated-opacity, 0.6))Color of translated/romanized lyrics
--blyrics-footer-font-familyRoboto, Noto Naskh Arabic UI, Arial, sans-serifFont family of footer
--blyrics-footer-font-size14pxFont size of footer
--blyrics-footer-font-weight400Font weight of footer
--noto-sans-universal²OmittedA family of NotoSans fonts covering a large majority of langauges used.

¹To add a custom web-font, use @import. It must be placed at the very top of your theme.

²The extension resolves this fallback at each lyric and translation’s lang boundary, selecting the appropriate Japanese, Korean, Simplified Chinese, Taiwan, or Hong Kong Noto subset. Keep the default font overrides unset to use this behavior. When adding a custom family with the Noto fallback, declare it at these boundaries rather than at :root, where CSS would resolve the fallback before the text language is known:

.blyrics--line,
.blyrics--translated,
.blyrics--romanized {
  --blyrics-font-family: "My Custom Font", var(--noto-sans-universal), sans-serif;
}

Animations

JavaScript controls animation timing with the Web Animations API, but the visual values are CSS variables.

VariableDefault ValueDescription
--blyrics-loader-transition-duration0.6sDuration of loader enter/exit transitions
--blyrics-loader-transition-easingcubic-bezier(0.22, 1, 0.36, 1)Easing curve for loader enter/exit transitions
--blyrics-scale-transition-duration0.166sDuration of lyric line scale enter/exit
--blyrics-line-enter-transform-fromscale(var(--blyrics-scale))Line enter start transform
--blyrics-line-enter-transform-toscale(var(--blyrics-active-scale))Line enter end transform
--blyrics-line-exit-transform-fromscale(var(--blyrics-active-scale))Line exit start transform
--blyrics-line-exit-transform-toscale(var(--blyrics-scale))Line exit end transform
--blyrics-line-enter-easingeaseLine enter easing
--blyrics-line-exit-easingeaseLine exit easing
--blyrics-lyric-highlight-fade-in-duration0.33sLine-synced highlight fade-in duration; rich-sync word-timed highlights become visible instantly
--blyrics-lyric-highlight-fade-out-duration0.5sHighlight fade-out duration
--blyrics-lyric-highlight-fade-in-easingeaseLine-synced highlight fade-in easing
--blyrics-lyric-highlight-fade-out-easingeaseHighlight fade-out easing
--blyrics-highlight-swipe-easinglinearRich-sync swipe easing
--blyrics-highlight-swipe-start-from-0.2Swipe gradient start value before animation
--blyrics-highlight-swipe-end-from-0.1Swipe gradient end value before animation
--blyrics-highlight-swipe-start-to1.4Swipe gradient start value after animation
--blyrics-highlight-swipe-end-to1.5Swipe gradient end value after animation
--blyrics-highlight-glow-radius-from0.8remGlow blur radius at the start of the pulse; color stays var(--blyrics-glow-color) and resolves per word
--blyrics-highlight-glow-radius-to0Glow blur radius at the end of the pulse
--blyrics-highlight-glow-filter-from(unset)Optional full override of the glow start filter; when set, its color resolves once at the container (one color for all words)
--blyrics-highlight-glow-filter-to(unset)Optional full override of the glow end filter
--blyrics-highlight-glow-duration-ratio1.2Glow duration multiplier relative to word duration
--blyrics-highlight-glow-min-duration1.2sMinimum glow duration
--blyrics-highlight-glow-easingeaseGlow easing
--blyrics-wobble-duration1sWord wobble duration
--blyrics-word-wobble-transform-fromscaleX(1)Word wobble start transform
--blyrics-word-wobble-transform-peaktranslateX(0.05em) scaleX(1.025)Word wobble peak transform
--blyrics-word-wobble-transform-settletranslateX(0) scaleX(1)Word wobble settle transform
--blyrics-word-wobble-transform-toscaleX(1)Word wobble end transform
--blyrics-word-wobble-peak-offset0.125Word wobble peak keyframe offset
--blyrics-word-wobble-settle-offset0.75Word wobble settle keyframe offset
--blyrics-word-wobble-easingeaseWord wobble animation easing
--blyrics-word-wobble-peak-easingease-in-outWord wobble peak keyframe easing
--blyrics-word-wobble-end-easingease-outWord wobble end keyframe easing
--blyrics-letter-wave-transformtranslateY(-0.05em)Letter-wave crest lift (experimental, on by default; disable with blyrics-letter-wave = false)
--blyrics-letter-wave-settletranslateY(-0.02em)Resting lift the letter eases back to after the crest
--blyrics-letter-wave-emphasis-scale1.11Per-letter scale swell on data-long-word words at the crest; 1 disables it
--blyrics-letter-wave-duration0.9sPer-letter float duration
--blyrics-letter-wave-rise-easingease-in-outEasing on the rise to the crest
--blyrics-letter-wave-fall-easingease-outEasing on the settle back to the resting lift
--blyrics-instrumental-fill-fade-duration150msInstrumental note fill opacity duration
--blyrics-instrumental-fill-fade-easingeaseInstrumental note fill opacity easing
--blyrics-instrumental-fill-transform-fromtranslateY(78%)Instrumental fill travel start
--blyrics-instrumental-fill-transform-totranslateY(-4%)Instrumental fill travel end
--blyrics-instrumental-fill-easinglinearInstrumental fill travel easing
--blyrics-instrumental-wave-transform-fromscaleY(1.2)Instrumental wave start transform
--blyrics-instrumental-wave-transform-toscaleY(0.0001)Instrumental wave end transform
--blyrics-instrumental-wave-easingease-inInstrumental wave easing
--blyrics-instrumental-wave-path-highpath("M -4 3 Q 1 2 5 3 ...")Wave surface at the top of its oscillation
--blyrics-instrumental-wave-path-lowpath("M -4 3 Q 1 4 5 3 ...")Wave surface at the bottom of its oscillation
--blyrics-instrumental-wave-oscillation-duration1.25sDuration of each instrumental wave surface oscillation loop
--blyrics-instrumental-wave-oscillation-easingease-in-outEasing for the instrumental wave surface oscillation
--blyrics-line-scroll-duration750msPer-line scroll-triggered translate animation duration
--blyrics-line-scroll-above-durationcalc(750ms + log(var(--blyrics-line-scroll-abs-relative-index) + 1) * 10ms)Duration for lines above the active line; can use line-scroll input variables
--blyrics-line-scroll-below-durationcalc(750ms + log(var(--blyrics-line-scroll-abs-relative-index) + 1, 2.71828) * 80ms + var(--blyrics-line-scroll-abs-relative-index) * 20ms)Duration for lines below the active line; can use line-scroll input variables
--blyrics-line-scroll-timing-functionSame as --blyrics-lyric-scroll-timing-functionPer-line scroll-triggered translate animation easing
--blyrics-line-scroll-start-easing--blyrics-line-scroll-timing-functionEasing attached to the start keyframe
--blyrics-line-scroll-end-easinglinearEasing attached to the end keyframe
--blyrics-line-scroll-relative-indexSet by JSRelative line index during line-scroll animation; active line is 0, below lines are positive
--blyrics-line-scroll-abs-relative-indexSet by JSAbsolute value of --blyrics-line-scroll-relative-index
--blyrics-line-scroll-sideSet by JSDirection-aware side used for side-specific knobs: above, active, or below
--blyrics-line-scroll-delta-pxSet by JSSigned scroll delta for the autoscroll that triggered the animation
--blyrics-line-scroll-distance-pxSet by JSAbsolute scroll distance for the autoscroll that triggered the animation
--blyrics-line-scroll-translate-y-start--blyrics-line-scroll-delta-pxShared per-line scroll animation start Y offset
--blyrics-line-scroll-translate-y-end0pxShared per-line scroll animation end Y offset
--blyrics-line-scroll-above-translate-y-startShared start valueStart Y offset for lines above the active line
--blyrics-line-scroll-below-translate-y-startShared start valueStart Y offset for lines below the active line
--blyrics-line-scroll-above-start-easingShared start easingStart keyframe easing for lines above the active line
--blyrics-line-scroll-below-start-easingShared start easingStart keyframe easing for lines below the active line
--blyrics-timing-offset0.115sOffsets lyrics highlighting for synced lyrics
--blyrics-richsync-timing-offset0.150sOffsets highlighting for richsynced lyrics
--blyrics-scroll-timing-offset0.5sOffsets the scroll time after other timing offsets

Animation effect flags use 1/0 values:

VariableDefault ValueDescription
--blyrics-animate-line-scale1Enable line scale animation
--blyrics-animate-word-wobble1Enable word wobble animation
--blyrics-animate-highlight-swipe1Enable rich-sync gradient swipe
--blyrics-animate-highlight-glow1Enable highlight glow
--blyrics-animate-highlight-fade1Enable animated line-synced highlight fade and highlight fade-out; rich-sync word-timed fade-in is always instant
--blyrics-animate-scroll1Enable smooth WAAPI scroll animation
--blyrics-animate-instrumental1Enable instrumental fill travel, wave flattening, and wave oscillation

The default CSS includes a prefers-reduced-motion: reduce block that disables the vestibular triggers (zoom-on-active line scale, translateX word wobble, and instrumental fill / wave motion) and flattens the active/inactive scale delta to 1. Smooth scroll remains enabled as the position indicator for synced lyrics, but the engine suppresses side-specific line-scroll differential effects so every visible line uses the shared scroll duration, easing, and offsets. The karaoke gradient swipe on richsync lyrics and the highlight glow stay on because they are gradient / drop-shadow fills rather than movement; and the line-synced highlight fade-in / highlight fade-out are opacity transitions, not motion.

Scroll smoothing uses per-line translate animations. When the lyrics pane autoscrolls, JS finds the lyric lines visible in the previous or current viewport and sets input variables on those lines: --blyrics-line-scroll-relative-index (0 active, positive below, negative above), --blyrics-line-scroll-abs-relative-index, --blyrics-line-scroll-side, --blyrics-line-scroll-delta-px, and --blyrics-line-scroll-distance-px. Visible lyric lines also receive inline will-change: transform, translate so text rasterization does not shift when a scroll animation starts.

Side-specific knobs are relative to scroll direction. On a downward scroll, lines above the active line use above settings and lines below use below settings. On an upward scroll, those sides swap so the animation can mirror the movement direction. The numeric --blyrics-line-scroll-relative-index does not swap; it always describes real line position, with negative values above the active line and positive values below it.

Configure line-scroll style values with comment-based knobs, not :root declarations. JS copies those values onto each animated line before resolving the Web Animation, so formulas that reference per-line variables resolve in the correct line scope. The resolved keyframes are snapshotted before the animation starts, which lets later scrolls update the same line variables without changing animations that are already running.

The default two-keyframe animation starts each visible line at the signed scroll delta and animates it back to 0. Scrolls have no duration-based gate; the default per-line visual tail uses 750ms plus a side-specific relative-index delay. Lines above the active lyric get a subtle logarithmic tail. Lines below get a stronger logarithmic tail plus a small linear term, so farther trailing lines keep finishing progressively later during larger jumps instead of flattening at a fixed cap.

/*
blyrics-line-scroll-translate-y-start = var(--blyrics-line-scroll-delta-px);
blyrics-line-scroll-translate-y-end = 0px;
blyrics-line-scroll-duration = 750ms;
blyrics-line-scroll-start-easing = var(--blyrics-line-scroll-timing-function);
blyrics-line-scroll-below-duration = calc(
    750ms + log(var(--blyrics-line-scroll-abs-relative-index) + 1, 2.71828) * 80ms +
      var(--blyrics-line-scroll-abs-relative-index) * 20ms
  );
blyrics-line-scroll-above-duration = calc(
    750ms + log(var(--blyrics-line-scroll-abs-relative-index) + 1) * 10ms
  );
*/

The engine animates the individual translate property instead of transform, so this effect can run alongside the existing line-scale transform animation. Per-line scroll effects can overlap: a new scroll starts as soon as an ungrouped lyric reaches its scroll time, even while earlier line animations are running. Overlapping scroll effects are composed with additive Web Animations (composite: "add"), so this behavior expects modern browser support for additive element.animate() effects.

Layout

VariableDefault ValueDescription
--blyrics-padding2remStandard padding
--blyrics-margin2remStandard margin
--blyrics-border-radius1000remStandard border radius
--blyrics-padding-top(dynamic)Extra top padding for always-scrollable lyrics (calculated automatically)
--blyrics-padding-bottom(dynamic)Extra bottom padding for always-scrollable lyrics (calculated automatically)
--blyrics-panel-size 50%Size of lyrics panel (not fullscreen, audio only)
--blyrics-video-panel-size30%Size of lyrics panel (not fullscreen, video mode)
--blyrics-fullscreen-panel-size66%Size of lyrics panel (fullscreen, audio only)
--blyrics-fullscreen-video-panel-size25%Size of lyrics panel (fullscreen, video mode)

Effects

VariableDefault ValueDescription
--blyrics-blur-amount30pxAmount of blur applied to elements for visual effects
--blyrics-scale0.95Scale factor applied to inactive elements
--blyrics-active-scale1Scale factor applied to active elements
--blyrics-error-opacity0.33Opacity value used for error messages
--blyrics-background-blur100pxAmount of blur applied to background elements for depth
--blyrics-background-saturate2Saturation multiplier for background elements to enhance vibrancy

Lyric Transition Properties

VariableDefault ValueDescription
--blyrics-lyric-scroll-timing-functioncubic-bezier(0.86, 0, 0.2, 1)Timing function for scrolling lyric transitions

--blyrics-lyric-scroll-duration and its --blyrics-lyric-transition-duration alias have been removed, along with the old container transform transition. Use the line-scroll duration knobs to control visual motion; early-consider is independent of animation duration. Replace any explicit references to the removed variables with a duration or your own custom property. If a line duration is missing, invalid or resolves to a nonpositive value, the engine uses an internal 750ms fallback.

Gradient Stops

--blyrics-gradient-stops:
  transparent 0%, rgba(0, 0, 0, 0.013) 8.1%,
  rgba(0, 0, 0, 0.049) 15.5%, rgba(0, 0, 0, 0.104) 22.5%,
  rgba(0, 0, 0, 0.175) 29%, rgba(0, 0, 0, 0.259) 35.3%,
  rgba(0, 0, 0, 0.352) 41.2%, rgba(0, 0, 0, 0.45) 47.1%,
  rgba(0, 0, 0, 0.55) 52.9%, rgba(0, 0, 0, 0.648) 58.8%,
  rgba(0, 0, 0, 0.741) 64.7%, rgba(0, 0, 0, 0.825) 71%,
  rgba(0, 0, 0, 0.896) 77.5%, rgba(0, 0, 0, 0.951) 84.5%,
  rgba(0, 0, 0, 0.987) 91.9%, #000 100%;

This variable defines a sophisticated gradient used for creating smooth visual transitions in fullscreen mode.

Additional Configuration Options (Knobs)

Knobs are tweakable parameters that can be configured by creating special comments in your themes. Inside any comment, anywhere in your theme, you can set the value of special keys using the syntax:

<key>=<value>;

An example may look something like this:

/*
blyrics-disable-richsync = false;
blyrics-line-synced-animation-delay = 50; (in ms)
blyrics-debug-renderer=false;
blyrics-debug-animation-timing=false;
blyrics-target-scroll-pos-ratio = 0.37;
blyrics-swipe-lead-ratio = 0.1;
blyrics-swipe-duration-ratio = 1.6;
*/

If a value isn’t specified, default is used.

The following options are avalible:

KeyDefault ValueDescription
blyrics-disable-richsyncfalseSet to true to render richsynced lyrics through the line-synced path instead, including zero-duration .blyrics-line-synced-word spans and line-synced fade-in.
blyrics-line-synced-animation-delay50For non-richsynced lyrics, this value controls the delay each word gets when highlighting (in ms).
blyrics-lyric-ending-threshold-s0.5Controls the time (in seconds) before a lyric line is finished that we consider it completed for scrolling purposes.
blyrics-early-scroll-consider-s0.54Controls how far into the future (in seconds) we should look for lines to group together for scrolling purposes.
blyrics-debug-rendererfalseSet to true to enable the debug renderer.
blyrics-debug-animation-timingfalseSet to true to log WAAPI lyric animation timing samples, learned offsets, and timing cleanup events.
blyrics-target-scroll-pos-ratio0.37Position on the screen lyrics should be at. 0.5 means the selected lyric will be in the middle of the screen, 0 means top, 1 means bottom.
blyrics-swipe-lead-ratio0.1Rich-sync swipe starts this fraction of word duration before the word time.
blyrics-swipe-duration-ratio1.6Rich-sync swipe lasts this multiple of word duration.
blyrics-long-word-threshold1500Duration threshold (in ms) above which words get data-long-word="true". Useful for glow effects on held notes.
blyrics-letter-wavetrueExperimental, on by default. Split every word into per-letter spans and float each letter up as it is sung, layered on top of the word wobble. Tune with the --blyrics-letter-wave-* variables; words past blyrics-long-word-threshold also get a per-letter scale swell. Disable with blyrics-letter-wave = false.
blyrics-hide-instrumental-onlyfalseTreat “[Instrumental Only]” as no lyrics (enables fullscreen effect).
blyrics-hide-creditsfalseSet to true to never build the songwriter credits line. See Songwriter Credits.
blyrics-passive-scroll-enabledtrueEnable/disable unsynced lyrics auto-scroll entirely. Overrides the user setting when set to false.
blyrics-passive-scroll-seconds-per-line3.5For unsynced lyrics auto-scroll: seconds spent scrolling per lyric line. Controls overall scroll speed.
blyrics-passive-scroll-bottom-pause-s1.5For unsynced lyrics auto-scroll: seconds to pause at the bottom before scrolling back to top.
blyrics-passive-scroll-reset-duration-s0.6For unsynced lyrics auto-scroll: seconds for the scroll-back-to-top animation.
blyrics-passive-scroll-top-pause-s0.8For unsynced lyrics auto-scroll: seconds to pause at the top before scrolling down again.
blyrics-line-scroll-duration750msPer-line scroll animation duration.
blyrics-line-scroll-above-durationcalc(750ms + log(var(--blyrics-line-scroll-abs-relative-index) + 1) * 10ms)Duration for visible lines above the active line.
blyrics-line-scroll-active-durationInherits line-scroll durationDuration for the active line.
blyrics-line-scroll-below-durationcalc(750ms + log(var(--blyrics-line-scroll-abs-relative-index) + 1, 2.71828) * 80ms + var(--blyrics-line-scroll-abs-relative-index) * 20ms)Duration for visible lines below the active line.
blyrics-line-scroll-timing-functionvar(--blyrics-lyric-scroll-timing-function)Shared line-scroll easing.
blyrics-line-scroll-start-easingvar(--blyrics-line-scroll-timing-function)Easing attached to the start keyframe.
blyrics-line-scroll-end-easinglinearEasing attached to the end keyframe.
blyrics-line-scroll-above-start-easingInherits shared start easingStart keyframe easing for visible lines above the active line.
blyrics-line-scroll-active-start-easingInherits shared start easingStart keyframe easing for the active line.
blyrics-line-scroll-below-start-easingInherits shared start easingStart keyframe easing for visible lines below the active line.
blyrics-line-scroll-above-end-easingInherits shared end easingEnd keyframe easing for visible lines above the active line.
blyrics-line-scroll-active-end-easingInherits shared end easingEnd keyframe easing for the active line.
blyrics-line-scroll-below-end-easingInherits shared end easingEnd keyframe easing for visible lines below the active line.
blyrics-line-scroll-translate-y-startvar(--blyrics-line-scroll-delta-px)Shared line-scroll start offset.
blyrics-line-scroll-translate-y-end0pxShared line-scroll end offset.
blyrics-line-scroll-above-translate-y-startInherits shared start offsetStart offset for visible lines above the active line.
blyrics-line-scroll-active-translate-y-startInherits shared start offsetStart offset for the active line.
blyrics-line-scroll-below-translate-y-startInherits shared start offsetStart offset for visible lines below the active line.
blyrics-line-scroll-above-translate-y-endInherits shared end offsetEnd offset for visible lines above the active line.
blyrics-line-scroll-active-translate-y-endInherits shared end offsetEnd offset for the active line.
blyrics-line-scroll-below-translate-y-endInherits shared end offsetEnd offset for visible lines below the active line.

blyrics-early-scroll-consider-s is an independent lookahead in seconds. When a lyric triggers an autoscroll, nearby upcoming lines within this window participate in choosing the target. Entering the window alone does not trigger a scroll. Lines included in that scroll do not trigger another scroll when their own scroll time arrives. Seeking and resuming autoscroll can still reposition the viewport.

The scroll gate and blyrics-queue-scroll-ms have been removed; the old queue knob has no effect. There is no timing equation to balance against animation duration.

Scoping the scroll position to one view

Knobs are global, so the side panel, fullscreen and the Picture-in-Picture window all read the same values. If you want the Picture-in-Picture window to scroll differently, set --blyrics-target-scroll-pos-ratio on its .blyrics-container instead. Wherever that variable resolves, it wins over the blyrics-target-scroll-pos-ratio knob. Use a plain number from 0 to 1: values outside that range get clamped, units like % aren’t converted, and anything that isn’t a number falls back to the knob.

The variable is read once, when the theme is applied. The side panel and fullscreen share one view, so a selector that only matches in fullscreen won’t take effect when you toggle it. The Picture-in-Picture window has its own view, which is why scoping to it works.

This keeps lyrics centered everywhere except the Picture-in-Picture window:

/* blyrics-target-scroll-pos-ratio = 0.5; */
.blyrics-pip-shell .blyrics-container {
  --blyrics-target-scroll-pos-ratio: 0.37;
}

The Picture-in-Picture window also defaults --blyrics-scroll-timing-offset to 0s, so a line stays put until its last syllable ends instead of moving up half a second early inside the short window. A value set on :root doesn’t reach it; to change it, set the variable on the same selector:

.blyrics-pip-shell .blyrics-container {
  --blyrics-scroll-timing-offset: 0.2s;
}

Switching lyrics in the Picture-in-Picture window

When the Picture-in-Picture window gets a new set of lyrics (a new song, or a different source picked from the lyrics dock), the old lines fade out and the lines on screen rise into place one after another. Switching source within the same song keeps the old lyrics up, dimmed, while the next ones load, and the loader only appears if they take longer than 250ms. While they wait, the old lyrics have data-held on .blyrics-pip-scroller, so you can change how far they dim:

.blyrics-pip-scroller[data-held] {
  opacity: 0.5;
}

The lines fading out are a short-lived copy marked .blyrics-pip-lyrics__exit, and .blyrics-pip-lyrics has data-revealing while lines are still rising. Each rising line ends at the opacity your theme gives it, and the blur is added on top of its own filter, so lines you dim or blur keep that look when the rise ends. With reduced motion on, both steps become a short fade. None of this applies to the side panel or fullscreen.

You can retune the motion with these variables on .blyrics-pip-lyrics. They’re read when a swap starts, so changing them mid-swap does nothing until the next one:

VariableDefaultWhat it does
--blyrics-pip-lyrics-hold-delay250msHow long old lyrics stay up, dimmed, before the loader replaces them
--blyrics-pip-lyrics-exit-duration200msFade-out of the old lines (and fade-in of the loader)
--blyrics-pip-lyrics-reveal-duration500msHow long each line takes to rise
--blyrics-pip-lyrics-reveal-delay120msWait before the first line starts rising
--blyrics-pip-lyrics-reveal-stagger40msGap between one line and the next
--blyrics-pip-lyrics-reveal-linesAll on screenMost lines that rise; the rest appear at once. 0 turns the rise off
--blyrics-pip-lyrics-reveal-distance12pxHow far below its place a line starts
--blyrics-pip-lyrics-reveal-blur3pxBlur a line starts with
--blyrics-pip-lyrics-reveal-easingcubic-bezier(0.22, 1, 0.36, 1)Easing for the rise
.blyrics-pip-lyrics {
  --blyrics-pip-lyrics-reveal-stagger: 70ms;
  --blyrics-pip-lyrics-reveal-blur: 0px;
}

Tip: Pay attention to the units of the values; Some values are in seconds (s), while others are in milliseconds (ms).

Dynamic Properties

--blyrics-duration is a special custom property that is set dynamically by the extension’s main script. It represents the duration of the current lyric line and is used to ensure that animations are synchronized with the music playback.

To learn more about CSS custom properties, check out the MDN Web Docs on Using CSS custom properties.

4. Styling the Main Lyrics Container

The main container for the lyrics is styled using the .blyrics-container class:

.blyrics-container {
  font-family: var(--blyrics-font-family, var(--blyrics-default-font-family));
  font-size: var(--blyrics-font-size);
  font-weight: var(--blyrics-font-weight);
  isolation: isolate;
  line-height: var(--blyrics-line-height);
  position: relative !important;
  z-index: 1;
  padding-top: 2rem;
  padding-bottom: calc(var(--blyrics-padding-bottom));
}

This sets the overall appearance of the lyrics container, including typography, positioning, and scroll behavior. The isolation: isolate property creates a new stacking context to prevent z-index issues with other page elements. Autoscroll updates the viewport position and smooths the movement with additive per-line translate animations.

Container Data Attributes

The .blyrics-container element has two data attributes that indicate its current state:

AttributeValuesDescription
data-sync"richsync", "synced", "none"Indicates the synchronization type of the current lyrics
data-loader-visible"true", "false"Indicates whether the loading spinner is currently visible
data-credits-focusedpresent or absentPresent while the songwriter credits hold the scroll focus

Sync Type Styling

Use data-sync to style lyrics differently based on their synchronization level:

/* Lyrics with no timing information */
.blyrics-container[data-sync="none"] > div {
  opacity: 1;
  filter: none;
}

/* Standard synced lyrics (line-level timing) */
.blyrics-container[data-sync="synced"] > div {
  /* your synced styles */
}

/* Rich-synced lyrics (word-level timing) */
.blyrics-container[data-sync="richsync"] > div {
  /* your richsync styles */
}

Note: When lyrics are first loading, they may temporarily appear with data-sync="none" before transitioning to "synced" or "richsync". Ensure your theme handles this transition smoothly.

Loader Visibility Styling

Use data-loader-visible to adjust styles when the loader is active:

/* Hide or adjust lyrics container when loader is visible */
.blyrics-container[data-loader-visible="true"] {
  /* your styles during loading */
}

/* Normal display when loader is hidden */
.blyrics-container[data-loader-visible="false"] {
  /* your normal styles */
}

5. Styling Individual Lyric Lines

Lyric lines are rendered as normal block and inline text so the browser can wrap at natural whitespace. Timed words are still represented by spans, and animation timing is driven by the Web Animations API.

Base Structure

The lyrics use a hierarchical structure with specific class names:

  • .blyrics-container - The main container for all lyrics
  • .blyrics--line - Each lyric line (a <div>)
  • .blyrics-line-main - Main lyric text row inside a line
  • .blyrics-background-line - Background vocal row, shown below the main row when background vocals are present
  • .blyrics-bidi-run - Inline text-flow wrapper that lets the browser apply native bidi ordering across timed word spans
  • .blyrics-highlight-run - An aria-hidden copy of a text row’s runs, positioned over the visible text; it holds the .blyrics-word-highlight overlays
  • .blyrics-bidi-sensitive - Applied to lyric text rows that contain RTL script; changes word wrappers to inline text flow for correct browser bidi and wrapping
  • .blyrics-word-group - Word group; syllable-synced parts for one word stay together. It is inline-block for LTR-only rows and display: contents inside .blyrics-bidi-sensitive
  • .blyrics--word - Each word within a line (a <span>)
  • .blyrics-word-highlight - Real active-color overlay for a timed word; every timed word has one
  • .blyrics-background-lyric - Background vocal word group or timed word

Example:

<div class="blyrics--line" data-agent="v1" data-time="10.259" data-duration="10.291">
  <div class="blyrics-line-main" dir="auto">
    <span class="blyrics-bidi-run blyrics-highlight-run" dir="auto" aria-hidden="true">
      <span class="blyrics-word-group">
        <span class="blyrics--word blyrics-word-highlight" data-time="10.259" data-duration="0.42" data-content="Hello" data-word-state="active">Hello</span>
      </span>
      text
    </span>
    <span class="blyrics-bidi-run" dir="auto">
      <span class="blyrics-word-group">
        <span class="blyrics--word" data-time="10.259" data-duration="0.42" data-content="Hello" data-word-state="active">Hello</span>
      </span>
      text
    </span>
  </div>
  <div class="blyrics-background-line">
    <span class="blyrics-bidi-run blyrics-highlight-run" dir="auto" aria-hidden="true">
      <span class="blyrics-word-group blyrics-background-lyric">...</span>
    </span>
    <span class="blyrics-bidi-run" dir="auto">
      <span class="blyrics-word-group blyrics-background-lyric">...</span>
    </span>
  </div>
  <div class="blyrics--romanized blyrics-content-line">...</div>
  <div class="blyrics--translated blyrics-content-line">...</div>
</div>

Base Styling for Each Lyric

.blyrics-container > div {
  cursor: pointer;
  padding: var(--blyrics-padding) 0.25em !important;
  transform: scale(var(--blyrics-scale));
  transform-origin: left center;
}

.blyrics--line {
  color: var(--blyrics-lyric-inactive-color);
  unicode-bidi: plaintext;
  white-space: normal;
}

.blyrics-line-main,
.blyrics-background-line {
  display: block;
  text-align: inherit;
  unicode-bidi: plaintext;
  white-space: normal;
}

.blyrics-word-group {
  display: inline-block;
  white-space: nowrap;
}

.blyrics-bidi-sensitive .blyrics-word-group {
  display: contents;
}

.blyrics-bidi-sensitive .blyrics--word {
  display: inline;
}
  • Inactive Scale: The element is scaled by --blyrics-scale for inactive lyrics
  • Text Layout: Uses normal inline text flow, not flexbox or CSS order
  • Word Grouping: Syllable-synced parts inside a word stay in one .blyrics-word-group. In LTR-only rows it is an unbreakable inline box; in RTL-sensitive rows it becomes display: contents so browser bidi can order and wrap the line correctly
  • Background Vocals: Primary lyric background vocals are moved to .blyrics-background-line; timed romanization does not split background parts onto a separate line
  • Transform Origin: Set to left center for proper scaling animation

Activating a Lyric

When a lyric becomes active, the line gets .blyrics--active for current-line state and its scale is animated with element.animate():

.blyrics--active {
  cursor: default;
}

The scale animation uses --blyrics-scale, --blyrics-active-scale, and --blyrics-scale-transition-duration. Lyric timing is managed by the Web Animations API.

Important: Prefer styling stable structure classes and data attributes. The extension may select multiple nearby lines for scrolling, so avoid assumptions that exactly one .blyrics--active line exists.

Two clocks: .blyrics--active vs .blyrics--animating

A line carries two independent state classes, and they do not open and close together.

ClassClockWindow
.blyrics--activeScroll clock: audio time plus --blyrics-scroll-timing-offset (0.5s default)Opens --blyrics-early-scroll-consider-s before the line, closes when the scroll target moves on
.blyrics--animatingAudio clockOpens ~2s before the line so animations can be prepared, closes only once the line’s own words have finished
.blyrics--pausedPlay statePresent on the line and on each of its words whenever playback is paused. Lets theme-authored CSS animations freeze alongside the engine’s element.animate() pause

Because the scroll clock deliberately runs ahead of the audio, .blyrics--active is dropped roughly half a second before a line stops being sung. Anything that dims, blurs, or shrinks previous lines should account for that:

/* dims the previous line while its last word is still playing */
.blyrics-container > div:not(.blyrics--active) {
  opacity: 0.33;
}

/* keeps it lit until the line is genuinely done */
.blyrics-container > div.blyrics--animating {
  opacity: 1;
}

Use .blyrics--active for scroll-anchored affordances (which line is centered, cursor, hit targets) and .blyrics--animating for anything that must survive until the singing stops.

Styling Each Word

Every word uses the .blyrics--word class:

.blyrics--word {
  color: var(--blyrics-lyric-inactive-color);
  position: relative;
  unicode-bidi: normal;
  white-space: inherit;
}
  • Color: Set to inactive color initially
  • Active Overlay: Every timed word has a real .blyrics-word-highlight. A row’s overlays sit in a .blyrics-highlight-run, an aria-hidden copy of the run laid over the visible text so both share one layout
  • Wrapped Words: A word past the wrap threshold carries <wbr> points in both copies, so the overlay wraps exactly like the visible text
  • Line-Synced Words: Zero-duration line-synced words get .blyrics-line-synced-word and fade in without the rich-sync swipe
  • RTL-Sensitive Words: Rows containing RTL script use .blyrics-bidi-sensitive; their .blyrics--word spans compute to display: inline and their .blyrics-word-group wrappers compute to display: contents

Word Data Attributes

Each word span has the following data attributes:

AttributeDescription
data-timeStart time of the word in seconds
data-durationDuration of the word in seconds
data-contentThe word text
data-long-wordPresent (with value "true") when word duration exceeds the threshold
data-word-stateupcoming, active, or past: whether the word is not yet reached, being sung, or already sung

Targeting Long Words

Words with duration exceeding blyrics-long-word-threshold (default: 1500ms) get data-long-word="true". This is useful for adding glow effects to held/sustained notes:

/* Set the threshold (in ms) */
/* blyrics-long-word-threshold = 1500; */

/* Add glow effect to long words */
.blyrics-word-highlight[data-long-word] {
  --blyrics-glow-color: color(display-p3 1 1 1 / 1);
}

--blyrics-glow-color resolves per word against each .blyrics-word-highlight, so different long words can glow different colors.

Changing the threshold triggers a lyric reload automatically.

Styling by Word State

data-word-state marks whether a word is being sung. It carries upcoming before the word starts, active while it is being sung, and past once it has finished, and it is written on both layers of a word (the base .blyrics--word and its .blyrics-word-highlight overlay), so you can key either. A word stays active for exactly as long as it is sung, so a transition on the flip is the whole of a per-word karaoke effect where whole words change rather than sweeping:

.blyrics--word {
  color: var(--blyrics-lyric-inactive-color);
  transition: color 180ms ease;
}

.blyrics--word[data-word-state="active"],
.blyrics--word[data-word-state="past"] {
  color: var(--blyrics-lyric-active-color);
}

The renderer writes data-word-state only when a word’s state changes, not every frame, so selecting on it adds no per-frame cost. A line-synced word, which has no duration of its own, is active from its start until the next word begins.

Applying the Wobble Animation

The wobble effect is created by JavaScript with element.animate(), but the keyframe values come from CSS variables:

:root {
  --blyrics-wobble-duration: 1s;
  --blyrics-word-wobble-transform-from: scaleX(1);
  --blyrics-word-wobble-transform-peak: translateX(0.05em) scaleX(1.025);
  --blyrics-word-wobble-transform-settle: translateX(0) scaleX(1);
  --blyrics-word-wobble-transform-to: scaleX(1);
}

Use --blyrics-animate-word-wobble: 0 to disable the wobble effect.

RTL note: Word wobble uses CSS transforms on .blyrics--word. Rows that contain RTL script are rendered as true inline text (.blyrics-bidi-sensitive .blyrics--word { display: inline; }) so the browser can apply the Unicode Bidirectional Algorithm correctly. Transforms do not reliably apply to normal inline text, so word wobble should be treated as unavailable for RTL-sensitive rows.

Implementing the Swipe (Karaoke) Transition

Defining Custom Properties

Two custom properties control the swipe transition:

@property --lyric-transition-amount-start {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}

@property --lyric-transition-amount-end {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}

The highlight overlay

The swipe effect runs on each word’s .blyrics-word-highlight overlay with background-clip: text. A row’s overlays sit in a .blyrics-highlight-run, an aria-hidden layer that is position: absolute over the visible text, so the active copy and the inactive text share one layout:

.blyrics--word.blyrics-word-highlight {
  color: transparent;
  background-image: linear-gradient(
    90deg,
    var(--blyrics-lyric-active-color) calc(100% * var(--lyric-transition-amount-start)),
    #00000000 calc(100% * var(--lyric-transition-amount-end) + 1px)
  );
  background-clip: text;
  opacity: 0;
  pointer-events: none;
  --lyric-transition-amount-start: var(--blyrics-highlight-swipe-start-from, -0.2);
  --lyric-transition-amount-end: var(--blyrics-highlight-swipe-end-from, -0.1);
}

This creates an overlay using background-clip: text with a gradient that reveals the active color progressively. Rich-synced words animate the gradient and make the active overlay visible instantly at the word start time; they do not fade in. Line-synced words (.blyrics-line-synced-word) skip the gradient swipe and fade the fully highlighted overlay in word by word.

Timing Model

The swipe and glow are driven by element.animate():

  • Rich-sync swipe starts at wordStart - blyrics-swipe-lead-ratio * wordDuration
  • Rich-sync swipe duration is blyrics-swipe-duration-ratio * wordDuration
  • With the default ratios, the swipe reaches the end of the word at wordStart + wordDuration; the remaining 0.5 * wordDuration is the tail moving past the word
  • Rich-sync word-timed opacity becomes visible instantly at wordStart; --blyrics-lyric-highlight-fade-in-duration is not used for rich-sync fade-in
  • Line-synced opacity fade-in starts at its generated word time and uses --blyrics-lyric-highlight-fade-in-duration
  • Glow starts at wordStart and lasts max(wordDuration * --blyrics-highlight-glow-duration-ratio, --blyrics-highlight-glow-min-duration)
  • Fade-out uses --blyrics-lyric-highlight-fade-out-duration
  • --blyrics-animate-highlight-swipe: 0 keeps rich-sync timing in JS but makes each word fully highlighted instantly at wordStart instead of moving the gradient

6. Creating Animation Effects

Most lyric timing uses the Web Animations API, but the same conceptual effects remain:

@keyframes blyrics-wobble {
  0% {
    transform: scaleX(1);
  }
  12.5% {
    transform: translateX(0.05em) scaleX(1.025);
    animation-timing-function: ease-in-out;
  }
  75% {
    transform: translateX(0) scaleX(1);
  }
  100% {
    transform: scaleX(1);
    animation-timing-function: ease-out;
  }
}

@keyframes blyrics-glow {
  0% {
    filter: drop-shadow(0 0 0.8rem var(--blyrics-glow-color));
  }
  to {
    filter: drop-shadow(0 0 0rem var(--blyrics-glow-color));
  }
}

@keyframes blyrics-spin {
  0% {
    transform: translateY(-50%) rotate(0deg);
  }
  to {
    transform: translateY(-50%) rotate(1turn);
  }
}

@keyframes blyrics-shimmer {
  0% {
    background-position: 200% 0;
  }
  100% {
    background-position: -200% 0;
  }
}

These animations create:

  • blyrics-wobble: Subtle horizontal movement and scale for active lyrics (includes scaleX(1.025) for emphasis)
  • blyrics-glow: Drop shadow glow effect that fades out (uses filter: drop-shadow for better compatibility with background-clip: text)
  • blyrics-spin: Rotating animation for the loading spinner
  • blyrics-shimmer: Shimmer animation for loading text

Theme authors should prefer CSS variables and stable structural selectors. The default line scale, word swipe, word glow, instrumental fill, and lyric scroll smoothing are all run with element.animate().

7. Modifying YouTube Music’s Layout

The ytmusic/ directory contains extensive modifications to YouTube Music™‘s interface to create a more immersive lyrics experience.

Background and Transparency Effects

ytmusic-player-page:before {
  background: var(--blyrics-background-img);
  background-position: 50% !important;
  background-repeat: no-repeat;
  background-size: cover;
  bottom: 0;
  content: "";
  filter: saturate(var(--blyrics-background-saturate)) brightness(25%) blur(var(--blyrics-background-blur));
  left: 0;
  position: absolute;
  right: 0;
  top: 0;
  transform: scale(1.2);
  z-index: -100;
}

This creates a blurred, saturated background image from the album artwork.

Player Bar Styling

ytmusic-player-bar,
#player-bar-background {
  backdrop-filter: blur(var(--blyrics-blur-amount));
}

Applies a blur effect to the player bar for a modern glass-like appearance.

Side Panel Adjustments

#side-panel {
  min-width: 33em;
}

Ensures the lyrics panel has adequate space for comfortable reading. The 33em width provides consistent sizing regardless of viewport.

8. Handling Loading and Errors

Loading Spinner

#blyrics-loader {
  align-items: center;
  display: flex !important;
  gap: 1rem;
  height: 100%;
  justify-content: center;
  opacity: 1;
  position: relative;
  transition: height 0.2s, opacity 0.2s;
}

#blyrics-loader:before {
  animation: blyrics-spin 1s linear infinite;
  background: url(https://betterlyrics.org/icon-512.png);
  background-position: 50%;
  background-size: cover;
  content: "";
  display: block;
  height: 3rem;
  margin-top: 3rem;
  width: 3rem;
}

#blyrics-loader:after {
  background: linear-gradient(
        110deg,
        color-mix(in srgb, var(--blyrics-ui-text-color) 50%, transparent) 30%,
        var(--blyrics-ui-text-color) 50%,
        color-mix(in srgb, var(--blyrics-ui-text-color) 50%, transparent) 70%
    );
  background-size: 200% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  content: "Better Lyrics is searching for lyrics...";
  font-family: var(--blyrics-font-family, var(--blyrics-default-font-family));
  font-size: 2rem;
  font-weight: 700;
  isolation: isolate;
  line-height: 1.5;
  white-space: pre;
  z-index: 1;
}

Creates an animated loading state with the Better Lyrics icon and descriptive text.

Loader State Attributes

The loader supports several attributes for different states:

#blyrics-loader[active] { /* Loader is visible and active */ }
#blyrics-loader:not([active]) {
  height: 0;
  opacity: 0;
}

#blyrics-loader[small-loader][active] {
  padding-top: 5rem;
  height: 5rem;
}

#blyrics-loader[small-loader]:after {
  content: "Still searching for synced lyrics...";
}

#blyrics-loader[no-sync-available]:after {
  content: "Better Lyrics could not find synced lyrics";
}
AttributeDescription
[active]Shows the loader
[small-loader]Compact loader shown while searching for synced lyrics
[no-sync-available]Indicates synced lyrics couldn’t be found

Error Messages

.blyrics--error {
  align-self: flex-start !important;
  color: var(--blyrics-error-color);
  cursor: default;
  font-family: var(--blyrics-font-family, var(--blyrics-default-font-family));
  font-size: var(--blyrics-font-size);
  font-weight: var(--blyrics-font-weight);
  line-height: 1;
  margin-block: var(--blyrics-margin) !important;
  opacity: var(--blyrics-error-opacity);
  padding-bottom: var(--blyrics-padding) !important;
  padding-top: var(--blyrics-padding) !important;
  word-break: break-word;
}

Styles error messages with reduced opacity and a distinct color.

No Lyrics Button

.blyrics-add-lyrics-button {
  background-color: var(--blyrics-footer-bg-color);
  border: 1px solid var(--blyrics-footer-border-color);
  border-radius: var(--blyrics-border-radius);
  color: var(--blyrics-ui-text-color);
  cursor: pointer;
  font-family: var(--blyrics-font-family, var(--blyrics-default-font-family));
  font-size: 1.5rem;
  font-weight: 600;
  padding: 1rem 2rem;
  transition: all 0.3s ease;
}

.blyrics-add-lyrics-button:hover {
  background-color: rgba(255, 255, 255, 0.2);
  border-color: rgba(255, 255, 255, 0.3);
  transform: translateY(-2px);
}

Provides an interactive button for when lyrics are not available.

9. Making the Design Responsive

The CSS uses media queries to adapt the layout for different screen sizes.

Large Screens (615px and above)

@media (min-width: 615px) {
  ytmusic-player-page:not([video-mode]):not([player-fullscreened]):not([blyrics-dfs]):not([player-ui-state="MINIPLAYER"]) #player.ytmusic-player-page {
    left: 50%;
    max-width: 400px !important;
    transform: translateX(-50%);
  }
}

Centers the player with a maximum width for optimal viewing.

Medium Screens (up to 936px)

@media (max-width: 936px) {
  ytmusic-player-page:not([is-video-truncation-fix-enabled])[player-fullscreened]:not([blyrics-dfs]) #player.ytmusic-player-page {
    top: 0;
    height: 30rem;
    width: 30rem;
    left: 50%;
    transform: translateX(-50%);
  }
}

Adjusts player size and positioning for medium screens.

Small Screens (up to 615px)

@media (max-width: 615px) {
  .blyrics-container > div {
    margin-inline: 4rem !important;
  }
  
  .blyrics-container:before {
    background: linear-gradient(to right, #030303 4rem, rgba(3, 3, 3, 0.5), #030303 96%), var(--blyrics-background-img) !important;
    background-position: 50% !important;
    background-size: cover !important;
    border-radius: 1rem;
    bottom: 0;
    filter: blur(80px) saturate(2);
    top: 0;
  }
}

Adds special background effects and adjusts margins for mobile devices.

10. Implementing Fullscreen Mode

The CSS includes comprehensive styles for fullscreen mode:

ytmusic-player-page:not([is-video-truncation-fix-enabled])[player-fullscreened]:not([blyrics-dfs]) #player.ytmusic-player-page {
  height: 40rem;
  width: 40rem;
  top: calc(50% - var(--ytmusic-nav-bar-height));
  left: 20%;
  transform: translate(-50%, -50%);
  mask-image: none !important;
  box-shadow:
    rgba(0, 0, 0, 0.2) 0px 54px 55px, rgba(0, 0, 0, 0.12) 0px -12px 30px, rgba(0, 0, 0, 0.12) 0px 4px 6px, rgba(0, 0, 0, 0.17) 0px 12px 13px, rgba(0, 0, 0, 0.09) 0px -3px 5px;
}

This complex selector targets fullscreen mode specifically and:

  • Positions the player optimally
  • Adds dramatic shadow effects
  • Removes any mask images
  • Centers content appropriately

The [blyrics-dfs] attribute allows users to disable fullscreen styling if preferred.

11. Supporting Right-to-Left (RTL) Languages

.blyrics-rtl {
  direction: rtl;
}

.blyrics-bidi-sensitive .blyrics-word-group {
  display: contents;
}

.blyrics-bidi-sensitive .blyrics--word {
  display: inline;
}

The .blyrics-rtl class is applied when RTL language content is detected, ensuring proper text direction for languages like Arabic or Hebrew. Timed lyric text is also wrapped in .blyrics-bidi-run, an inline wrapper that preserves logical DOM order while allowing the browser’s Unicode Bidirectional Algorithm to decide visual order and line wrapping.

When a text row contains RTL script, the row also receives .blyrics-bidi-sensitive. In that mode, .blyrics-word-group becomes display: contents and .blyrics--word becomes normal inline text. This is intentional: inline-block word boxes make each RTL word correct internally but can make the word sequence render in the wrong order. Do not reverse DOM order for RTL lyrics; that breaks wrapping and timing. Use normal logical order and let browser bidi layout produce the visual order.

Because RTL-sensitive word spans are inline, transform-based word wobble does not apply reliably on those rows. Highlight timing, glow, swipe, click seeking, and line selection still use the same timed .blyrics--word elements.

Agent-Based Alignment

For multi-voice lyrics (duets, conversations), the data-agent attribute controls alignment:

.blyrics--line[data-agent="v2"],
.blyrics--line[data-agent="v3"] {
  text-align: right;
  transform-origin: right center !important;
}
AttributeDescription
data-agent="v1"Primary voice (default, left-aligned)
data-agent="v2"Secondary voice (right-aligned)
data-agent="v3"Tertiary voice (right-aligned)
data-agent="v1000"Both speakers simultaneously (duet/chorus, centered)

This right-aligns secondary voices (v2, v3) while the primary voice (v1) remains left-aligned, creating a visual conversation layout. When both speakers sing simultaneously (v1000), the lyrics are centered. The lyric layout does not use flexbox; use text-align and transform-origin for agent-specific alignment.

12. Adding a Watermark

#blyrics-watermark {
  display: none;
  position: absolute;
  bottom: 1rem;
  left: 1rem;
  margin-bottom: 0 !important;
  justify-content: center;
  user-select: none;
}

#blyrics-watermark > .blyrics-watermark__container {
  display: flex;
  align-items: center;
  border-radius: var(--blyrics-border-radius);
  padding: 0.25rem;
  padding-right: 0.75rem;
  background-color: rgba(0, 0, 0, 0);
  border: 1px solid rgba(0, 0, 0, 0.02);
  backdrop-filter: brightness(0.66) blur(var(--blyrics-blur-amount));
}

The watermark is initially hidden but can be displayed for branding or attribution purposes. It includes a subtle backdrop filter effect.

13. Displaying Song Information

#blyrics-song-info > p#blyrics-title {
  font-size: 2rem;
  font-weight: 700;
  margin: 0;
}

#blyrics-song-info > p#blyrics-artist {
  font-size: 1.5rem;
  margin: 0;
  opacity: 0.5;
  margin-top: 0.25rem;
}

#blyrics-song-info #blyrics-album {
  opacity: 0.8;
}

These styles create a clean display for song title and artist information, with the artist name appearing more subdued. When an album is known, it follows the artist as #blyrics-album.

The artist and album names are links to their YouTube Music pages, wrapped in .blyrics-song-link. Those links inherit the surrounding text color and underline on hover, so they blend in unless you style them:

#blyrics-song-info .blyrics-song-link:hover {
  text-decoration: underline;
}

In fullscreen mode this block sits above a full set of playback controls. See Fullscreen Player Controls.

.blyrics-footer__container {
  align-items: center;
  background-color: var(--blyrics-footer-bg-color);
  border: 1px solid var(--blyrics-footer-border-color);
  border-radius: var(--blyrics-border-radius);
  color: var(--blyrics-footer-text-color);
  cursor: default;
  display: flex;
  font-family: var(--blyrics-footer-font-family);
  font-size: var(--blyrics-footer-font-size);
  font-weight: var(--blyrics-footer-font-weight);
  line-height: var(--ytmusic-body-line-height);
  padding: 0.5rem 1.25rem;
  position: relative;
  white-space: pre;
  width: fit-content;
  z-index: 2;
}

.blyrics-footer__discord {
  background-color: var(--blyrics-footer-bg-color);
  border: 1px solid var(--blyrics-footer-border-color);
  padding: 0.5rem;
  border-radius: var(--blyrics-border-radius);
  display: grid;
  place-items: center;
  transition: background-color 0.2s;
}

.blyrics-footer__discord:hover {
  background-color: var(--blyrics-discord-hover-color);
}

Creates styled footer elements including a Discord button with hover effects.

Songwriter Credits

When the lyrics name their songwriters, a .blyrics-credits line (“Written by A, B & C”) follows the last lyric, before the footer. It stays dim while the song plays. When the last line ends, it brightens, takes the scroll focus, and the container gets data-credits-focused. Seeking back gives the focus to the lines again. Unsynced lyrics show the credits at full strength from the start.

Custom propertyDefaultDescription
--blyrics-credits-label"Written by"Text before the names. Set to the user’s language by the extension.
--blyrics-credits-font-sizemax(0.4em, 12px)Font size of the credits line
--blyrics-credits-opacity0.2Opacity while the song plays
--blyrics-credits-focused-opacity0.85Opacity after the last line ends
.blyrics-credits {
  font-style: italic;
}

.blyrics-container[data-credits-focused] > .blyrics-credits {
  color: var(--blyrics-lyric-active-color);
}

The credits are a <p>, not a <div>, so rules written for lyric lines as .blyrics-container > div (hover effects, per-line opacity and blur) do not apply to them. Style them through .blyrics-credits.

To hide the credits, set .blyrics-credits { display: none; } (autoscroll then ignores them), or declare /* blyrics-hide-credits = true; */ so they are never built.

15. ThemeSong Compatibility

The themesong.css file ensures compatibility with the ThemeSong browser extension:

@supports (selector(:root:has(#ThemeSong-MainContainer))) {
  :root:has(#ThemeSong-MainContainer) {
    #player-page {
      top: 0 !important;
      padding-block: 64px;
    }
    
    [style*="--blyrics-background-img"] {
      --blyrics-background-img: unset !important;
    }
  }
}

This CSS feature query detects when ThemeSong is active and adjusts the layout accordingly, including disabling the background image feature to prevent conflicts.

16. Translated and Romanized Lyrics

.blyrics--translated,
.blyrics--romanized {
  display: block;
  font-size: var(--blyrics-translated-font-size);
  font-weight: var(--blyrics-translated-font-weight);
  font-family: var(--blyrics-translated-font-family, var(--blyrics-font-family, var(--blyrics-default-font-family)));
  color: var(--blyrics-translated-color);
  white-space: normal;
  line-height: 1.1;
  margin-top: 8px;
}

.blyrics--romanized {
  width: fit-content;
  padding-block: 1rem;
  font-size: calc(var(--blyrics-translated-font-size) / 1.25);
  background: rgba(255, 255, 255, 0.05);
  padding: 0.375rem 0.75rem;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 1rem;
}

Provides distinct styling for translated lyrics and romanized text, with romanized text getting a subtle background container. If romanization is timed, it can contain the same .blyrics-line-main, .blyrics-word-group, and .blyrics--word structure as the primary lyric line, but background parts are not split into a separate .blyrics-background-line inside romanization.

17. Instrumental Breaks

Better Lyrics detects instrumental breaks (intros, outros, and mid-song gaps) and displays an animated music note icon. Instrumental breaks are always visible and styled like regular lyrics lines, inheriting data-agent from surrounding lines for proper alignment.

Instrumental Break Structure

<div class="blyrics--instrumental blyrics--line" data-instrumental="true" data-time="0" data-duration="33" data-agent="v1">
  <svg class="blyrics--instrumental-icon" viewBox="0 0 24 24">
    <defs>
      <filter id="blyrics-glow-...">...</filter>
      <clipPath id="blyrics-wave-clip-..." class="blyrics--wave-clip">
        <path class="blyrics--wave-rect" d="..." />
        <path class="blyrics--wave-path" d="..." />
      </clipPath>
    </defs>
    <path class="blyrics--instrumental-bg" d="..." />
    <g filter="url(#blyrics-glow-...)">
      <path class="blyrics--instrumental-fill" clip-path="url(#blyrics-wave-clip-...)" d="..." />
    </g>
  </svg>
</div>

Instrumental CSS Variables

VariableDefault ValueDescription
--blyrics-duration(dynamic)Duration of the break in ms (set by extension)

Instrumental Classes

ClassPurpose
.blyrics--instrumentalBase container for instrumental breaks (also has .blyrics--line)
.blyrics--instrumental-iconThe SVG music note icon
.blyrics--instrumental-bgBackground path of the music note (uses inactive color)
.blyrics--instrumental-fillFill path of the music note (uses active color)
.blyrics--wave-clipClipPath for the fill animation
.blyrics--wave-rectStatic lower block of the fill clip
.blyrics--wave-pathAnimated wavy top edge inside the clip

Instrumental Animation

When the note becomes active, the engine fades in .blyrics--instrumental-fill, moves .blyrics--wave-clip upward to fill the note, flattens .blyrics--wave-path over the break duration, and runs a looping wave-surface d path animation on .blyrics--wave-path.

Use --blyrics-instrumental-fill-transform-* for the fill travel, --blyrics-instrumental-wave-transform-* for the wave flattening, and --blyrics-instrumental-wave-oscillation-duration / --blyrics-instrumental-wave-oscillation-easing for the visible wave loop.

The shape of that loop is --blyrics-instrumental-wave-path-high and --blyrics-instrumental-wave-path-low, the two path() values the surface morphs between. Redraw them to change the wave’s amplitude or how many crests it has:

/* A taller, slower swell with two bumps instead of five */
.blyrics-container {
  --blyrics-instrumental-wave-path-high: path("M -4 3 Q 4 1.5 13 3 Q 22 4.5 30 3 L 30 4 L -4 4 Z");
  --blyrics-instrumental-wave-path-low: path("M -4 3 Q 4 4.5 13 3 Q 22 1.5 30 3 L 30 4 L -4 4 Z");
  --blyrics-instrumental-wave-oscillation-duration: 2s;
}

Both paths must use the same commands in the same order with the same number of arguments, as in the pair above where each is M Q Q L L Z. A browser only interpolates two paths smoothly when their command sequences match; a mismatched pair falls back to discrete interpolation and the wave snaps at the halfway point instead of flowing.

Two things constrain the geometry. The paths close along L 30 4 L -4 4 Z so the surface overlaps the static block below it, which .blyrics--wave-rect draws from y = 3.9 down. And a quadratic sits a quarter of the way from its endpoints to its control point, so a control at y = 4.5 peaks at 3.75 and stays clear of that edge, while a control at y = 5 would peak at exactly 4 and pinch the surface shut.

Styling Instrumental Breaks

Instrumental breaks are always visible and styled like regular lyrics lines. They have the .blyrics--line class and support data-agent for alignment (v1, v2, v3, v1000). The icon uses --blyrics-lyric-inactive-color for the background and --blyrics-lyric-active-color for the fill:

.blyrics--instrumental-icon {
  height: var(--blyrics-font-size);
  width: calc(var(--blyrics-font-size) + var(--blyrics-font-size) / 3);
  overflow: visible;
  margin-left: calc(var(--blyrics-font-size) / -3);
}

/* Right-aligned for secondary/tertiary vocals */
.blyrics--instrumental[data-agent="v2"] .blyrics--instrumental-icon,
.blyrics--instrumental[data-agent="v3"] .blyrics--instrumental-icon {
  margin-left: 0;
  margin-right: calc(var(--blyrics-font-size) / -3);
}

/* Centered for duets */
.blyrics--instrumental[data-agent="v1000"] .blyrics--instrumental-icon {
  margin-left: 0;
  margin-right: 0;
}

.blyrics--instrumental-bg {
  fill: var(--blyrics-lyric-inactive-color);
}

.blyrics--instrumental-fill {
  fill: var(--blyrics-lyric-active-color);
}

To customize instrumental breaks:

/* Change the icon size (independent of font size) */
.blyrics--instrumental-icon {
  width: 4rem;
  height: 4rem;
}

/* Custom background/fill colors */
.blyrics--instrumental-bg {
  fill: rgba(255, 255, 255, 0.3);
}
.blyrics--instrumental-fill {
  fill: rgba(255, 255, 255, 1);
}

Data Attributes

AttributeDescription
data-instrumental"true" indicates this is an instrumental
data-timeStart time of the break in seconds
data-durationDuration of the break in seconds
data-line-numberIndex of this element in the lyrics array
data-agentVoice alignment: "v1", "v2", "v3", "v1000"

18. Autoscroll Resume Button

.autoscroll-resume-button {
  font-family: var(--blyrics-font-family, var(--blyrics-default-font-family));
  position: absolute;
  display: block;
  font-size: 1.75rem;
  font-weight: 600;
  white-space: normal;
  line-height: 1.1;
  margin-top: 8px;
  width: fit-content;
  padding-block: 1rem;
  background: rgba(255, 255, 255, 0.05);
  padding: 0.5rem 1rem;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 1rem;
  color: rgba(255, 255, 255, 1);
  left: 50%;
  transform: translate(-50%, 2em);
  backdrop-filter: blur(5px);
  will-change: backdrop-filter;
  pointer-events: auto;
  cursor: pointer;
  user-select: none;
  transition: opacity 0.15s linear 0.05s, transform 0.2s cubic-bezier(0.5, 1, 0.89, 1), text-shadow 0.25s ease;
}

.autoscroll-resume-button[autoscroll-hidden="true"] {
  transition: opacity 0.1s linear, transform 0.15s ease-in;
  opacity: 0;
  pointer-events: none;
  transform: translate(-50%, -3em);
}

Creates an elegant button that appears when autoscroll is paused, with smooth show/hide transitions.

19. Unison Submitter Card and Floating Dock

When a song is served by the Unison provider, the extension injects two extra UI blocks into the YouTube Music page: a submitter card inside the lyrics footer, and a floating dock anchored to the side panel. Both are themeable from your custom CSS the same way as everything else on this page. Class names follow the existing blyrics-footer__* and blyrics-unison-* conventions.

New CSS Variables

VariableDefault ValueDescription
--blyrics-vote-hover-colorhsla(0, 0%, 100%, 0.2)Background of vote and report buttons on hover
--blyrics-small-border-radius1remBorder radius for vote buttons
--blyrics-fullscreen-bottom-dock-shift-24pxY-offset applied to bottom-anchored docks in fullscreen. Lifts the dock above the player bar so it stays reachable. Negative values move up

When the Unison provider is active, an extra card is appended to the lyrics footer. The card opens the standalone Unison page in a new tab when clicked anywhere outside the buttons.

<div class="blyrics-footer__unison">
  <div class="blyrics-footer__container blyrics-footer__unison-card">
    <div class="blyrics-footer__unison-author">
      <div class="blyrics-footer__unison-author-row">
        <strong class="blyrics-footer__author-name">PetName</strong>
        <span class="blyrics-footer__trust-tier" data-tier="trusted">Trusted</span>
      </div>
      <div class="blyrics-footer__unison-author-label">submitted this</div>
    </div>
    <div class="blyrics-footer__unison-divider"></div>
    <div class="blyrics-footer__unison-actions-block">
      <div class="blyrics-footer__unison-actions">
        <button class="blyrics-footer__vote">…</button>
        <button class="blyrics-footer__vote">…</button>
        <button class="blyrics-footer__vote">…</button>
      </div>
      <div class="blyrics-footer__unison-score-line">
        <strong>+12</strong> <span>score</span> · <strong>12</strong> <span>votes</span>
      </div>
    </div>
  </div>
</div>
ClassPurpose
.blyrics-footer__unisonOuter wrapper for the card. Forces full-width inside the footer
.blyrics-footer__unison-cardCombined with .blyrics-footer__container. Translucent rounded card with hover lighten
.blyrics-footer__unison-divider1px vertical line between submitter block and actions block. Only present when there is a submitter
.blyrics-footer__unison-actions-blockRight column. Holds the action row plus the score line
.blyrics-footer__unison-actionsRow of three buttons: upvote, downvote, report
.blyrics-footer__unison-score-lineScore and vote-count line beneath the actions

Submitter Block

ClassPurpose
.blyrics-footer__unison-authorColumn with author row plus subtitle
.blyrics-footer__unison-author-rowRow holding the author name and the trust tier pill
.blyrics-footer__author-nameThe submitter handle. Generated from their public key as a deterministic pet name
.blyrics-footer__unison-author-labelSmall “submitted this” label below the author row

Trust Tier Pill

The tier pill colors itself based on a data-tier attribute. Tier is derived from the submitter’s reputation in getTrustTier().

SelectorColor
.blyrics-footer__trust-tier[data-tier="new"]Blue
.blyrics-footer__trust-tier[data-tier="trusted"]Green
.blyrics-footer__trust-tier[data-tier="veteran"]Purple
.blyrics-footer__trust-tier[data-tier="expert"]Gold

To restyle, target .blyrics-footer__trust-tier[data-tier="<tier>"] and override color and background-color.

Vote Button

The same button class is reused for upvote, downvote, and report buttons in both the footer card and the floating dock.

ClassPurpose
.blyrics-footer__voteBase style. 30px square, rounded, glassy background. Hover uses --blyrics-vote-hover-color
.blyrics-footer__vote--activeActive state. The SVG path with fill-opacity becomes fully opaque

Inside the floating dock, vote buttons are scaled up to 32px square via the .blyrics-unison-dock__inner .blyrics-footer__vote selector.

Floating Dock

A second copy of the vote buttons floats over the side panel so they remain reachable while the user scrolls past the footer card.

<div class="blyrics-unison-dock" data-position="top-center">
  <div class="blyrics-unison-dock__inner">
    <button class="blyrics-footer__vote">…</button>
    <button class="blyrics-footer__vote">…</button>
    <button class="blyrics-footer__vote">…</button>
  </div>
</div>
ClassPurpose
.blyrics-unison-dockAbsolute-positioned wrapper inside #side-panel. pointer-events: none so empty space stays click-through
.blyrics-unison-dock__innerThe actual button group. Glass background, blur, pointer-events: auto

The dock chooses one of six anchor positions via the data-position attribute:

ValueAnchor
top-lefttop: 64px; left: 0
top-centertop: 64px; left: 50% (translated -50% via --dock-tx)
top-righttop: 64px; left: 100% (translated -100%)
bottom-lefttop: calc(100% - 64px); left: 0
bottom-centertop: calc(100% - 64px); left: 50%
bottom-righttop: calc(100% - 64px); left: 100%

When the autoscroll resume button is visible, the top-center dock shifts down by 72px via --dock-y-shift so the two controls do not overlap.

Hide and Idle States

ClassPurpose
.blyrics-unison-dock--hiddenApplied while the footer card is in the viewport. The dock fades, blurs, and scales down so the user only sees one set of controls at a time
.blyrics-unison-dock--idle-hiddenApplied while the player is idle in fullscreen. Hides the dock with the same animation when controls auto-hide

Both states animate transform, opacity, and filter: blur over 320ms.

A few related rules ship in the YTM stylesheets and you may want to override them:

/* Hide the dock when the side panel is showing something other than lyrics */
#side-panel:has(#tab-renderer:not([page-type="MUSIC_PAGE_TYPE_TRACK_LYRICS"])) .blyrics-unison-dock { … }

/* Hide top-anchored docks in fullscreen */
#layout[player-fullscreened]:not([blyrics-dfs]) .blyrics-unison-dock[data-position^="top-"] { … }

/* Pin bottom-anchored docks above the player bar in fullscreen */
#layout[player-fullscreened]:not([blyrics-dfs]) .blyrics-unison-dock[data-position^="bottom-"] {
  --dock-y-shift: var(--blyrics-fullscreen-bottom-dock-shift, -24px);
}

Override --blyrics-fullscreen-bottom-dock-shift to tune the lift distance.

20. Fullscreen Player Controls

In fullscreen (audio) mode, Better Lyrics adds a column under the album art with the song info and a set of playback controls: a seekable progress bar, previous/play-pause/next, like and dislike, and a quick-actions button. The progress bar and the button press animations are also used by the floating lyrics window. The controls strip hides during ads, and the “Fullscreen player controls” setting can turn it off.

Structure

#blyrics-fs-column
├── #blyrics-fs-info-row
│   ├── #blyrics-song-info               (title, artist, album)
│   └── .blyrics-fs-info-actions
│       ├── .blyrics-volume              (see Volume)
│       └── .blyrics-fs-more             (quick-actions trigger)
└── #blyrics-fs-controls
    ├── .blyrics-progress                (seek bar and times)
    │   └── .blyrics-progress__times
    └── .blyrics-fs-transport-slot
        ├── .blyrics-fs-like
        ├── previous / .blyrics-fs-play / next   (.blyrics-fs-disc)
        └── .blyrics-fs-dislike

Control Buttons

Every round button shares .blyrics-fs-disc. Restyle the whole set at once, or target individual buttons:

.blyrics-fs-disc {
  color: #fff;
}

/* the emphasized play/pause button */
.blyrics-fs-play {
  background: rgba(255, 255, 255, 0.16);
}

/* keep the quick-actions button lit while its menu is open */
.blyrics-fs-disc[data-menu-open] {
  background: rgba(255, 255, 255, 0.24);
}

Like and dislike show an outline icon by default and a filled icon when active. The active state is [data-active]:

.blyrics-fs-like[data-active] {
  color: #4fc3f7;
}

Progress Bar

The seek bar is .blyrics-progress. The filled portion is .blyrics-progress__fill, the handle is .blyrics-progress__knob, and the times sit in .blyrics-progress__times. The end time shows either the total duration or the time remaining; a click toggles it, and [data-mode] targets each state:

.blyrics-progress__fill {
  background: var(--blyrics-lyric-active-color);
}

.blyrics-progress__end[data-mode="remaining"] {
  opacity: 0.7;
}

Volume

The speaker icon is .blyrics-volume. Hovering it opens a slider that grows to the left, and a click mutes. In fullscreen it sits left of the quick-actions button, in .blyrics-fs-info-actions, and stays usable during ads. In the floating window it sits on the artwork overlay, in the bottom-right corner across from the like button (.blyrics-pip-artwork__control--like), and shows with the other artwork controls. Over the music video it sits at the end of the transport row instead and opens to the right.

HookMeaning
.blyrics-volume[data-open]The slider is showing
.blyrics-volume[data-muted]Sound is muted
.blyrics-volume[data-level]high, low or muted
.blyrics-volume__button / .blyrics-volume__iconSpeaker button and its icon
.blyrics-volume__icon [data-part]Icon parts that fade in and out: wave-low, wave-high, mute
.blyrics-volume__bar / __fill / __knobSlider track, fill and handle

Variables on .blyrics-volume:

.blyrics-volume {
  --blyrics-volume-width: 96px;        /* slider length when open */
  --blyrics-volume-rest-opacity: 0.6;  /* speaker opacity while closed, default 0.4 */
  --blyrics-volume-knob-size: 10px;    /* handle diameter, default 8px */
}

Better Lyrics sets --blyrics-volume-level (0 to 1) whenever the volume changes. Read it, but don’t set it.

Dim the speaker with opacity, not a translucent color. The icon has overlapping fill and stroke, so a translucent colour shows a lighter outline.

Hiding the Controls

The controls strip is #blyrics-fs-controls. It hides on its own during ads ([data-ad]) and when the fullscreen-controls setting is off (html[blyrics-no-fs-controls]). In Auto it also fades out while YouTube Music shows the player bar; when the setting is On, html[blyrics-fs-controls-pinned] is set and it stays visible. To hide it from your theme:

#blyrics-fs-controls {
  display: none;
}

21. Best Practices for Modifying CSS

When modifying this CSS:

  1. Start small and test often - It’s easier to fix one small change than many big ones
  2. Use comments to remind yourself why you made each change
  3. Be careful with structural elements - You might accidentally break the layout
  4. Follow naming conventions - Use blyrics- prefix for new class names
  5. Understand complex selectors - Make sure you don’t deviate from the original intent
  6. Experiment freely - Better Lyrics allows extensive customization
  7. Ask for help - Join the Better Lyrics Discord community if stuck
  8. Test responsive behavior - Check your changes on different screen sizes
  9. Consider performance - Avoid overly complex animations that might cause lag
  10. Have fun - CSS is about creativity and expression!

22. Importing/Exporting Styles

The Better Lyrics extension allows you to import and export custom CSS styles for sharing and backup purposes.

To export your custom CSS:

  1. Open the Better Lyrics extension
  2. Go to the “Edit CSS” section
  3. Click “Export to file” button
  4. Save the CSS file to your computer

To import custom CSS:

  1. Open the Better Lyrics extension
  2. Go to the “Edit CSS” section
  3. Click “Import from file” button
  4. Select the CSS file to import
  5. The styles will be applied immediately

Share your custom themes with the Better Lyrics community on Discord and get featured in the extension!

23. Additional Resources

To learn more about CSS and web development:

The best way to learn CSS is through experimentation and practice. Join the Better Lyrics Discord community for help, inspiration, and to share your creations!

24. Karaoke Subtitles

When the “Karaoke subtitles” option is on (it is on by default) and a music video plays in fullscreen, the video fills the screen and the synced lyrics show over it one line at a time, on a blurred plate. The Better Lyrics side panel and the fullscreen controls column stay hidden for as long as karaoke owns the fullscreen video, including while lyrics load and when a song has no lyrics or only unsynced ones.

While karaoke owns the layout, ytmusic-app-layout and #player-page carry the blyrics-karaoke attribute. The fullscreen rules from section 10 skip it with :not([blyrics-karaoke]), so use the attribute when a fullscreen rule of yours should also skip karaoke:

ytmusic-player-page[player-fullscreened]:not([blyrics-dfs], [blyrics-karaoke]) #blyrics-watermark {
  display: none;
}

Karaoke Structure

The overlay is a fixed layer added to the page the first time karaoke is used. The lyrics inside it are the normal .blyrics-container, laid out as a stage:

#blyrics-karaoke                                   ([data-bar], [data-title-card])
├── .blyrics-karaoke__stage
│   ├── .blyrics-karaoke__plate.blyrics-karaoke-surface   ([data-plate] when shown)
│   ├── .blyrics-karaoke__plate.blyrics-karaoke-surface
│   └── .blyrics-karaoke__mount
│       └── .blyrics-container[data-layout="stage"]
│           ├── .blyrics--line                     (one per lyric line)
│           └── .blyrics-credits                   (end card)
│               └── .blyrics-karaoke-source        (provider name and sync icon)
└── .blyrics-karaoke__title
    └── .blyrics-karaoke__card.blyrics-karaoke-surface
        ├── .blyrics-karaoke__card-title
        ├── .blyrics-karaoke__card-artist
        └── .blyrics-karaoke__card-credit

There are two plates so that when the next line is somewhere else on screen, a new plate can fade in there while the old one fades out. The title card shows the song, artist and songwriters during an intro, when the first sung line is at least five seconds in.

AttributeWhen applied
#blyrics-karaoke[data-bar]The YouTube Music player bar is showing; the lines lift above it
#blyrics-karaoke[data-title-card]The intro title card is showing; the stage is hidden
#blyrics-karaoke[data-card]A card is showing: title for the intro title card, end for the end card. Absent while sung lines show
.blyrics-karaoke__plate[data-plate]This plate is behind the line being sung
.blyrics-karaoke__plate[data-end-card]This plate is behind the end card (songwriter credits and source); it keeps the attribute while it fades out

Styling the Plate

Both plates and the title card share .blyrics-karaoke-surface, so one rule restyles the whole backdrop. --blyrics-karaoke-video-scale shrinks the video and the overlay together relative to the screen. It is a unitless number and defaults to 1. Below 1, the frame takes the video’s own aspect ratio, so a 4:3 or ultrawide video never shows black bars inside it:

:root {
  --blyrics-karaoke-video-scale: 0.94;
}

#blyrics-karaoke .blyrics-karaoke-surface {
  border-radius: 0.3em;
  background: rgb(0 0 0 / 70%);
  box-shadow: none;
  backdrop-filter: blur(12px);
}

Each backdrop can also be styled on its own: the intro card is .blyrics-karaoke__card, the plate behind sung lines is .blyrics-karaoke__plate:not([data-end-card]), and the plate behind the end card is .blyrics-karaoke__plate[data-end-card]:

#blyrics-karaoke .blyrics-karaoke__card {
  background: rgb(0 0 0 / 55%);
}

#blyrics-karaoke .blyrics-karaoke__plate:not([data-end-card]) {
  background: rgb(0 0 0 / 70%);
}

#blyrics-karaoke .blyrics-karaoke__plate[data-end-card] {
  background: rgb(20 20 40 / 80%);
}

The plate’s size and corners come from five variables. Set them on #blyrics-karaoke or any parent. Any unit works: em follows the lyric size, rem and px stay fixed. The plate grows around the text by its padding, and the lines are clipped to the same corners.

VariableDefaultWhat it does
--blyrics-karaoke-plate-padding-block0.18emSpace above and below a sung line
--blyrics-karaoke-plate-padding-inline0.55emSpace left and right of a sung line
--blyrics-karaoke-card-padding-block0.36emSpace above and below the end card
--blyrics-karaoke-card-padding-inline0.8emSpace left and right of the end card
--blyrics-karaoke-plate-radius0.46emCorner radius of both plates and the intro title card
#blyrics-karaoke {
  --blyrics-karaoke-plate-padding-block: 0.3em;
  --blyrics-karaoke-plate-radius: 0.6em;
}

The lyric size comes from --blyrics-font-size, the same as everywhere else. Line scaling is turned off inside karaoke: --blyrics-scale and --blyrics-active-scale are forced to 1 on #blyrics-karaoke .blyrics-container, so sung and waiting lines keep the same size.

Bars Around the Video

When a video’s shape doesn’t match the screen, YouTube Music fills the leftover space with black bars. In karaoke those bars are transparent, so they show whatever is behind the player: your theme’s background, or the album art backdrop if that option is on. The video keeps its full frame. To bring the black back:

ytmusic-player-page[video-mode][blyrics-karaoke] :is(#player, .html5-video-player) {
  background: #000 !important;
}

Stage Lines

Style lines as you do in the scrolling view. Colors, fonts and animations carry over. The stage only decides where each line sits. The stage marks each line with extra attributes:

AttributeMeaning
data-stage-role="current"The line being sung
data-stage-role="previous"The line that just finished
data-stage-role="queued"A line waiting its turn
data-stage-role="gone"A line that has left the stage
data-stage-visibleThe line is on screen; without it the line is visibility: hidden
.blyrics-container[data-stage-duet]The sung lines use both sides: v1 lines sit on the left, v2 and v3 on the right, v1000 stays centred. A song with one singer stays centred whichever voice it is

The stage fades lines in and out through --blyrics-stage-opacity, and it sets opacity from that property with !important. Do not set opacity on stage lines. It is overridden, so it cannot show a hidden line or hold one that is leaving.

25. Floating Window Music Video

With the “Show music video” option on, the floating lyrics window plays the music video in place of the album art. The video shows only while YouTube Music is in video mode. In song mode the window keeps the album art.

If karaoke subtitles are also on and the song has synced lyrics, the window switches to a stage layout: the video fills the window and the lines play over it, as in fullscreen karaoke. Unsynced lyrics, or karaoke turned off, keep the normal layout with the video in the art slot.

With the option on, ads are never mirrored. While one plays, the artwork and lyrics fade out and a countdown intermission takes their place.

Window States

All three attributes sit on .blyrics-pip-shell:

AttributeWhen applied
[data-video="on"]The music video is playing in the window
[data-video="ad"]An ad is playing; the intermission shows instead of the artwork and lyrics
[data-video="off"]No video: the option is off, YouTube Music is in song mode, or there is no video yet
[data-layout="stage"]The karaoke stage is showing over the video
[data-instant]A switch between song and video is in progress. Transitions are off until the new content settles, so nothing animates from the old layout

.blyrics-pip-artwork[data-video-face] is set while the visible artwork face holds the video rather than the cover.

Video Elements

SelectorWhat it is
.blyrics-pip-artwork__music-videoThe video on an artwork face. There is one per face, so the swap between cover and video uses the normal artwork transition
.blyrics-pip-stageFull-window layer behind the stage, shown with [data-layout="stage"]
.blyrics-pip-stage__videoThe video inside the stage layer

--blyrics-pip-video-aspect holds the video’s own size as W / H (for example 1920 / 1080). The extension sets it on .blyrics-pip-shell once the video reports its size. It is unset before that, and the window then falls back to 16 / 9. Read it in your own rules; setting it does nothing, because the inline value wins.

Karaoke Stage in the Window

The stage is a second #blyrics-karaoke overlay, built inside the floating window. It has the same structure, classes and attributes as section 24: the plates, .blyrics-karaoke-surface, the title card and the end card. Rules you write for #blyrics-karaoke apply to both. To change only the window, put .blyrics-pip-shell in front:

.blyrics-pip-shell #blyrics-karaoke .blyrics-karaoke-surface {
  background: rgb(0 0 0 / 55%);
}

The window sets its own plate size on .blyrics-pip-shell: a 0.75rem radius, 0.5rem by 0.75rem padding around a line, and 0.5rem by 1rem around the end card. They are in rem so the plate stays readable around the scaled down text. To change them in the window, set the same variables on .blyrics-pip-shell[data-layout="stage"] and use rem or px. The window reads them there to keep the lines clear of the progress row and the transport, so a value set only on #blyrics-karaoke or in em can let the plate run into the controls.

.blyrics-pip-shell[data-layout="stage"] {
  --blyrics-karaoke-plate-padding-block: 0.375rem;
}

While a card shows, the window sets data-stage-card on .blyrics-pip-shell (title or end, the same as data-card on the overlay) and --blyrics-pip-card-height, the card’s height in pixels. The transport uses them to stay above the card, and the header hides while the title card shows because the card repeats the title and artist. In stage windows 340px wide or less (not tall ones), the song and video pill also fades out during the title card so the transport fits beside the close button. Both are read-only.

The window scales the karaoke text down to its size. The factor is --blyrics-pip-karaoke-text-scale, a unitless number the extension sets on .blyrics-pip-shell when the window resizes. Inside the stage, --blyrics-font-size and --blyrics-translated-font-size are your values multiplied by it, so set those as usual and the window keeps them in proportion. Treat the factor as read-only: the transport controls place themselves with it, so an override moves the text but not the controls.

In a tall window (narrower than 4:5) the video sits at the top and the lines play in the space under it, with the plate background removed.

Ad Intermission

.blyrics-pip-intermission
├── .blyrics-pip-intermission__row
│   ├── .blyrics-pip-intermission__mark
│   └── .blyrics-pip-intermission__label      ("Ad playing")
├── .blyrics-pip-intermission__count
│   └── .blyrics-pip-intermission__glyph      (one per character, [data-digit] on digits)
├── .blyrics-pip-intermission__bar
│   └── .blyrics-pip-intermission__fill       (drains through transform: scaleX)
└── .blyrics-pip-intermission__next
    ├── .blyrics-pip-intermission__thumb
    └── span                                  ("Then <b>song</b>")

The intermission is always in the window and only shows under .blyrics-pip-shell[data-video="ad"]. The count, the bar and the next line are hidden when YouTube Music has no time or title for them. Each digit rolls in its own clip, and [data-digit] gives digits a fixed width so the countdown doesn’t shift as it changes.

.blyrics-pip-intermission__fill {
  background: var(--blyrics-lyric-active-color);
}

Window Controls

The hover controls on the artwork have a song and video toggle and a close button:

SelectorWhat it is
.blyrics-pip-mode-toggleSong and video pill. It is hidden when YouTube Music offers no switch for the current track, and it is not shown in windows under 130px tall
.blyrics-pip-mode-toggle__optionOne option; the current one has [aria-pressed="true"]
.blyrics-pip-artwork__control--closeCloses the window
.blyrics-pip-artwork__control--likeLikes the song; [data-active] when liked

The control discs and the pill share two variables, set on .blyrics-pip-artwork__controls:

VariableDefaultWhat it does
--blyrics-pip-control-tintrgb(0 0 0 / 28%)Background behind each control and the pill
--blyrics-pip-control-backdropblur(8px)backdrop-filter behind each control and the pill
.blyrics-pip-artwork__controls {
  --blyrics-pip-control-tint: rgb(255 255 255 / 12%);
  --blyrics-pip-control-backdrop: blur(16px);
}

--blyrics-pip-control-size sets the size of the previous and next buttons; play and pause is 1.25 times larger. It defaults to 36px, or 44px in tall windows, and is set on .blyrics-pip-artwork. The size stays the same when the window resizes. Instead, the artwork grows until the pill, the close button and the transport all fit inside it, at its own aspect ratio. In short windows the progress row under the artwork is hidden to make room. The karaoke stage sizes its own transport, so this variable only changes the normal layout.

.blyrics-pip-artwork {
  --blyrics-pip-control-size: 32px;
}

The stage layout uses more --blyrics-pip-* variables to place the transport, the plate and the progress row (-stage-width, -stage-height, -progress-inset, -progress-height, -plate-gap, -plate-overhang, -plate-top, -frame-bottom-gap, -header-bottom, -transport-margin, -transport-y, -karaoke-font-size, -karaoke-translated-font-size). They are internal. Each is derived from the others, so changing one can push the controls into the lines. The normal layout also has internal variables for its artwork minimum: --blyrics-pip-artwork-aspect, -artwork-min-height, -artwork-min-width, -chrome-height and -transport-edge.

Optional image highlights and HDR

The renderer supports theme-provided image fills. They are disabled by default. A theme opts in with /* blyrics-image-highlights = true; */ and supplies one CSS image URL through --blyrics-highlight-image on .blyrics-container. This works with SDR textures too; the renderer does not enable HDR or choose a brightness on its own.

The bundled HDR theme supplies a gain-map JPEG only inside HDR-display and CSS-support queries. Select it in the theme picker to enable brighter lyrics, the instrumental note, and image-colored glow. --dynamic-range controls the display-dependent brightness ceiling (the default is 70% standard, 30% no-limit). Glow follows the normal per-word duration, easing and blur radius settings, with --blyrics-image-glow-opacity-from and --blyrics-image-glow-opacity-to controlling its strength.

The glow blurs a separate copy after its karaoke mask, preserving the sharp fill and leaving room at word edges without padding changes. Letter masks and motion share the renderer’s clock. For fragmented long words and bidi-sensitive inline runs, a parallel text run keeps the halo aligned with every wrapped fragment. Joining-script and mixed-direction word groups use whole-word sweeps to preserve native text shaping. Forced colors retain system text colors. Missing note images fall back to the ordinary note fill.

A display’s HDR headroom and browser compositing affect the observed brightness. Screenshots can check animation and clipping, but cannot establish the monitor’s emitted luminance.