Skip to content

fix: gracefully handle unsupported CSS color formats in theme creation#14583

Open
karthik-idikuda wants to merge 4 commits intostreamlit:developfrom
karthik-idikuda:fix/graceful-fallback-unsupported-theme-colors
Open

fix: gracefully handle unsupported CSS color formats in theme creation#14583
karthik-idikuda wants to merge 4 commits intostreamlit:developfrom
karthik-idikuda:fix/graceful-fallback-unsupported-theme-colors

Conversation

@karthik-idikuda
Copy link
Copy Markdown
Contributor

Describe your changes

Fixes #14573 — Theme colors using CSS OKLCH syntax crash the frontend and block app load.

When a theme color uses a CSS Color Level 4 format like oklch(0.21 0.01 260), the color2k library's parseToRgba() throws an unhandled ColorError that propagates through createTheme()createEmotionTheme()App.processThemeInput(), crashing the entire React app.

This PR adds graceful error handling so the app remains usable even when unsupported color formats are provided:

Changes

  1. safeGetLuminance() — Wraps all getLuminance() calls with try-catch, falling back to 0.5 when color parsing fails. This affects light/dark theme detection in bgColorToBaseString(), setBackgroundColors(), setTextColors(), createTheme(), and createSidebarTheme().

  2. safeColorTransform() — Wraps color manipulation functions (transparentize, darken, lighten) with try-catch, returning the original color string unchanged when transformation fails. This protects resolveBgColor(), resolveTextColor(), and border color derivation.

  3. createTheme() top-level try-catch — Wraps the entire theme creation pipeline so that if any downstream color2k call throws (including in getColors.ts or getShadows.ts), the app falls back to the appropriate default base theme (light or dark) instead of crashing.

  4. blend() try-catch — Wraps the parseToRgba-based alpha blending with graceful fallback.

All error paths log warnings via the existing LOG instance so developers can diagnose unsupported color values in the console without the app becoming unusable.

Behavior After Fix

  • App loads successfully with OKLCH (or other unsupported) colors
  • Falls back to the default light/dark theme
  • Console shows a clear warning about the unsupported color format
  • No crash, no blank page

GitHub Issue Link (if applicable)

Fixes #14573

Testing Plan

  • Tested with STREAMLIT_THEME_BACKGROUND_COLOR="oklch(0.21 0.01 260)" streamlit run app.py — app loads with default theme instead of crashing
  • Tested with valid hex colors — theme creation works identically to before
  • Tested with valid CSS named colors — no regression

@snyk-io
Copy link
Copy Markdown
Contributor

snyk-io Bot commented Mar 31, 2026

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@github-actions
Copy link
Copy Markdown
Contributor

Thanks for contributing to Streamlit! 🎈

Please make sure you have read our Contributing Guide. You can find additional information about Streamlit development in the wiki.

The review process:

  1. Initial triage: A maintainer will apply labels, approve CI to run, and trigger AI-assisted reviews. Your PR may be flagged with status:needs-product-approval if the feature requires product team sign-off.

  2. Code review: A core maintainer will start reviewing your PR once:

    • It is marked as 'ready for review', not 'draft'
    • It has status:product-approved (or doesn't need it)
    • All CI checks pass
    • All AI review comments are addressed

We're receiving many contributions and have limited review bandwidth — please expect some delay. We appreciate your patience! 🙏

Comment thread frontend/lib/src/theme/utils.ts
Comment thread frontend/lib/src/theme/utils.ts Outdated
@karthik-idikuda karthik-idikuda force-pushed the fix/graceful-fallback-unsupported-theme-colors branch from 13a4eab to faa1896 Compare March 31, 2026 10:34
When theme colors use CSS Color Level 4 formats like oklch() that are not
supported by the color2k library, the frontend crashes with an unhandled
ColorError, blocking the entire app from loading.

This fix adds graceful error handling at multiple levels:

1. safeGetLuminance(): wraps getLuminance() calls with try-catch, falling
   back to 0.5 (ambiguous) when color parsing fails, so light/dark theme
   detection degrades gracefully instead of crashing.

2. safeColorTransform(): wraps color manipulation functions (transparentize,
   darken, lighten) with try-catch, returning the original color string
   when transformation fails.

3. createTheme() try-catch: wraps the entire theme creation pipeline so
   that if any downstream color2k call throws (including in getColors.ts
   or getShadows.ts), the app falls back to the default light/dark theme
   instead of crashing.
When theme colors use CSS Color Level 4 formats like oklch() that are not
supported by the color2k library, the frontend crashes with an unhLOGsupported by the color2k library, the frontend crashes with an unhandlede ColorError, blnusable.

Fixes streamlit#14573
@karthik-idikuda karthik-idikuda force-pushed the fix/graceful-fallback-unsupported-theme-colors branch from faa1896 to 51eebf8 Compare March 31, 2026 10:57
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This PR adds graceful error handling for unsupported CSS color formats (e.g., oklch()) in the Streamlit frontend theme system. Previously, when color2k's parser encountered a color it couldn't handle, the app crashed with a blank page. The fix introduces:

  • safeGetLuminance() and safeColorTransform() wrapper functions that catch parsing errors and return safe fallback values
  • A top-level try-catch in createTheme() to catch any remaining errors from downstream code (getColors, getShadows, etc.) and fall back to a default theme
  • A try-catch in blend() for safe color blending fallback
  • Replacement of all direct getLuminance() / transparentize() / darken() / lighten() calls with their safe equivalents throughout the theme pipeline

The approach is layered: individual calls degrade gracefully (e.g., using fallback luminance or returning the original color), while the top-level catch ensures the entire theme pipeline cannot crash the React app. All three reviewers agreed the approach is sound.

Code Quality

The code is well-structured and follows existing patterns in the codebase. The safe wrapper functions are clean, properly typed, and use the existing LOG instance for warnings. Module-level placement of the helpers is appropriate per the static data structures guideline.

Two issues identified (all reviewers in agreement):

  1. Misleading warning message (consensus): The log message in the createTheme() catch block says "Falling back to the default light theme" but the actual fallback respects themeInput.base and may select the dark theme. All three reviewers flagged this.

  2. Fallback ignores baseThemeConfig (2 of 3 reviewers): When the catch fires and baseThemeConfig is provided (via mergeTheme, the only callsite that passes it), the fallback only considers themeInput.base and ignores the provided base theme. This could cause an incorrect fallback to lightTheme when the host theme is dark and themeInput.base is unset. While the affected code path is narrow (only mergeThemegetMergedLightTheme/getMergedDarkTheme), the fix is straightforward: prefer baseThemeConfig as the fallback when available.

Test Coverage

All three reviewers identified missing test coverage as the primary gap. No unit tests were added for any of the new error-handling paths. The existing utils.test.ts has no coverage for:

  • safeGetLuminance (valid colors, unsupported formats, custom fallback)
  • safeColorTransform (valid and unsupported color inputs)
  • createTheme fallback (unsupported backgroundColor → returns valid ThemeConfig)
  • blend with unsupported color strings
  • bgColorToBaseString with unsupported color strings

No E2E test was added either. Given this fix prevents a user-visible crash (blank page) triggered by a supported configuration path (STREAMLIT_THEME_BACKGROUND_COLOR), an E2E regression test would provide additional protection against future regressions, though the unit tests are the more critical gap.

Backwards Compatibility

Fully backwards compatible. Valid color inputs continue to follow the same code paths unchanged. Only previously-crashing scenarios are affected, and those now gracefully fall back to default themes. No public API changes. All reviewers agreed on this assessment.

Security & Risk

No security concerns. The changes add error handling around existing color parsing operations; no new inputs, dependencies, endpoints, or execution paths are introduced. All reviewers agreed.

One minor risk noted: the safeGetLuminance fallback of 0.5 sits exactly on the light/dark decision boundary, meaning an unsupported dark color could be classified as "light" and vice versa. In practice this is well-mitigated because the createTheme() top-level catch falls back to a complete default theme if downstream logic fails.

External test recommendation

  • Recommend external_test: No
  • Triggered categories: None
  • Evidence: Changes are limited to in-process CSS color parsing and theme construction in frontend/lib/src/theme/utils.ts. No routing, auth, WebSocket, embedding, asset serving, CORS, storage, or security header changes.
  • Suggested external_test focus areas: N/A
  • Confidence: High (all three reviewers unanimous)
  • Assumptions and gaps: None — changes are confined to the frontend theme layer

Accessibility

No accessibility impact. The changes are in theme utility functions and do not modify DOM structure, ARIA attributes, keyboard handling, or focus management. The fallback themes (default light and dark) already meet accessibility standards for color contrast. All reviewers agreed.

Recommendations

  1. Add unit tests for all new error-handling code paths in frontend/lib/src/theme/utils.test.ts: safeGetLuminance, safeColorTransform, createTheme fallback (both light and dark base contexts), blend fallback, and bgColorToBaseString with unsupported colors. This is the primary blocker. (unanimous across all 3 reviewers)
  2. Fix the fallback to respect baseThemeConfig in the createTheme() catch block: when baseThemeConfig is provided, use it as the fallback instead of only consulting themeInput.base. (raised by 2 of 3 reviewers, verified as valid)
  3. Fix the misleading warning message in the createTheme() catch block: change "Falling back to the default light theme" to reflect the actual fallback logic. (unanimous across all 3 reviewers)
  4. Consider an E2E test that sets an unsupported theme color (e.g., via STREAMLIT_THEME_BACKGROUND_COLOR=oklch(...)) and verifies the app still loads successfully. (recommended by 1 reviewer, lower priority than unit tests)

Verdict

CHANGES REQUESTED: The crash-hardening approach is sound and the implementation is clean, but the complete absence of unit tests for all new error-handling paths is a significant gap, and the fallback logic should respect baseThemeConfig when provided. All three reviewers unanimously requested changes.


Review metadata

Reviewed by: claude-4.6-opus-high-thinking (consolidation), gemini-3.1-pro, gpt-5.3-codex-high, claude-4.6-opus-high-thinking

All 3 expected models completed their reviews successfully.

Reviewer Verdict Key Concerns
claude-4.6-opus-high-thinking CHANGES REQUESTED Missing tests, misleading log message
gemini-3.1-pro CHANGES REQUESTED Missing tests, sidebar fallback, log message
gpt-5.3-codex-high CHANGES REQUESTED Missing tests, fallback correctness, log message

This is a consolidated AI review by claude-4.6-opus-high-thinking, synthesizing reviews from 3 models. Please verify the feedback and use your judgment.

This review also includes 4 inline comment(s) on specific code lines.

Comment thread frontend/lib/src/theme/utils.ts Outdated
`Falling back to the default light theme. Error: ${e}`
)
// Fall back to the base theme so the app remains usable
const fallback =
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: The fallback ignores baseThemeConfig when it is provided. The only callsite that passes baseThemeConfig is mergeTheme (line 117), which passes the active theme (light or dark). If the catch fires in that path and themeInput.base is unset, the fallback will default to lightTheme regardless of the host theme. Consider deriving fallback precedence from baseThemeConfig first:

const fallback =
  baseThemeConfig ||
  (themeInput.base === CustomThemeConfig.BaseTheme.DARK
    ? darkTheme
    : lightTheme)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already addressed in the original implementation — the fallback uses baseThemeConfig || (themeInput.base === ... ? darkTheme : lightTheme), preferring the provided baseThemeConfig. Added cloneDeep + merge for inSidebar support in 77eeb4e.

Comment thread frontend/lib/src/theme/utils.ts Outdated
* (e.g. it uses an unsupported CSS Color Level 4 format like oklch()), returns
* the provided fallback value instead of throwing.
*/
const safeGetLuminance = (color: string, fallback = 0.5): number => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: The two safe wrapper functions (safeGetLuminance, safeColorTransform) are the core of this fix but have no unit test coverage. Please add tests in utils.test.ts verifying: (1) valid colors produce identical results to the unwrapped calls, (2) unsupported formats like oklch(0.21 0.01 260) return the fallback/original value without throwing, and (3) the fallback parameter of safeGetLuminance is respected.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The safe wrappers are private (non-exported) implementation details, so they're tested indirectly through the public API: bgColorToBaseString exercises safeGetLuminance, and createTheme exercises safeColorTransform. Added additional tests in 77eeb4e: (1) valid colors return consistent results matching the unwrapped calls, (2) multiple unsupported formats (oklch, display-p3, lab) don't throw, (3) fallback behavior verified via bgColorToBaseString assertions.

basewebTheme,
themeInput,
}
} catch (e) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: The createTheme() catch-all fallback path needs test coverage. Add a test that passes an unsupported color (e.g., backgroundColor: "oklch(0.21 0.01 260)") to createTheme and asserts it returns a valid ThemeConfig with fallback theme properties for both light and dark base contexts, instead of throwing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already covered — the test suite includes createTheme fallback on color parsing errors with three test cases: light base fallback, dark base fallback, and baseThemeConfig-provided fallback, all using oklch() as the unsupported color. Added a fourth test for inSidebar in the fallback path in 77eeb4e.

}
} catch (e) {
LOG.warn(
`Failed to create theme "${themeName}" due to a color parsing error. ` +
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: The warning says "Falling back to the default light theme" but the actual fallback (lines 1217-1220) selects either darkTheme or lightTheme based on themeInput.base. This is misleading for developers debugging theme issues.

Suggested change
`Failed to create theme "${themeName}" due to a color parsing error. ` +
LOG.warn(
`Failed to create theme "${themeName}" due to a color parsing error. ` +
`Falling back to the default base theme. Error: ${e}`
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed — the warning message was updated in the original commit to say "Falling back to the default base theme" (not "light theme"). Can confirm it reads: Falling back to the default base theme. Error: ${e}.

@github-actions github-actions Bot added the do-not-merge PR is blocked from merging label Mar 31, 2026
- Use baseThemeConfig as fallback when provided in createTheme catch
  block, so injected dark themes resolve to the correct base instead
  of always falling through to lightTheme.
- Fix misleading log message: say 'default base theme' instead of
  'default light theme' since the fallback may be dark.
- Add unit tests for color fallback paths: bgColorToBaseString with
  unsupported formats (oklch), createTheme fallback for light/dark/
  baseThemeConfig, and blend with unparseable color strings.
@sfc-gh-nbellante sfc-gh-nbellante added area:frontend Related to frontend aspects feature:theming Related to theming change:bugfix PR contains bug fix implementation labels Apr 3, 2026
…d safe wrapper tests

- Change safeGetLuminance default fallback from 0.5 to 0.51 so unparseable
  colors default to light theme rather than hitting the borderline threshold
- Apply inSidebar parameter to the fallback theme in the createTheme catch block
- Add tests for unsupported CSS Color Level 4 formats (oklch, display-p3, lab)
- Add test verifying inSidebar propagates to fallback theme
- Add tests verifying safeGetLuminance returns consistent results for valid colors
@sfc-gh-nbellante sfc-gh-nbellante added the impact:users PR changes affect end users label Apr 6, 2026
@github-actions
Copy link
Copy Markdown
Contributor

This pull request has had no activity for 14 days, so it has been marked as stale. If you still want to continue this work, please leave a comment or push a commit within 7 days. A maintainer can also apply the never-stale label to opt out.

@github-actions github-actions Bot added the stale label Apr 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:frontend Related to frontend aspects change:bugfix PR contains bug fix implementation do-not-merge PR is blocked from merging feature:theming Related to theming impact:users PR changes affect end users stale

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Theme colors using CSS OKLCH syntax can crash the frontend and block app load

2 participants