Skip to content

fix(remotion): keep themed text legible on light playbooks - #469

Open
bbudaedu wants to merge 1 commit into
calesthio:mainfrom
bbudaedu:fix/light-theme-text-contrast
Open

fix(remotion): keep themed text legible on light playbooks#469
bbudaedu wants to merge 1 commit into
calesthio:mainfrom
bbudaedu:fix/light-theme-text-contrast

Conversation

@bbudaedu

@bbudaedu bbudaedu commented Aug 3, 2026

Copy link
Copy Markdown

The defect

Three of the five shipped playbooks are light-background — clean-professional, minimalist-diagram, premium-minimalist — but several Remotion components render near-white text unconditionally.

The worst case is the burned-in captions. Explainer.tsx passes CaptionOverlay the theme's captionHighlightColor and captionBackgroundColor, but never its color, so the word color stays at CaptionOverlay's dark-theme default of #F8FAFC:

caption text #F8FAFC on the light caption bar  ->  1.05:1

WCAG AA for normal text is 4.5:1. Captions are an accessibility feature, and on the majority of playbooks they were invisible.

HeroTitle was worse: it took no color props at all and hardcoded #22D3EE / #F8FAFC / #A78BFA plus a dark scrim. SectionTitle and StatReveal hardcoded #F8FAFC for their secondary text. Measured on clean-professional:

Element Color On Contrast
Caption word #F8FAFC light caption bar 1.05:1
HeroTitle body #F8FAFC #FFFFFF 1.05:1
HeroTitle accent #22D3EE #FFFFFF 1.81:1

The repair

  • Explainer passes color={theme.textColor} to CaptionOverlay.
  • OverlayRenderer now receives the theme. It previously had no access to one, so no overlay could follow the theme even in principle.
  • SectionTitle, StatReveal and HeroTitle take a textColor prop; HeroTitle also takes accentColor / subtitleColor / scrimBackground.
  • Every new prop defaults to the exact value it replaced, so callers that do not thread a theme are unchanged. TalkingHead.tsx resolves no theme and renders identically.

The scrim had to flip too

HeroTitle's dark radial wash under a light theme composites #FFFFFF down to about #7B808A, which puts the theme's #1F2937 title at ~3.4:1 — the same legibility bug in reverse. Fixing only the text color would have left the hero title failing AA.

heroScrim() derives the wash from isLightColor(theme.backgroundColor), reusing the helper already present in Explainer.tsx, and reproduces the previous gradient byte-for-byte on dark themes.

Coverage

tests/contracts/test_theme_text_contrast_contract.py follows the source-text idiom of test_remotion_video_transition_contract.py for the wiring, and adds a behavioral check on top:

  • for every playbook, the theme _build_theme_from_playbook derives must clear WCAG AA against its own caption bar — compositing the translucent bar over the background the way the GPU would, and scoring it with the repo's own validate_contrast.

Current margins after the fix are 14.7–17.6:1.

Verification

  • 12 of the 16 new tests fail on the unfixed tree; all pass after.
  • Full suite: 964 → 980 passed, 11 skipped, no regressions.
  • tsc --noEmit clean under strict.

Independent of #468 — no overlapping files. The one skipped test is anime-ghibli, which cannot yield a theme on current main for the reason #468 fixes; it starts running on its own once either lands.

🤖 Generated with Claude Code

Three of the five shipped playbooks are light-background — clean-professional,
minimalist-diagram, premium-minimalist — but several components render
near-white text unconditionally.

Worst case is the burned-in captions. Explainer passed CaptionOverlay the
theme's `captionHighlightColor` and `captionBackgroundColor` but never its
`color`, so the word color stayed at CaptionOverlay's dark-theme default:

    caption text #F8FAFC on the light caption bar  ->  1.05:1
    (WCAG AA for normal text is 4.5:1)

Captions are an accessibility feature; on the majority of playbooks they were
invisible. HeroTitle was worse — it took no color props at all and hardcoded
#22D3EE / #F8FAFC / #A78BFA plus a dark scrim. SectionTitle and StatReveal
hardcoded #F8FAFC for their secondary text.

- Explainer passes `color={theme.textColor}` to CaptionOverlay.
- OverlayRenderer now receives the theme; it previously had no access to one,
  so no overlay could follow the theme even in principle.
- SectionTitle, StatReveal and HeroTitle take a `textColor` prop; HeroTitle
  also takes accentColor/subtitleColor/scrimBackground.
- Every new prop defaults to the exact value it replaced, so callers that do
  not thread a theme are unchanged — TalkingHead.tsx resolves no theme and
  renders identically.

The scrim needed to flip too. HeroTitle's dark radial wash under a light
theme's dark title composites to ~#7B808A, putting #1F2937 text at ~3.4:1 —
the same legibility bug in reverse. `heroScrim()` derives the wash from
`isLightColor(theme.backgroundColor)`, reusing the helper already in
Explainer.tsx, and reproduces the previous gradient exactly for dark themes.

Coverage follows the source-text idiom of
test_remotion_video_transition_contract.py for the wiring, and adds a
behavioral check on top: for every playbook, the theme `_build_theme_from_playbook`
derives must clear WCAG AA with its own caption bar, compositing the
translucent bar over the background the way the GPU would. Current margins are
14.7-17.6:1.

Verified: 12 of the 16 new tests fail on the unfixed tree; full suite goes
964 -> 980 passed with no regressions; `tsc --noEmit` clean under strict.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant