[DynamicContainers] Dynamic Tabs#13910
Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. |
✅ PR preview is ready!
|
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
SummaryThis PR adds dynamic tabs functionality to Changed files:
Code QualityBackend (Python): Frontend (TypeScript):
Test CoveragePython unit tests (9 new tests): Good coverage of:
E2E tests (2 new tests): Good coverage of:
Missing frontend unit tests: No new tests were added to
Frontend unit tests should be added at minimum for:
Missing Python tests:
Backwards CompatibilityThe changes are mostly backwards compatible:
Potential issue —
This causes unnecessary reruns whenever a user switches tabs with Security & Risk
Accessibility
Recommendations
VerdictCHANGES REQUESTED: The This is an automated AI review by |
📈 Frontend coverage change detectedThe frontend unit test (vitest) coverage has increased by 0.0200%
✅ Coverage change is within normal range. |
0f4ce6f to
76a4107
Compare
76a4107 to
70f0fde
Compare
bd695b7 to
a74035b
Compare
70f0fde to
4c1d24d
Compare
a74035b to
ec3f270
Compare
e4574bd to
e6191a1
Compare
SummaryThis PR adds Key changes:
Code QualityThe code is well-structured, clean, and follows the established patterns in the codebase. Backend (
Frontend (
Minor observations:
st.write("Gamma tab content")
# Key-only (no on_change) — should NOT trigger reruns on tab switchThis is a minor formatting issue that doesn't affect functionality.
Test CoverageTest coverage is thorough and well-designed: Python unit tests (10 new tests in
Frontend unit tests (6 new tests in
E2E tests (3 new tests in
The E2E tests correctly use One note: the E2E tests don't include fragment-scoped testing ( Backwards CompatibilityThis PR is fully backwards compatible:
Security & RiskNo security concerns identified:
Risk assessment: Low risk. The new functionality is cleanly gated behind the AccessibilityNo accessibility concerns. The PR does not modify any ARIA attributes, roles, or keyboard behavior. The tabs continue to use BaseUI's Recommendations
These are all minor suggestions and none are blocking. VerdictAPPROVED: Well-designed, backwards-compatible feature addition with comprehensive test coverage, clean code paths, and proper separation between stateful and non-stateful tab behavior. This is an automated AI review by |
e6191a1 to
aebf31d
Compare
ec3f270 to
2ca96bc
Compare
aebf31d to
11e9015
Compare
11e9015 to
f78bbec
Compare
There was a problem hiding this comment.
Pull request overview
This PR implements dynamic tabs for Streamlit, adding key and on_change parameters to st.tabs to enable stateful, lazily-executed tabs. When on_change="rerun" is set, switching tabs triggers a rerun and each tab's .open property indicates whether it's active, allowing developers to conditionally execute expensive operations only for visible tabs. The implementation also supports programmatic tab switching via session state.
Changes:
- Adds
keyandon_changeparameters tost.tabs()with validation for "ignore", "rerun", or None values - Implements widget registration for dynamic tabs tracking the active tab label as string state
- Extends the
TabContainerprotobuf message with an optionalwidget_idfield for widget state tracking - Updates frontend to send widget state updates on tab changes and sync with backend programmatic control
- Includes comprehensive unit tests (Python and TypeScript) and E2E tests for lazy execution and programmatic control scenarios
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| proto/streamlit/proto/Block.proto | Adds optional widget_id field to TabContainer message for dynamic tab state tracking |
| lib/streamlit/elements/layouts.py | Implements key and on_change parameters with widget registration, serde, and .open property logic |
| lib/tests/streamlit/elements/layouts_test.py | Adds 8 unit tests covering dynamic tab behavior, session state, widget ID proto, and edge cases |
| frontend/lib/src/components/elements/Tabs/Tabs.tsx | Implements widget state updates on tab click, programmatic sync via defaultTabIndex effect, and CSS key class extraction (deferred feature) |
| frontend/lib/src/components/elements/Tabs/Tabs.test.tsx | Adds tests for dynamic tab widget state updates and programmatic control synchronization |
| frontend/lib/src/components/core/Block/Block.tsx | Passes fragmentId to Tabs component for fragment-scoped widget updates |
| e2e_playwright/st_tabs_test.py | Adds E2E tests for lazy execution, programmatic control, and key-only (non-dynamic) behavior |
| e2e_playwright/st_tabs.py | Test app demonstrating dynamic tabs with execution counters, programmatic switching, and key-only tabs |
88f803d to
ed2aa5a
Compare
Consolidated Code Review: PR #13910 — [DynamicContainers] Dynamic TabsSummaryThis PR adds Changes span the full stack:
Reviewer AgreementBoth reviewers (
Reviewer DisagreementThe key disagreement is on severity:
Resolution: Duplicate Tab LabelsAfter verifying the code, Resolution: Stale Session State FallbackAfter inspecting the code, there is a real (minor) inconsistency: when
This is a valid observation worth addressing in a follow-up, but it is not a blocking issue for initial merge. Code QualityThe implementation is clean and well-structured:
Test CoverageStrong coverage for core functionality. Both reviewers noted the following gaps:
Recommendations
VerdictAPPROVED — This is a well-implemented feature addition with clean code, solid test coverage, full backwards compatibility, and no security or accessibility concerns. The edge cases identified by reviewers (duplicate labels, stale session state fallback) are real but non-blocking: the duplicate label behavior is consistent with pre-existing semantics, and the stale state issue is a narrow transient inconsistency. The recommendations above are suggested as follow-up improvements. Consolidated review by 📋 Review by `gpt-5.3-codex-high`SummaryThis PR adds dynamic/stateful behavior to Code QualityOverall structure is clean and aligned with existing patterns, but I found two blocking logic issues:
Test CoverageCoverage is good for core flows:
Coverage gaps for the issues above:
Backwards CompatibilityDefault behavior ( Security & RiskNo direct security concerns identified. Main risk is behavioral regression: app logic that relies on AccessibilityNo clear accessibility regressions in this PR. It continues using existing BaseWeb tab semantics and does not introduce new non-semantic interactive patterns. Recommendations
VerdictCHANGES REQUESTED: Dynamic tabs are close, but duplicate-label selection and stale-session-state fallback handling can produce incorrect behavior and should be fixed before merge. This is an automated AI review by 📋 Review by `opus-4.6-thinking`SummaryThis PR adds Changes span the full stack:
Code QualityBackend ( The implementation is clean and follows existing Streamlit patterns well. The Minor observations:
Frontend ( The frontend changes are well-structured. The
Protobuf ( The Test CoveragePython Unit Tests (8 new tests): Excellent coverage of the new functionality:
All tests have docstrings as required by the test guide. Frontend Unit Tests (3 new tests): Good coverage:
E2E Tests (3 new tests): Well-designed end-to-end coverage:
The E2E tests follow best practices: using Potential gaps:
Backwards CompatibilityThe changes are fully backwards compatible:
Security & RiskNo security concerns identified:
Regression risk: Low. The changes are well-isolated behind the AccessibilityNo new accessibility concerns. The tabs component continues to use BaseWeb's Recommendations
VerdictAPPROVED: This is a well-implemented feature addition with clean code, good test coverage, full backwards compatibility, and no security concerns. The minor recommendations above are non-blocking improvements that could be addressed in follow-up PRs. This is an automated AI review by |
There was a problem hiding this comment.
Adding a case with fragment here as suggested by AI might make sense.
There was a problem hiding this comment.
Added, thanks!
ed2aa5a to
f0ce12b
Compare
|
@lukasmasuch also updated the other two widget PRs with the same changes. |
| // Update widget state for dynamic tabs | ||
| if (isDynamic && widgetId && widgetMgr) { | ||
| widgetMgr.setStringValue( | ||
| { id: widgetId, formId: "" }, |
There was a problem hiding this comment.
question: Can't we pass in the node.deltaBlock?.tabContainer here as well similar to how we do it for most widgets. But not sure if thats a lot better.
And, I guess the usage in form is not supported. Is there some check that makes sure that lazy tabs aren't used in forms?
There was a problem hiding this comment.
We could use node.deltaBlock?.tabContainer, but it is not very different from this.
Good point about using these inside of a form. Currently there is no check, but we should handle this explicitly in some way. I will make a follow up PR. Disallowing lazy tabs inside of forms seems like the right call to me.
|
|
||
| ctx = get_script_run_ctx() | ||
|
|
||
| element_id = compute_and_register_element_id( |
There was a problem hiding this comment.
Same comment as on expander, potentially also compute ID when key is set.
f0ce12b to
d23672d
Compare
Co-authored-by: lawilby <[email protected]>
d23672d to
7bbc231
Compare
## Describe your changes Test Demo App: https://dynamic-tabs.streamlit.app/ **Stacked on #13867.** Adds `key` and `on_change` parameters to `st.tabs`, enabling stateful, lazily-executed tabs. When `on_change="rerun"`, switching tabs triggers a rerun and each tab's `.open` property returns `True` for the active tab and `False` for inactive ones. This lets apps skip expensive work in hidden tabs: ```python tabs = st.tabs(["Sales", "Customers"], on_change="rerun") if tabs[0].open: with tabs[0]: expensive_sales_query() if tabs[1].open: with tabs[1]: expensive_customer_query() ``` Programmatic tab switching is supported via `st.session_state`: ```python def goto_settings(): st.session_state.my_tabs = "Settings" st.button("Go to Settings", on_click=goto_settings) tabs = st.tabs(["Home", "Settings"], key="my_tabs", on_change="rerun") ``` ### Backend - Adds `key` and `on_change` parameters to `st.tabs()`. - When `on_change="rerun"`, registers tabs as a widget via `register_widget`, tracking the active tab label as a string value. Sets `.open = True` on the active `TabContainer` and `.open = False` on the rest. Without `on_change`, `.open` remains `None`. - Falls back to the default tab when the stored label is no longer in the tab list. - **Note:** `st-key-` CSS class support for tabs is deferred to a follow-up PR to ensure the class is applied to the correct DOM element. ### Frontend - On tab switch in dynamic tabs, sends the new tab label to the backend via `widgetMgr.setStringValue`. - Syncs the selected tab when `defaultTabIndex` changes from the backend (programmatic control). - Passes `fragmentId` to the `Tabs` component for correct fragment-scoped widget updates. ## GitHub Issue Link (if applicable) #6004 ## Testing Plan - [x] Unit Tests (JS and/or Python) - [x] E2E Tests - [x] See demo app pointed to this PR for manual testing: https://dynamic-tabs.streamlit.app/ **Python unit tests** (`lib/tests/streamlit/elements/layouts_test.py`): 8 tests covering invalid `on_change` values, `.open` state with and without `default`, session state accessibility, and correct proto tab index. **Frontend unit tests** (`frontend/lib/src/components/elements/Tabs/Tabs.test.tsx`): Tests for dynamic tab behavior including widget state updates on tab switch and programmatic sync from backend. **E2E tests** (`e2e_playwright/st_tabs_test.py`): `test_dynamic_tabs_lazy_execution` verifies only the active tab's content executes on each rerun; `test_dynamic_tabs_programmatic_control` verifies switching tabs via session state buttons. --- **Contribution License Agreement** By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license. Co-authored-by: lawilby <[email protected]>
…#14143) ## Describe your changes The partial change that landed with callable `on_change` support used `callable(on_change)` inline in the `check_widget_policies` call for all three dynamic containers, but without the `is_callback` intermediate variable or `cast`. This aligns all three with the established pattern from `vega_charts.py`: - Extracts `is_callback = callable(on_change)` as a named variable for clarity - Passes `on_change=cast("WidgetCallback", on_change) if is_callback else None` for type correctness - `enable_check_callback_rules=is_callback` (functionally equivalent, but consistent with the pattern) ## Screenshot or video (only for visual changes) N/A ## GitHub Issue Link (if applicable) Surfaced in review of #13910. ## Testing Plan - [x] Python unit tests — added 6 regression tests to `lib/tests/streamlit/elements/layouts_test.py`, covering all three containers: - `test_callable_on_change_inside_form_raises` — verifies a callable `on_change` inside `st.form` raises `StreamlitInvalidFormCallbackError` (expander, tabs, popover) - `test_on_change_rerun_inside_form_does_not_raise` — verifies `on_change="rerun"` inside `st.form` is still allowed (expander, tabs, popover) --- **Contribution License Agreement** By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license. --------- Co-authored-by: lawilby <[email protected]>

Describe your changes
Test Demo App: https://dynamic-tabs.streamlit.app/
Stacked on #13867.
Adds
keyandon_changeparameters tost.tabs, enabling stateful, lazily-executed tabs. Whenon_change="rerun", switching tabs triggers a rerun and each tab's.openproperty returnsTruefor the active tab andFalsefor inactive ones. This lets apps skip expensive work in hidden tabs:Programmatic tab switching is supported via
st.session_state:Backend
keyandon_changeparameters tost.tabs().on_change="rerun", registers tabs as a widget viaregister_widget, tracking the active tab label as a string value. Sets.open = Trueon the activeTabContainerand.open = Falseon the rest. Withouton_change,.openremainsNone.st-key-CSS class support for tabs is deferred to a follow-up PR to ensure the class is applied to the correct DOM element.Frontend
widgetMgr.setStringValue.defaultTabIndexchanges from the backend (programmatic control).fragmentIdto theTabscomponent for correct fragment-scoped widget updates.GitHub Issue Link (if applicable)
#6004
Testing Plan
Python unit tests (
lib/tests/streamlit/elements/layouts_test.py): 8 tests covering invalidon_changevalues,.openstate with and withoutdefault, session state accessibility, and correct proto tab index.Frontend unit tests (
frontend/lib/src/components/elements/Tabs/Tabs.test.tsx): Tests for dynamic tab behavior including widget state updates on tab switch and programmatic sync from backend.E2E tests (
e2e_playwright/st_tabs_test.py):test_dynamic_tabs_lazy_executionverifies only the active tab's content executes on each rerun;test_dynamic_tabs_programmatic_controlverifies switching tabs via session state buttons.Contribution License Agreement
By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license.