Welcome to the Streamlit community and thanks for sharing your detailed code and description!
This “ghosting” or “faded previous tab” issue is a known quirk in Streamlit when switching between tabs, especially after a rerun triggered by widgets like spinners or data loads. The problem is that Streamlit sometimes doesn’t fully clear the previous tab’s content, causing remnants to appear below the new content—even when using containers and .empty().
Root Cause:
This happens because Streamlit’s tab rendering is purely visual and not programmatic; all tab contents are always rendered and sent to the frontend, and conditional rendering or lazy loading isn’t natively supported. When you trigger a rerun (e.g., after a spinner or data fetch), Streamlit can get confused about which tab’s content should be visible, especially if widgets or containers are created conditionally or after the tab definition. This can result in the previous tab’s UI lingering in a faded state below the current tab’s content. Using .empty() on containers doesn’t always resolve this, as the tab’s internal state isn’t reset on rerun. This is a well-documented issue in the community and is not fully addressed by the current Streamlit API (see here, see here).
Workarounds:
- Consistent Tab Definition: Always define your tabs at the very top of your page, before any conditional logic or widget creation. This helps Streamlit keep track of tab state more reliably.
- Use st.container(height=…): If your content heights vary, explicitly set the height of containers to prevent layout jumps and ghosting (see solution).
- Alternative Navigation: For more robust navigation, consider using
st.navigationandst.Pagefor multipage apps, or use ast.radioorst.selectboxas a tab replacement, which allows for programmatic control and avoids this issue (see here). - Fragment Decorator: Wrapping tab content in an
@st.fragment-decorated function can sometimes help Streamlit manage reruns and tab state more cleanly (see here).
If you’d like a step-by-step breakdown or code refactor to implement these workarounds, let me know! And if anyone in the community has found a bulletproof fix, please jump in and share your insights. ![]()
![]()
Sources: