from typing import Literal import streamlit as st from streamlit.components.v1 import html """ st_fixed_container consist of two parts - fixed container and opaque container. Fixed container is a container that is fixed to the top or bottom of the screen. When transparent is set to True, the container is typical `st.container`, which is transparent by default. When transparent is set to False, the container is custom opaque_container, that updates its background color to match the background color of the app. Opaque container is a helper class, but can be used to create more custom views. See main for examples. """ OPAQUE_CONTAINER_CSS = """ :root {{ --background-color: #ffffff; /* Default background color */ }} div[data-testid="stVerticalBlockBorderWrapper"]:has(div.opaque-container-{id}):not(:has(div.not-opaque-container)) div[data-testid="stVerticalBlock"]:has(div.opaque-container-{id}):not(:has(div.not-opaque-container)) > div[data-testid="stVerticalBlockBorderWrapper"] {{ background-color: var(--background-color); width: 100%; }} div[data-testid="stVerticalBlockBorderWrapper"]:has(div.opaque-container-{id}):not(:has(div.not-opaque-container)) div[data-testid="stVerticalBlock"]:has(div.opaque-container-{id}):not(:has(div.not-opaque-container)) > div[data-testid="element-container"] {{ display: none; }} div[data-testid="stVerticalBlockBorderWrapper"]:has(div.not-opaque-container):not(:has(div[class^='opaque-container-'])) {{ display: none; }} """.strip() OPAQUE_CONTAINER_JS = """ const root = parent.document.querySelector('.stApp'); let lastBackgroundColor = null; function updateContainerBackground(currentBackground) { parent.document.documentElement.style.setProperty('--background-color', currentBackground); ; } function checkForBackgroundColorChange() { const style = window.getComputedStyle(root); const currentBackgroundColor = style.backgroundColor; if (currentBackgroundColor !== lastBackgroundColor) { lastBackgroundColor = currentBackgroundColor; // Update the last known value updateContainerBackground(lastBackgroundColor); } } const observerCallback = (mutationsList, observer) => { for(let mutation of mutationsList) { if (mutation.type === 'attributes' && (mutation.attributeName === 'class' || mutation.attributeName === 'style')) { checkForBackgroundColorChange(); } } }; const main = () => { checkForBackgroundColorChange(); const observer = new MutationObserver(observerCallback); observer.observe(root, { attributes: true, childList: false, subtree: false }); } // main(); document.addEventListener("DOMContentLoaded", main); """.strip() def st_opaque_container( *, height: int | None = None, border: bool | None = None, key: str | None = None, ): global opaque_counter opaque_container = st.container() non_opaque_container = st.container() css = OPAQUE_CONTAINER_CSS.format(id=key) with opaque_container: html(f"", scrolling=False, height=0) st.markdown(f"", unsafe_allow_html=True) st.markdown( f"
", unsafe_allow_html=True, ) with non_opaque_container: st.markdown( f"", unsafe_allow_html=True, ) return opaque_container.container(height=height, border=border) FIXED_CONTAINER_CSS = """ div[data-testid="stVerticalBlockBorderWrapper"]:has(div.fixed-container-{id}):not(:has(div.not-fixed-container)){{ background-color: transparent; position: {mode}; width: inherit; background-color: inherit; {position}: {margin}; z-index: 999; }} div[data-testid="stVerticalBlockBorderWrapper"]:has(div.fixed-container-{id}):not(:has(div.not-fixed-container)) div[data-testid="stVerticalBlock"]:has(div.fixed-container-{id}):not(:has(div.not-fixed-container)) > div[data-testid="element-container"] {{ display: none; }} div[data-testid="stVerticalBlockBorderWrapper"]:has(div.not-fixed-container):not(:has(div[class^='fixed-container-'])) {{ display: none; }} """.strip() MARGINS = { "top": "2.875rem", "bottom": "0", } def st_fixed_container( *, height: int | None = None, border: bool | None = None, mode: Literal["fixed", "sticky"] = "fixed", position: Literal["top", "bottom"] = "top", margin: str | None = None, transparent: bool = False, key: str | None = None, ): if margin is None: margin = MARGINS[position] global fixed_counter fixed_container = st.container() non_fixed_container = st.container() css = FIXED_CONTAINER_CSS.format( mode=mode, position=position, margin=margin, id=key, ) def render_content(): with fixed_container: if transparent: return st.container(height=height, border=border) return st_opaque_container( height=height, border=border, key=f"opaque_{key}" ) def render_non_content(): with fixed_container: st.markdown(f"", unsafe_allow_html=True) st.markdown( f"", unsafe_allow_html=True, ) with non_fixed_container: st.markdown( f"", unsafe_allow_html=True, ) result = None if position == "top": result = render_content() render_non_content() else: render_non_content() result = render_content() return result if __name__ == "__main__": for i in range(30): st.write(f"Line {i}") # with st_fixed_container(mode="sticky", position="bottom", border=True): # with st_fixed_container(mode="sticky", position="top", border=True): # with st_fixed_container(mode="fixed", position="bottom", border=True): with st_fixed_container(mode="fixed", position="top", border=True): st.write("This is a fixed container.") st.write("This is a fixed container.") st.write("This is a fixed container.") # The following code creates a small control panel on the right side of the screen with two buttons inside it: with st_fixed_container(mode="fixed", position="bottom", transparent=True): _, right = st.columns([0.7, 0.3]) with right: with st_opaque_container(border=True): st.button("Feedback", use_container_width=True) st.button("Clean up", use_container_width=True) st.container(border=True).write("This is a regular container.") for i in range(30): st.write(f"Line {i}")