fix(remotion): keep themed text legible on light playbooks - #469
Open
bbudaedu wants to merge 1 commit into
Open
Conversation
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.
This was referenced Aug 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.tsxpassesCaptionOverlaythe theme'scaptionHighlightColorandcaptionBackgroundColor, but never itscolor, so the word color stays at CaptionOverlay's dark-theme default of#F8FAFC:WCAG AA for normal text is 4.5:1. Captions are an accessibility feature, and on the majority of playbooks they were invisible.
HeroTitlewas worse: it took no color props at all and hardcoded#22D3EE/#F8FAFC/#A78BFAplus a dark scrim.SectionTitleandStatRevealhardcoded#F8FAFCfor their secondary text. Measured onclean-professional:#F8FAFC#F8FAFC#FFFFFF#22D3EE#FFFFFFThe repair
Explainerpassescolor={theme.textColor}toCaptionOverlay.OverlayRenderernow receives the theme. It previously had no access to one, so no overlay could follow the theme even in principle.SectionTitle,StatRevealandHeroTitletake atextColorprop;HeroTitlealso takesaccentColor/subtitleColor/scrimBackground.TalkingHead.tsxresolves no theme and renders identically.The scrim had to flip too
HeroTitle's dark radial wash under a light theme composites#FFFFFFdown to about#7B808A, which puts the theme's#1F2937title 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 fromisLightColor(theme.backgroundColor), reusing the helper already present inExplainer.tsx, and reproduces the previous gradient byte-for-byte on dark themes.Coverage
tests/contracts/test_theme_text_contrast_contract.pyfollows the source-text idiom oftest_remotion_video_transition_contract.pyfor the wiring, and adds a behavioral check on top:_build_theme_from_playbookderives 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 ownvalidate_contrast.Current margins after the fix are 14.7–17.6:1.
Verification
tsc --noEmitclean under strict.Independent of #468 — no overlapping files. The one skipped test is
anime-ghibli, which cannot yield a theme on currentmainfor the reason #468 fixes; it starts running on its own once either lands.🤖 Generated with Claude Code