Skip to content

Theming

approved

The player's visual system: one theme, a fixed token set, shipped typography.

Design-value authority

The checked-in video.js playground is the read-only donor for numeric visual values: sizing formulas, spacing, radii, and typography measurements. Its current declarations supersede stale numeric examples in stories or older design notes. That authority is deliberately narrow: repository scope, behavior, documented product deviations, DOM and component ownership, and the working agreement remain governed by the player specification, LT-1652, and AGENTS.md. A conflict in those non-numeric rules stops for review instead of being resolved from the playground.

One theme

The player has exactly one appearance: the light theme. There is no dark variant, no theme switching, and no theme key in the configuration. Surface tones are fixed per component by the design: the over-video chrome is dark by nature, the context panel is a white card, the share dialog is a frosted dark card. None of that varies at runtime.

Design tokens

The primitive ramps come from the checked-in playground and live on the player root. New semantic player colors derive from these primitives; they are the names components consume. Existing LT semantic aliases without an exact ramp match remain documented bespoke exceptions rather than being silently moved to a nearby stop. The context-panel palette remains the separately bound shorts-context/Figma palette documented below.

Colors

RampStopValue
Neutral50hsl(0, 0%, 100%)
Neutral100hsl(0, 0%, 95%)
Neutral200hsl(0, 0%, 90%)
Neutral300hsl(202, 5%, 67%)
Neutral400hsl(0, 0%, 60%)
Neutral500hsl(0, 0%, 50%)
Neutral600hsl(0, 0%, 40%)
Neutral700hsl(197, 5%, 26%)
Neutral800hsl(0, 0%, 17.5%)
Neutral900hsl(220, 4%, 16%)
Neutral1000hsl(0, 0%, 0%)
Primary100hsl(48, 75%, 95%)
Primary200hsl(48, 75%, 82.5%)
Primary300hsl(48, 75%, 70%)
Primary400hsl(48, 75%, 60%)
Primary500hsl(60, 65%, 56%)
Primary600hsl(60, 65%, 46%)
Primary700hsl(60, 47%, 36%)
Primary800hsl(48, 75%, 17.5%)
Primary900hsl(48, 75%, 7.5%)
Accent100hsl(183, 100%, 80%)
Accent200hsl(183, 100%, 75%)
Accent300hsl(183, 100%, 70%)
Accent400hsl(183, 100%, 60%)
Accent500hsl(183, 100%, 50%)
Accent600hsl(201, 87%, 25%)
Accent700hsl(201, 87%, 21%)
Accent800hsl(201, 87%, 15%)
Accent900hsl(201, 87%, 10%)

Accessibility contrast

Rendered semantic pairs are tested against WCAG 2.2 rather than treating an isolated token as accessible. Normal text must reach 4.5:1; meaningful graphical and focus indicators must reach 3:1 against their adjacent color. Disabled controls and purely decorative marks are excluded.

Surface or roleForeground / indicatorAdjacent surfaceMinimumAudited result
Over-video controls and floating panelsNeutral 50Neutral 9004.5:114.35:1
Subdued floating-panel textNeutral 300Neutral 9004.5:16.26:1
Primary controls and active marksNeutral 900Primary 5004.5:19.45:1
Context-panel headings and linksContext accent #074566Neutral 504.5:110.23:1
Context-panel timestampsContext time #3f70bdNeutral 504.5:14.93:1
Context-panel metadataContext muted #687685Neutral 504.5:14.65:1
Context-panel unselected tabsContext subtle #6c717aNeutral 504.5:14.91:1
Caption text over a bright video frameNeutral 900Primary 500 at 0.8 over Neutral 504.5:110.26:1
Dark-surface focus indicatorNeutral 300Neutral 9003:16.26:1
Primary focus and copy-field indicatorsPrimary 500Neutral 9003:19.45:1
Context-panel focus indicatorContext accent #074566Neutral 503:110.23:1
Context header glyphContext accent soft #487ed1Context surface soft #f2f7fa3:13.76:1
Context close glyphContext close #7c8798Neutral 503:13.64:1

The shared --ltp-focus-ring-color is the dark-surface indicator. The context panel locally remaps it to --ltp-color-context-accent so keyboard focus remains visible on its light surfaces. Components may suppress the shared outline only when they provide an equivalent token-backed indicator, such as the cover affordance or Share copy-field ring.

Translucent semantic colors use relative HSL, for example hsl(from var(--ltp-neutral-900) h s l / 0.96). An opaque declaration precedes every relative-color declaration as the fallback for browsers that do not support that syntax. Baked eight-digit hexadecimal alpha values are not part of the token contract.

Retained bespoke semantic aliases

The following pre-LT-1653 values have no exact equivalent in the playground ramps. They remain fixed semantic exceptions so adopting the primitive inventory does not silently change a current or future surface.

Semantic token or groupRetained value(s)
primary-pale, primary-dark#e2e4a9, #757535
accent-dark, accent-3#1a1a1a, #3e4345
secondary, secondary-400, secondary-200, secondary-light#00f4ff, #92cbcb, #bbdbdd, #eaf5f5
neutral-50, neutral-100, neutral-200, neutral-300, neutral-500#ededed, #f5f5f5, #e3e3e3, #dbdbdb, #73797c
text-secondary, text-tertiary#73797c, #b2b3b3
surface-grey, surface-grey-dark#f5f5f5, #e3e3e3
Primary disabled surface/text#e2e4a9, #b2b3b3
Secondary surface and disabled surface/text#e3e3e3, #f5f5f5, #b2b3b3

These values are inherited LT semantics, not additions to the donor ramps. Their rendered contrast and appearance are unchanged by LT-1653. A later remap requires the same evidence and contrast ledger as any other semantic change.

Floating surfaces

SurfaceBackground and effects
Tooltip and timeline previewNeutral 900 at 0.96 alpha; no blur
Chapters and captions menusNeutral 900 at 0.96 alpha; no blur
Share dialogNeutral 900 at 0.96 alpha; 14px backdrop blur; 1px neutral 50 at 0.08 border; 0 30px 50px neutral 1000 at 0.42 shadow
Share dialog without blurOpaque neutral 900 fallback; the border and shadow remain
Context side card and dialogOpaque white; no blur
Context-card play affordanceIts existing local 0.1875rem blur
Scrims, captions, feedback, and state surfacesPreserve their existing semantic treatment; they are not frosted panels

The standalone player has no surface corresponding to the playground shorts header's 18px frost; that remains feed work. Menus deliberately remain translucent but unblurred. The share card adopts the playground overlay-dialog frost while retaining its LT-specific dimensions and lower-left radius. The context panel stays the separately bound opaque white shorts-context surface.

System colors

TokenValueLight tint
Error#de776c#faebe9
Warning#bea61f#f5f2de
Success#3ba06f#e7f3ee
Progress#d58854#f8ece2
Ready#5c8dd6none defined

Buttons

ButtonSurfaceTextClick stateDisabled surfaceDisabled text
PrimaryPrimary 500Neutral 900Neutral 900 at 0.2Bespoke #e2e4a9Bespoke #b2b3b3
SecondaryBespoke #e3e3e3Neutral 900Neutral 900 at 0.1Bespoke #f5f5f5Bespoke #b2b3b3
DestructiveSystem errorNeutral 50Neutral 900 at 0.1Error lightNeutral 50

Spacing

The spacing scale is 0.25rem, 0.5rem, 1rem, 1.25rem, and 1.5rem. Component spacing taken from the playground stays in rem, including intermediate donor values such as 0.35rem, rather than being rounded onto this convenience scale.

Fluid sizing

The player root is the inline-size query container. Fluid tokens use the playground's rendered clamp(min, base + Ncqi, max) declarations unchanged; component styles consume the tokens instead of repeating formulas.

RoleBinding valuePlayground declaration
Standard control/iconclamp(1.875rem, 0.4545rem + 6.0606cqi, 2.5rem)_controls.scss
Time/chapter labelclamp(1.125rem, 0.75rem + 0.8333cqi, 1.25rem)_controls.scss
Controls insetclamp(0.625rem, -0.0543rem + 2.8986cqi, 1.25rem)_controls.scss
Feedback paddingclamp(0.5rem, 2.5cqi, 0.875rem)_input-feedback.scss
Feedback iconclamp(1.5rem, 7cqi, 2.5rem)_input-feedback.scss
Landscape cover authorclamp(1rem, 0.592rem + 1.739cqi, 3.375rem)_cover.scss
Landscape cover titleclamp(1rem, 0.271rem + 3.112cqi, 5.25rem)_cover.scss
Portrait cover authorclamp(1rem, 0.723rem + 1.585cqi, 2.625rem)_cover--portrait.scss
Portrait cover titleclamp(1rem, 0.402rem + 3.415cqi, 4.5rem)_cover--portrait.scss
Caption text, landscapeclamp(1.05rem, 2.25cqi, 1.6rem)_captions.scss
Caption text, portraitclamp(0.96rem, 3.8cqi, 1.28rem)_captions.scss

cqi and cqb are allowed only for player-owned surfaces whose query container is the player. Portaled and browser top-layer surfaces use rem, percentages, their measured available width, or explicit viewport geometry supplied by the owner; they never resolve fluid sizing against the browser viewport by accident. round(nearest, ..., 1px) snaps percentage-derived timeline fill, buffer-range, thumb, and chapter-segment edges, where fractional pixels can otherwise produce visible seams. Hairline borders and deliberately fixed panel geometry may remain in px. Fluid spacing and typography are never pixel-rounded.

Retained values without a donor

When the playground has no equivalent surface, LT keeps the existing value and records why rather than inventing a donor mapping.

SurfaceRetained LT valueReason
Buffering spinnerControl-sized ring with 0.25rem strokeNo playground-owned buffering presentation equivalent
Error presentation32ch message, 1rem gap/padding, compact primary RetryNo playground-owned error presentation equivalent
Captions menu behaviorOff row, localized labels, available-size anchoringNo dedicated playground captions menu; visual shell inherits chapter-popup values
Share announcementVisually hidden live region geometryLT accessibility-only surface
Share top-layer boundsOwner-supplied center plus percentage/max-height constraintsNative top-layer architecture is a deliberate LT deviation
Chapter menu width constraintmin(16rem, available player width)Preserves the donor cap without its viewport-relative fallback

Context panel

The responsive context panel uses semantic measurements bound by its capability document:

TokenValue
Side-card minimum width420px
Side-card maximum width650px
Side-card minimum gap2rem
Side-card maximum gap12rem
Side-card preferred gap12% of the presentation stage
Side-card outer allowance2rem
Desktop header padding0.9rem 1rem 0.85rem
Desktop body inset1.15rem 1rem 1rem
Dialog viewport inset8px plus safe areas
Dialog header padding1rem
Dialog body inset1.5rem 1rem 1rem
Header glyph / close control1.5rem / 2.25rem
Desktop / dialog radius1rem / 0.5rem
Side-card fade220ms ease
Side-card slide340ms cubic-bezier(0.22, 1, 0.36, 1)

These are internal design tokens, not consumer override points.

Selection and user-select

Selection inside the player uses Primary 500 behind Neutral 900 text. Selection remains available only where copying authored or generated text is a legitimate task:

  • context-panel reading content, including transcript and publication metadata;
  • the Share dialog's read-only URL and embed-code fields.

Player chrome, the cover identity, rendered captions, menus, labels, status and error text, and interactive elements are not selectable. This prevents accidental drag selection without removing selection from the two reading/copying surfaces.

Motion

TokenValueConsumers
Fade duration220msLandscape chrome, menus, context-panel opacity
Movement duration340msContext-panel horizontal entrance
Entrance easingcubic-bezier(0.22, 1, 0.36, 1)Menus and context-panel movement
Menu entrance offset1remChapters and captions menus

Chapters and captions menus enter from opacity zero and translateY(1rem) over the fade duration with the entrance easing. The context panel combines the fade duration with the movement duration. Reduced-motion preference removes entrance/movement animation and makes visibility changes immediate. Persistent will-change declarations are forbidden; an animating surface may use it only for the active animation interval.

Intentional visual changes in LT-1653

Surface or semanticPrevious valueLT-1653 valueContrast resultEvidence
Share dialogOpaque #27282aNeutral 900 at 0.96 with the documented frostExisting light text remains above AA; automated translucent-surface checks passPlayground _overlays.scss overlay-dialog panel
Chapters and captions menu backgroundsOpaque #27282aNeutral 900 at 0.96, without blurExisting light text remains above AA; automated translucent-surface checks passPlayground _overlays.scss and _slider.scss floating surfaces
Landscape control fade250msShared 220ms fadeNot applicable to color contrastPlayground shared fade timing
Chapters and captions menu entranceNo entrance motion220ms, translateY(1rem), shared easingNot applicable to color contrastPlayground popup entrance motion
--ltp-color-neutral-400#b2b3b3Neutral 300, hsl(202, 5%, 67%) (approximately #a7acaf)7.02:1 to 6.41:1 against Neutral 900; both exceed WCAG AAExact donor Neutral 300 declaration in src/css/theme/_colors.scss
--ltp-color-text-subdued-on-dark#a8adb0Neutral 300, hsl(202, 5%, 67%) (approximately #a7acaf)6.51:1 to 6.41:1 against Neutral 900; both exceed WCAG AAExact donor Neutral 300 declaration in src/css/theme/_colors.scss
Context time#487ed1#3f70bd4.05:1 to 4.93:1 against Neutral 50; now exceeds WCAG AAAccessibility correction to the bound Figma context semantic
Context muted#6f7e8d#6876854.16:1 to 4.65:1 against Neutral 50; now exceeds WCAG AAAccessibility correction to the bound Figma context semantic
Context subtle#7a7f89#6c717a4.02:1 to 4.91:1 against Neutral 50; now exceeds WCAG AAAccessibility correction to the bound Figma context semantic
Context-panel focus ringNeutral 300Context accent #0745662.29:1 to 10.23:1 against Neutral 50; now exceeds WCAG 1.4.11Accessibility remap to the bound Figma context accent
Chapter-aware timeline preview drop shadowBlack at 0.32Black at 0.32; the proposed 0.25 remap was rejectedNot applicable; this is a non-content elevation shadowExact rgba(0, 0, 0, 0.32) preview shadow in playground _slider.scss

No other semantic color changes are authorized without recording the previous value, new value, contrast result, and donor or Figma evidence here first.

Typography

The player's typeface is Nunito Sans, one family for every player text, shipped inside the package under the SIL Open Font License with a generic system fallback stack. This is a deliberate decision superseding the Inter, Inter Tight, and SF Pro entries found in the design file's variables; implementation must not reintroduce those families.

The type scale carries over from the design tokens with the family swapped:

StyleSize / line heightWeightTracking
Title 12rem / 2.125rem6000
Title 21.625rem / 2rem6000
Title 31.25rem / 1.75rem6000
Headline1rem / 1.625rem5000
Body1rem / 1.5rem400-0.025rem
Body medium1rem / 1.5rem500-0.025rem
Footnote0.8125rem / 1.25rem400-0.0125rem
Footnote medium0.8125rem / 1rem500-0.0125rem
Caption0.75rem / 1rem400-0.0125rem
Small button0.875rem / 1rem600-0.0125rem

The shared weight scale is light 300, normal 400, medium 500, semibold 600, and bold 700. The base line-height scale is tight 1, text 1.2, body 1.5, and relaxed 1.65; role-specific token values may use another exact donor ratio where required.

All weights the player uses (300, 400, 500, 600, 700) ship, subset for the languages the player serves. The package vendors the normal Latin and Latin Extended WOFF2 subsets from @fontsource/nunito-sans 5.3.0, whose metadata identifies the upstream font as Nunito Sans v19. Each weight and subset is a separate file so browsers fetch only the glyph range they need. The SIL Open Font License 1.1 ships beside the font files and is included in the package's generated third-party notices.

Icons

Icons ship as an SVG sprite inside the player. One canonical set serves both layouts; landscape has only the six source-specific exceptions named in the layouts document. Icon colors come from the token set.

Consumer overrides

None. The player is visually locked: configuration controls what is shown, never how it looks. This is deliberate brand protection, consistent with the license. A sanctioned set of CSS custom properties may become a future, documented addition if a real consumer need appears; nothing is exposed today.