Repository: streamlit/streamlit
Pull request #10848: Fix some sidebar theming aspects
Author: login: lukasmasuch
Labels: name: impact:internal
description: PR changes only affect internal code
name: change:bugfix
description: PR contains bug fix implementation
Created: 2025-03-19T15:32:30Z | Merged: 2025-03-19T17:54:36Z

## Describe your changes

Apply some sidebar theming tweaks and fixes from this deprecated PR: https://github.com/streamlit/streamlit/pull/10368

## Testing Plan

- Updated e2e test to have a bigger contrast between sidebar (dark mode) & main theme (light mode).

---

**Contribution License Agreement**

By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license.



Diff:
diff --git a/e2e_playwright/__snapshots__/linux/sidebar_custom_theme_test/sidebar_custom_theme[chromium].png b/e2e_playwright/__snapshots__/linux/sidebar_custom_theme_test/sidebar_custom_theme[chromium].png
index a06c31775e0..f5bc4dcfb3e 100644
Binary files a/e2e_playwright/__snapshots__/linux/sidebar_custom_theme_test/sidebar_custom_theme[chromium].png and b/e2e_playwright/__snapshots__/linux/sidebar_custom_theme_test/sidebar_custom_theme[chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/sidebar_custom_theme_test/sidebar_custom_theme[firefox].png b/e2e_playwright/__snapshots__/linux/sidebar_custom_theme_test/sidebar_custom_theme[firefox].png
index 126b0930758..8c8352cd4b4 100644
Binary files a/e2e_playwright/__snapshots__/linux/sidebar_custom_theme_test/sidebar_custom_theme[firefox].png and b/e2e_playwright/__snapshots__/linux/sidebar_custom_theme_test/sidebar_custom_theme[firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/sidebar_custom_theme_test/sidebar_custom_theme[webkit].png b/e2e_playwright/__snapshots__/linux/sidebar_custom_theme_test/sidebar_custom_theme[webkit].png
index 3e5f9b036bd..6782b098ee8 100644
Binary files a/e2e_playwright/__snapshots__/linux/sidebar_custom_theme_test/sidebar_custom_theme[webkit].png and b/e2e_playwright/__snapshots__/linux/sidebar_custom_theme_test/sidebar_custom_theme[webkit].png differ
diff --git a/e2e_playwright/theming/sidebar_custom_theme_test.py b/e2e_playwright/theming/sidebar_custom_theme_test.py
index a76b2ac3626..335a5982bc9 100644
--- a/e2e_playwright/theming/sidebar_custom_theme_test.py
+++ b/e2e_playwright/theming/sidebar_custom_theme_test.py
@@ -26,18 +26,22 @@
 def configure_sidebar_custom_theme():
     """Configure sidebar custom theme."""
     os.environ["STREAMLIT_THEME_BASE"] = "light"
-    os.environ["STREAMLIT_THEME_SIDEBAR_BACKGROUND_COLOR"] = "#e8e7dd"
+    os.environ["STREAMLIT_THEME_BASE_FONT_SIZE"] = "14"
+    os.environ["STREAMLIT_THEME_BASE_RADIUS"] = "full"
+    os.environ["STREAMLIT_THEME_SHOW_BORDER_AROUND_INPUTS"] = "True"
+    os.environ["STREAMLIT_THEME_SIDEBAR_BACKGROUND_COLOR"] = "black"
     os.environ["STREAMLIT_THEME_SIDEBAR_BASE_RADIUS"] = "none"
-    os.environ["STREAMLIT_THEME_SIDEBAR_BORDER_COLOR"] = "#d3d2ca"
+    os.environ["STREAMLIT_THEME_SIDEBAR_BORDER_COLOR"] = "white"
     os.environ["STREAMLIT_THEME_SIDEBAR_HEADING_FONT"] = "bold, serif"
-    os.environ["STREAMLIT_THEME_SIDEBAR_LINK_COLOR"] = "#3d3a2a"
-    os.environ["STREAMLIT_THEME_SIDEBAR_PRIMARY_COLOR"] = "#bb5a38"
-    os.environ["STREAMLIT_THEME_SIDEBAR_SECONDARY_BACKGROUND_COLOR"] = "#ecebe3"
-    os.environ["STREAMLIT_THEME_SIDEBAR_SHOW_BORDER_AROUND_INPUTS"] = "True"
-    os.environ["STREAMLIT_THEME_SIDEBAR_TEXT_COLOR"] = "#4747d4"
+    os.environ["STREAMLIT_THEME_SIDEBAR_LINK_COLOR"] = "#90EE90"
+    os.environ["STREAMLIT_THEME_SIDEBAR_PRIMARY_COLOR"] = "blue"
+    os.environ["STREAMLIT_THEME_SIDEBAR_SECONDARY_BACKGROUND_COLOR"] = "#222222"
+    os.environ["STREAMLIT_THEME_SIDEBAR_TEXT_COLOR"] = "white"
     os.environ["STREAMLIT_CLIENT_TOOLBAR_MODE"] = "minimal"
     yield
     del os.environ["STREAMLIT_THEME_BASE"]
+    del os.environ["STREAMLIT_THEME_BASE_RADIUS"]
+    del os.environ["STREAMLIT_THEME_SHOW_BORDER_AROUND_INPUTS"]
     del os.environ["STREAMLIT_THEME_SIDEBAR_BACKGROUND_COLOR"]
     del os.environ["STREAMLIT_THEME_SIDEBAR_BASE_RADIUS"]
     del os.environ["STREAMLIT_THEME_SIDEBAR_BORDER_COLOR"]
@@ -45,7 +49,6 @@ def configure_sidebar_custom_theme():
     del os.environ["STREAMLIT_THEME_SIDEBAR_LINK_COLOR"]
     del os.environ["STREAMLIT_THEME_SIDEBAR_PRIMARY_COLOR"]
     del os.environ["STREAMLIT_THEME_SIDEBAR_SECONDARY_BACKGROUND_COLOR"]
-    del os.environ["STREAMLIT_THEME_SIDEBAR_SHOW_BORDER_AROUND_INPUTS"]
     del os.environ["STREAMLIT_THEME_SIDEBAR_TEXT_COLOR"]
     del os.environ["STREAMLIT_CLIENT_TOOLBAR_MODE"]
 
diff --git a/frontend/app/src/App.tsx b/frontend/app/src/App.tsx
index 60d5aa7f41b..cc320328325 100644
--- a/frontend/app/src/App.tsx
+++ b/frontend/app/src/App.tsx
@@ -1153,13 +1153,10 @@ export class App extends PureComponent<Props, State> {
       return "hash_for_undefined_custom_theme"
     }
 
-    const themeInputEntries = Object.entries(themeInput)
-    // Ensure that our themeInput fields are in a consistent order when
-    // stringified below. Sorting an array of arrays in javascript sorts by the
-    // 0th element of the inner arrays, uses the 1st element to tiebreak, and
-    // so on.
-    themeInputEntries.sort()
-    return hashString(themeInputEntries.join(":"))
+    // Hash the sorted representation of the theme input:
+    return hashString(
+      JSON.stringify(themeInput, Object.keys(themeInput).sort())
+    )
   }
 
   processThemeInput(themeInput: CustomThemeConfig): void {
diff --git a/frontend/app/src/components/Sidebar/Sidebar.tsx b/frontend/app/src/components/Sidebar/Sidebar.tsx
index d1423aec404..23a9cc315b1 100644
--- a/frontend/app/src/components/Sidebar/Sidebar.tsx
+++ b/frontend/app/src/components/Sidebar/Sidebar.tsx
@@ -35,6 +35,7 @@ import {
   isColoredLineDisplayed,
   isEmbed,
   IsSidebarContext,
+  LibContext,
 } from "@streamlit/lib"
 import { IAppPage, Logo, PageConfig } from "@streamlit/protobuf"
 import { localStorageAvailable } from "@streamlit/utils"
@@ -129,6 +130,8 @@ const Sidebar: React.FC<SidebarProps> = ({
     window ? window.innerWidth : Infinity
   )
 
+  const { activeTheme } = React.useContext(LibContext)
+
   useEffect(() => {
     setCollapsedSidebar(
       shouldCollapse(initialSidebarState, mediumBreakpointPx)
@@ -270,7 +273,14 @@ const Sidebar: React.FC<SidebarProps> = ({
         data-testid="stSidebarCollapsedControl"
       >
         {renderLogo(true)}
-        <StyledOpenSidebarButton>
+        <StyledOpenSidebarButton
+          theme={
+            // Use the active theme from the LibContext to use the main theme
+            // for styling the open button since otherwise it would use the colors
+            // of the sidebar theme (which is not what we want here).
+            activeTheme.emotion
+          }
+        >
           <BaseButton
             kind={BaseButtonKind.HEADER_NO_PADDING}
             onClick={toggleCollapse}
diff --git a/frontend/app/src/components/Sidebar/ThemedSidebar.tsx b/frontend/app/src/components/Sidebar/ThemedSidebar.tsx
index 5cf58c5dae4..1764976a4eb 100644
--- a/frontend/app/src/components/Sidebar/ThemedSidebar.tsx
+++ b/frontend/app/src/components/Sidebar/ThemedSidebar.tsx
@@ -16,6 +16,8 @@
 
 import React, { ReactElement } from "react"
 
+import { getLuminance } from "color2k"
+
 import {
   createTheme,
   LibContext,
@@ -24,6 +26,7 @@ import {
 } from "@streamlit/lib"
 import { AppContext } from "@streamlit/app/src/components/AppContext"
 import { notNullOrUndefined } from "@streamlit/utils"
+import { CustomThemeConfig } from "@streamlit/protobuf"
 
 import Sidebar, { SidebarProps } from "./Sidebar"
 
@@ -33,17 +36,30 @@ const createSidebarTheme = (theme: ThemeConfig): ThemeConfig => {
     sidebarOverride = theme.themeInput.sidebar
   }
 
+  // Either use the configured background color or secondary background from main theme:
+  const sidebarBackground =
+    theme.themeInput?.sidebar?.backgroundColor ??
+    theme.emotion.colors.secondaryBg
+
+  // Eeither use the configured secondary background color or background from main theme:
+  const secondaryBackgroundColor =
+    theme.themeInput?.sidebar?.secondaryBackgroundColor ??
+    theme.emotion.colors.bgColor
+
   return createTheme(
     "Sidebar",
     {
-      ...theme.themeInput,
-      secondaryBackgroundColor: theme.emotion.colors.bgColor,
-      backgroundColor: theme.emotion.colors.secondaryBg,
+      ...theme.themeInput, // Use the theme props from the main theme as basis
+      base:
+        getLuminance(sidebarBackground) > 0.5
+          ? CustomThemeConfig.BaseTheme.LIGHT
+          : CustomThemeConfig.BaseTheme.DARK,
+      backgroundColor: sidebarBackground,
+      secondaryBackgroundColor: secondaryBackgroundColor,
       ...sidebarOverride,
     },
-    theme,
-    // inSidebar
-    true
+    undefined, // Creating a new theme from scratch
+    true // inSidebar
   )
 }
 
diff --git a/frontend/app/src/components/Sidebar/styled-components.ts b/frontend/app/src/components/Sidebar/styled-components.ts
index ea49cbe4cba..841aa2dcb4f 100644
--- a/frontend/app/src/components/Sidebar/styled-components.ts
+++ b/frontend/app/src/components/Sidebar/styled-components.ts
@@ -52,7 +52,11 @@ export const StyledSidebar = styled.section<StyledSidebarProps>(
       // Nudge the sidebar by 2px so the header decoration doesn't go below it
       top: adjustTop ? theme.sizes.headerDecorationHeight : theme.spacing.none,
       backgroundColor: theme.colors.bgColor,
+      // Since the sidebar can have a different theme (+ background)
+      // we need to explicitly set the font color and color scheme
+      // here again so that it is inherited correctly by all sidebar elements:
       color: theme.colors.bodyText,
+      colorScheme: hasLightBackgroundColor(theme) ? "light" : "dark",
       zIndex: theme.zIndices.header + 1,
 
       minWidth,


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR fixes sidebar theming aspects by updating background color logic, theme derivation, and component styling for the sidebar.  
- Adjusts theming logic in ThemedSidebar.tsx using luminance to select between light and dark modes  
- Adds explicit color and colorScheme settings in styled-components for consistent sidebar styling  
- Updates the theme hashing function in App.tsx to use JSON.stringify for hash generation

### Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

| File                                                   | Description                                                                               |
| ------------------------------------------------------ | ----------------------------------------------------------------------------------------- |
| frontend/app/src/components/Sidebar/ThemedSidebar.tsx  | Implements dynamic theming based on luminance and updates background color settings         |
| frontend/app/src/components/Sidebar/styled-components.ts | Sets font color and color scheme explicitly to support the new sidebar theming changes         |
| frontend/app/src/App.tsx                               | Modifies the theme hash calculation method                                                  |
| frontend/app/src/components/Sidebar/Sidebar.tsx         | Integrates activeTheme from LibContext for improved sidebar button styling (context excerpt) |


<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**frontend/app/src/App.tsx:1156**
* Using JSON.stringify to generate a hash may lead to non-deterministic ordering of keys. Consider ensuring that object keys are sorted to maintain consistency in the hash output.
```
return hashString(JSON.stringify(themeInput))
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T15:34:21Z
user: login: lukasmasuch
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T16:06:40Z
user: login: lukasmasuch
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T16:43:55Z
user: login: sfc-gh-pchiu
body: LG
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T17:13:44Z

Review comments:
diff_hunk: @@ -33,17 +36,31 @@ const createSidebarTheme = (theme: ThemeConfig): ThemeConfig => {
     sidebarOverride = theme.themeInput.sidebar
   }
 
+  // Eeither use the configured background color or secondary background from main theme:
path: frontend/app/src/components/Sidebar/ThemedSidebar.tsx
user: login: Copilot
body: Typo in comment: 'Eeither' should be 'Either'.
```suggestion
  // Either use the configured background color or secondary background from main theme:
```
created_at: 2025-03-19T15:34:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,17 +36,31 @@ const createSidebarTheme = (theme: ThemeConfig): ThemeConfig => {
     sidebarOverride = theme.themeInput.sidebar
   }
 
+  // Eeither use the configured background color or secondary background from main theme:
+  const sidebarBackground =
+    theme.themeInput?.sidebar?.backgroundColor ??
+    theme.emotion.colors.secondaryBg
+
+  // Eeither use the configured secondary background color or background from main theme:
+  const secondaryBackgroundColor =
+    theme.themeInput?.sidebar?.secondaryBackgroundColor ??
+    theme.emotion.colors.bgColor
+
+  console.log(theme.themeInput?.sidebar?.primaryColor)
path: frontend/app/src/components/Sidebar/ThemedSidebar.tsx
user: login: Copilot
body: The console.log statement appears to be leftover debug output; consider removing it or using a proper logging mechanism before deploying to production.
created_at: 2025-03-19T15:34:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1153,13 +1153,7 @@ export class App extends PureComponent<Props, State> {
       return "hash_for_undefined_custom_theme"
     }
 
-    const themeInputEntries = Object.entries(themeInput)
-    // Ensure that our themeInput fields are in a consistent order when
-    // stringified below. Sorting an array of arrays in javascript sorts by the
-    // 0th element of the inner arrays, uses the 1st element to tiebreak, and
-    // so on.
-    themeInputEntries.sort()
-    return hashString(themeInputEntries.join(":"))
+    return hashString(JSON.stringify(themeInput))
path: frontend/app/src/App.tsx
user: login: lukasmasuch
body: The method before only worked on a flat structure. This should also work on a nested structure to detect changes in the theme.
created_at: 2025-03-19T16:06:40Z
author_association: COLLABORATOR
path: e2e_playwright/__snapshots__/linux/st_sidebar_test/st_sidebar-display[dark_theme-webkit].png
user: login: lukasmasuch
body: Note sure why this change 
created_at: 2025-03-19T16:43:55Z
author_association: COLLABORATOR

PR comments:
user: login: snyk-io[bot]
created_at: 2025-03-19T15:32:39Z
body: ![](https://res.cloudinary.com/snyk/image/upload/r-d/scm-platform/snyk-pull-requests/pr-banner-default.svg)

### :tada: **Snyk checks have passed. No issues have been found so far.**

:white_check_mark: **security/snyk** check is complete. No issues have been found. [(View Details)](https://app.snyk.io/org/streamlit-open-source/pr-checks/67ae8ed9-7586-476b-9396-98bf1bb3d29a?refs=48c3d8fa50579a34c2577817c33c520310a4f23c&source=prChecksComment)

:white_check_mark: **license/snyk** check is complete. No issues have been found. [(View Details)](https://app.snyk.io/org/streamlit-open-source/pr-checks/67ae8ed9-7586-476b-9396-98bf1bb3d29a/license?refs=48c3d8fa50579a34c2577817c33c520310a4f23c&source=prChecksComment)



author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-19T15:32:51Z
body: ### ✅ PR preview is ready!

| Name | Link |
| :----: | ---- |
| 📦 Wheel file | https://core-previews.s3-us-west-2.amazonaws.com/pr-10848/streamlit-1.43.2-py3-none-any.whl |
| 🕹️ Preview app | [pr-10848.streamlit.app](https://pr-10848.streamlit.app) (☁️ Deploy [here](https://share.streamlit.io/deploy?repository=streamlit/core-previews&branch=pr-10848&mainModule=E2E_Tester_🧪.py&subdomain=pr-10848) if not accessible)| 
author_association: CONTRIBUTOR