#15449·streamlit

Support `bind="query-params"` in `st.expander`, `st.tabs`, and `st.popover`

Author: JosephMarinierCreated Jun 5, 2026Updated Sep 16, 2026
Labelstype:enhancementpapercutfeature:query-paramsfeature:st.expanderfeature:st.popover

Checklist

  • I have searched the existing issues for similar feature requests.
  • I added a descriptive title and summary to this issue.

Summary

Add support for bind="query-params" in st.expander and st.popover.

Why?

I love both the new bind="query-params" feature, as well as the new "dynamic" st.expander (#13888), st.tabs (#13910), and st.popover (#13914) feature, and I'd like to use them together.

How?

Support bind="query-params" in st.expander(), st.tabs(), and st.popover().

Additional Context

As a workaround for st.tabs(bind="query-params"), I have been using st.segmented_control(bind="query-params"), but it's not as nice visually.

For a while now, in my app, I hacked together a dynamic expander + query param, using a st.button. It works, but it's messy.

from typing import Callable

import streamlit as st


def set_bool_query_param(key: str, value: bool):
    """Set a boolean query parameter in the URL."""
    if value:
        st.query_params[key] = value
    else:
        st.query_params.pop(key, None)


@st.fragment
def dynamic_expander(
    label: str,
    content: Callable,
    *,
    key: str,
    help: str | None = None,  # noqa: A002 Shadowing a Python builtin
):
    """Create an expander that only generates the content if it is expanded.

    Unlike `st.expander()`, which is a frontend component that always generates the content,
    this `dynamic_expander()` only generates the content if it is expanded.
    """
    expanded = key in st.query_params

    button = {
        "label": label,
        "help": help,
        "type": "tertiary",
        "icon": f":material/expand_{'less' if expanded else 'more'}:",
    }

    with st.container(border=True, key=key):
        placeholder = st.empty()
        if expanded:
            placeholder.button(key=f"{key}_button_disabled", disabled=True, **button)

            try:
                with st.spinner("Loading..."):
                    content()
            except Exception as e:
                st.exception(e)

        placeholder.button(
            key=f"{key}_button",
            on_click=set_bool_query_param,
            args=(key, not expanded),
            **button,
        )


def example():
    st.write("Content")


dynamic_expander("Example", example, key="example")