[DynamicContainers] Dynamic Expanders#13888
Conversation
✅ 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. |
49f8b53 to
e2b4c18
Compare
6c337d2 to
85202bd
Compare
e2b4c18 to
aa1c448
Compare
85202bd to
3f42201
Compare
SummaryThis PR implements "dynamic expanders" for Streamlit, adding
Code QualityThe code is well-structured and follows existing patterns in the codebase. Backend:
Frontend:
Minor observations:
Test CoveragePython unit tests (10 new tests): Comprehensive coverage including:
Frontend unit tests (7 new tests): Good coverage of widget mode behavior:
E2E tests (5 new tests): Thorough end-to-end coverage:
The E2E tests include good negative assertions (e.g., verifying execution counts don't change when expanders are closed). The One gap noted: There is no test for using Backwards CompatibilityThis PR is fully backwards compatible:
Security & Risk
Accessibility
Recommendations
VerdictAPPROVED: Well-implemented feature with thorough test coverage, full backwards compatibility, and clean code that follows established patterns. The recommendations above are minor improvements that could be addressed in follow-up PRs. This is an automated AI review by |
aa1c448 to
2997011
Compare
3f42201 to
540f4f9
Compare
2997011 to
386c302
Compare
540f4f9 to
5266c50
Compare
386c302 to
9acc792
Compare
9acc792 to
593d630
Compare
5266c50 to
19a78c9
Compare
56859f6 to
0c911d0
Compare
b6d595f to
f3e158b
Compare
There was a problem hiding this comment.
Pull request overview
This PR adds dynamic expander functionality to Streamlit, enabling stateful expanders that can trigger reruns and support lazy content execution. The implementation introduces key and on_change parameters to st.expander, allowing developers to programmatically control expander state via session state.
Changes:
- Adds
on_change="rerun"mode for dynamic expanders that trigger app reruns on toggle, with state accessible viast.session_state[key]and the.openattribute - Reworks the frontend animation system with improved ResizeObserver-based content size tracking, reducing the debounce delay from 50ms to 10ms for more responsive animations
- Implements widget-consistent loading skeleton using
SquareSkeletonwhen opening empty dynamic expanders
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| proto/streamlit/proto/Block.proto | Adds optional widget_id field to Expandable message for dynamic expander widget identification |
| lib/streamlit/elements/layouts.py | Implements expander widget registration logic, validation for on_change parameter, and session state integration |
| lib/tests/streamlit/elements/layouts_test.py | Comprehensive unit tests covering widget registration, state management, and ignore mode behavior |
| lib/streamlit/elements/lib/mutable_expander_container.py | Not in diff but referenced - defines .open property on ExpanderContainer |
| frontend/lib/src/components/elements/Expander/useDetailsAnimation.ts | Updates RESIZE_DEBOUNCE_MS from 50ms to 10ms with detailed rationale in comments |
| frontend/lib/src/components/elements/Expander/Expander.tsx | Adds widget mode handling, loading skeleton logic, CSS key class application, and backend toggle notification |
| frontend/lib/src/components/elements/Expander/Expander.test.tsx | Comprehensive frontend tests for widget mode, loading skeleton, and CSS class behavior |
| frontend/lib/src/components/core/Block/Block.tsx | Passes widget props (widgetMgr, widgetId, blockId, empty, fragmentId) to Expander component |
| e2e_playwright/st_expander.py | Test app demonstrating lazy execution, programmatic control, nested expanders, and ignore mode |
| e2e_playwright/st_expander_test.py | E2E tests verifying dynamic expander behavior, state persistence, and non-rerun behavior in ignore mode |
| specs/2026-01-14-dynamic-tabs-expander/product-spec.md | Updates key parameter type to include int for consistency with other widgets |
6a9159e to
3a22a4a
Compare
SummaryAdds stateful Code QualityNo blocking issues found. The backend/widget registration flow in Test CoverageGood coverage: Python unit tests in Backwards CompatibilityDefault behavior remains Security & RiskNo security concerns identified. The main risk is visual/behavioral regression around expander animations and empty content loading, which is mitigated by unit and e2e coverage. AccessibilityNo new accessibility regressions noted. The component continues to use semantic Recommendations
VerdictAPPROVED: Changes look safe, backwards compatible, and well-tested. This is an automated AI review by |
|
|
||
| ctx = get_script_run_ctx() | ||
|
|
||
| element_id = compute_and_register_element_id( |
There was a problem hiding this comment.
We might also want to call element_id = compute_and_register_element_id if key is provided, so that it gets added to block_proto.id and thereby added as a CSS key. Potentially, it might make sense to always calculate the ID to help with #8239, but always calling has the downside of throwing an exception of there are two identical expanders -> only doing it if key is set might be a safer option and also provides a way to resolve: #8239
There was a problem hiding this comment.
I do want to add the CSS key, but I just plan on doing this in a separate PR because I think it warrants some attention to how well it works for styling for users. Right now it will be added to an inner div that wraps the content in the expander (due to the container implementation). Users might expect it to wrap the entire expander.
That's a good point about making it a widget if the user provides a key, since this is a net new feature. I believe I was thinking of that when I wrote the spec, so thank you for brining it up again!
sfc-gh-lmasuch
left a comment
There was a problem hiding this comment.
LGTM 👍 with one comment about ID computation. But maybe better to add as a follow up.
Yeah, maybe I can make this change along with handling the CSS class + key, but prior to release cutoff so it won't introduce a change for users. |
13a2826 to
c3f6bce
Compare
c3f6bce to
8346511
Compare
475467e to
357b896
Compare
Bumps [immer](https://github.com/immerjs/immer) from 11.1.3 to 11.1.4. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/immerjs/immer/releases">immer's releases</a>.</em></p> <blockquote> <h2>v11.1.4</h2> <h2><a href="https://github.com/immerjs/immer/compare/v11.1.3...v11.1.4">11.1.4</a> (2026-02-10)</h2> <h3>Bug Fixes</h3> <ul> <li>handle nested proxies after spreading and inserting into an array (<a href="https://github.com/immerjs/immer/commit/90a77655af5a9dd3fbf7688f95952d32511b430c">90a7765</a>)</li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/immerjs/immer/commit/cdccf1a6d0d349ad587a736f0eb8a3da43fc7747"><code>cdccf1a</code></a> Merge pull request <a href="https://redirect.github.com/immerjs/immer/issues/1210">#1210</a> from immerjs/bugfix/1209-array-plugin-nested-drafts</li> <li><a href="https://github.com/immerjs/immer/commit/90a77655af5a9dd3fbf7688f95952d32511b430c"><code>90a7765</code></a> fix: handle nested proxies after spreading and inserting into an array</li> <li>See full diff in <a href="https://github.com/immerjs/immer/compare/v11.1.3...v11.1.4">compare view</a></li> </ul> </details> <br /> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Lukas Masuch <[email protected]> Co-authored-by: lawilby <[email protected]>
357b896 to
94b8adc
Compare
SummaryThis PR adds Key changes:
Code QualityThe implementation is well-structured and follows established codebase patterns closely. Backend:
Frontend:
Minor issues:
current_expanded = expander_state.value
expandable_proto = BlockProto.Expandable()
expandable_proto.expanded = current_expanded
Test CoverageTest coverage is thorough and well-organized: Python unit tests (
Frontend unit tests:
E2E tests ( Gaps (non-blocking):
Backwards CompatibilityNo breaking changes. Backwards compatibility is well-maintained:
Security & RiskNo security concerns identified. The widget registration follows the same established patterns used by popover and tabs. The Low regression risk: the default behavior is unchanged, and the new code paths are fully gated behind AccessibilityAccessibility is properly maintained:
Recommendations
VerdictAPPROVED: Well-implemented feature following established widget patterns with comprehensive test coverage and full backwards compatibility. The minor suggestions above are non-blocking improvements. This is an automated AI review by |
Demo App: https://dynamic-expander.streamlit.app/ ## Describe your changes Adds `key` and `on_change` parameters to `st.expander`, enabling stateful dynamic expanders that persist user toggle state across reruns. Reworks the frontend animation system with a `useDetailsAnimation` hook and `animateHeight` utility. - **`on_change="rerun"`**: Registers the expander as a widget so toggling triggers a rerun; expanded state is accessible via `st.session_state[key]` and `.open` - **`on_change="ignore"` (default)**: No state tracking — preserves current non-widget behavior - **ResizeObserver-based animation**: Replaces the Safari two-step repaint workaround with immediate measurement + `ResizeObserver` fallback, smooth mid-animation interruption via `getBoundingClientRect()`, and nullable `expanded` handling for backend-synced state changes - **Widget-consistent loading skeleton**: Uses `SquareSkeleton` (same as other widgets) instead of `TextLineSkeleton` for the loading placeholder when opening an empty dynamic expander ## Testing Plan - [x] Unit Tests (JS and/or Python) - `useDetailsAnimation.test.ts` — toggle behavior, backend sync, nullable handling, ResizeObserver lifecycle, rapid double-toggle, cleanup - `animateHeight.test.ts` — animation creation, custom options, finish/cancel lifecycle - `Expander.test.tsx` — widget mode rendering, skeleton, CSS key class, `onToggle` callback wiring - `layouts_test.py` — `on_change` validation, widget registration, session state access, widget ID assignment, ignore-mode negative checks - [x] E2E Tests - `st_expander_test.py` — stateful expand/collapse reruns, nested expanders, ignore-mode no-rerun verification --- **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: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: lawilby <[email protected]>

Demo App: https://dynamic-expander.streamlit.app/
Describe your changes
Adds
keyandon_changeparameters tost.expander, enabling stateful dynamic expanders that persist user toggle state across reruns. Reworks the frontend animation system with auseDetailsAnimationhook andanimateHeightutility.on_change="rerun": Registers the expander as a widget so toggling triggers a rerun; expanded state is accessible viast.session_state[key]and.openon_change="ignore"(default): No state tracking — preserves current non-widget behaviorResizeObserverfallback, smooth mid-animation interruption viagetBoundingClientRect(), and nullableexpandedhandling for backend-synced state changesSquareSkeleton(same as other widgets) instead ofTextLineSkeletonfor the loading placeholder when opening an empty dynamic expanderTesting Plan
useDetailsAnimation.test.ts— toggle behavior, backend sync, nullable handling, ResizeObserver lifecycle, rapid double-toggle, cleanupanimateHeight.test.ts— animation creation, custom options, finish/cancel lifecycleExpander.test.tsx— widget mode rendering, skeleton, CSS key class,onTogglecallback wiringlayouts_test.py—on_changevalidation, widget registration, session state access, widget ID assignment, ignore-mode negative checksst_expander_test.py— stateful expand/collapse reruns, nested expanders, ignore-mode no-rerun verificationContribution License Agreement
By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license.