Skip to content

Bind widgets to query params - st.pills & st.segmented_control#14006

Merged
mayagbarnes merged 3 commits intodevelopfrom
query-pills
Feb 20, 2026
Merged

Bind widgets to query params - st.pills & st.segmented_control#14006
mayagbarnes merged 3 commits intodevelopfrom
query-pills

Conversation

@mayagbarnes
Copy link
Copy Markdown
Collaborator

@mayagbarnes mayagbarnes commented Feb 18, 2026

Describe your changes

Adds bind="query-params" to st.pills and st.segmented_control for two-way sync between widget values and URL query parameters.

Key changes:

  • Uses string_array_value with repeated URL params, consistent with st.multiselect. Single-select: ?color=Red. Multi-select: ?tags=Red&tags=Blue.
  • Clearability is unconditionally True: Both widgets allow deselecting all options via the UI, so empty URL params (?key=) are accepted as an intentional clear to None/[].
  • Invalid URL values revert to default, not to cleared state: If no valid values remain after filtering, the widget falls back to its default — matching st.radio and st.selectbox. This distinction (invalid ≠ cleared) is verified by dedicated tests.
  • Single-select truncation: Multiple URL values on a single-select widget (e.g., ?color=Red&color=Blue) are truncated to the first via max_array_length=1.
  • Duplicate URL values are deduplicated for multi-select mode, consistent with st.multiselect.
  • format_func handled naturally: URL contains formatted option strings; validation checks the same list.

Testing Plan

  • JS & Python Unit Tests: ✅ Added
  • E2E Tests: ✅ Added
  • Manual Testing: ✅

@mayagbarnes mayagbarnes added security-assessment-completed change:feature PR contains new feature or enhancement implementation impact:users PR changes affect end users labels Feb 18, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 18, 2026

✅ PR preview is ready!

Name Link
📦 Wheel file https://core-previews.s3-us-west-2.amazonaws.com/pr-14006/streamlit-1.54.0-py3-none-any.whl
📦 @streamlit/component-v2-lib Download from artifacts
🕹️ Preview app pr-14006.streamlit.app (☁️ Deploy here if not accessible)

@snyk-io
Copy link
Copy Markdown
Contributor

snyk-io bot commented Feb 18, 2026

Snyk checks have passed. No issues have been found so far.

Status Scanner Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds query parameter binding support (bind="query-params") to st.pills and st.segmented_control, enabling two-way synchronization between widget values and URL query strings. This completes the query param binding feature for selection widgets in the Streamlit library.

Changes:

  • Extended protobuf schema with query_param_key field for ButtonGroup widgets
  • Added bind parameter to both st.pills and st.segmented_control Python APIs with comprehensive docstrings
  • Implemented frontend query param binding configuration using string_array_value with repeated URL format
  • Added comprehensive test coverage across Python unit tests, frontend unit tests, and E2E tests

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.

Show a summary per file
File Description
proto/streamlit/proto/ButtonGroup.proto Added optional query_param_key field (field 15) to enable query parameter binding
lib/streamlit/elements/widgets/button_group.py Added bind parameter to pills, segmented_control, and internal functions; configured register_widget with clearable=True, formatted_options, and max_array_length for proper query param handling
lib/tests/streamlit/elements/button_group_test.py Added Python unit tests for bind functionality covering key requirement, invalid values, format_func, and multi-mode
frontend/lib/src/components/widgets/ButtonGroup/ButtonGroup.tsx Configured queryParamBinding object with string_array_value, clearable=true, and repeated URL format
frontend/lib/src/components/widgets/ButtonGroup/ButtonGroup.test.tsx Added frontend unit tests verifying registration, unregistration, and config for both single and multi-select modes
e2e_playwright/st_pills_test.py Added 14 E2E tests covering seeding, URL updates, invalid values, format_func, deduplication, truncation, and edge cases
e2e_playwright/st_pills.py Added test widgets with various configurations (single, multi, defaults, format_func) for E2E testing
e2e_playwright/st_segmented_control_test.py Added 7 E2E tests covering seeding, URL updates, invalid values, and multi-select behavior
e2e_playwright/st_segmented_control.py Added test widgets for E2E testing of query param binding

@mayagbarnes mayagbarnes added the ai-review If applied to PR or issue will run AI review workflow label Feb 18, 2026
@github-actions github-actions bot removed the ai-review If applied to PR or issue will run AI review workflow label Feb 18, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Summary

This PR adds bind="query-params" support to st.pills and st.segmented_control, enabling two-way synchronization between widget values and URL query parameters. The changes span the full stack:

  • Protobuf: New optional string query_param_key field (field 15) added to ButtonGroup message.
  • Python backend: bind parameter plumbed through pills(), segmented_control(), _internal_button_group(), and _button_group(). The register_widget call includes bind, clearable=True, formatted_options, and max_array_length for single-select truncation.
  • TypeScript frontend: ButtonGroup.tsx constructs a queryParamBinding config when element.queryParamKey is set, passing it to useBasicWidgetState.
  • Tests: Python unit tests, frontend unit tests, and comprehensive E2E tests for both st.pills and st.segmented_control.

Code Quality

The implementation is clean and follows established patterns well:

  1. Consistency with existing widgets: The approach mirrors st.multiselect's query param binding almost exactly — same string_array_value type, "repeated" URL format, clearable: true, and queryParamKey on the proto. This is excellent for maintainability.

  2. Well-structured backend changes: The bind parameter is correctly threaded through all method signatures (both overloads and the implementation). The docstrings are thorough and well-formatted.

  3. Frontend changes are minimal and correct: The queryParamBinding construction in ButtonGroup.tsx (lines 315-322) is cleanly conditional on element.queryParamKey being set, following the exact same pattern as Multiselect.tsx.

  4. Proto change is clean: Adding optional string query_param_key = 15 with the reserved field numbers and next id: 16 comment properly updated.

  5. Minor pre-existing observation (not introduced by this PR): The segmented_control overloads use key: str | int | None while pills uses key: Key | None — this is a pre-existing inconsistency unrelated to this change.

Test Coverage

Test coverage is excellent and comprehensive across all three test levels:

Python unit tests (button_group_test.py):

  • Tests bind sets query_param_key in proto for both st.pills and st.segmented_control.
  • Tests bind without key raises StreamlitAPIException.
  • Tests no bind does not set query_param_key.
  • Tests invalid bind value raises StreamlitInvalidBindValueError.
  • Tests bind with format_func and multi-select mode.

Frontend unit tests (ButtonGroup.test.tsx):

  • Tests registration/unregistration of query param binding.
  • Tests multi-select binding config.
  • Negative test: no registration when queryParamKey is not set.

E2E tests (st_pills_test.py, st_segmented_control_test.py):

  • URL seeding (single, multi).
  • URL updates on selection/deselection.
  • Default override and revert-to-default clears URL.
  • Invalid value handling (revert to default, not clear).
  • Multi-select with partial/all invalid values.
  • format_func in URLs.
  • Empty URL param clearing.
  • Single-select truncation of multiple URL params.
  • Duplicate deduplication.
  • Deselect-all clears URL.
  • Switching selection updates URL correctly.

The E2E tests are thorough but numerous (17 for pills, 7 for segmented_control). Per the E2E AGENTS.md guidance to "prefer aggregated scenario tests over many micro-tests," some of these could potentially be consolidated — however, each test covers a distinct behavioral aspect with different URL seeding, so the granularity is reasonable for a feature of this complexity.

One gap: The typing tests in lib/tests/streamlit/typing/pills_types.py and segmented_control_types.py are not updated to include bind parameter assertions. While not strictly required (the bind parameter has a default value so existing type checks still pass), adding a type test like pills("foo", options, key="k", bind="query-params") would verify the new parameter is correctly typed.

Backwards Compatibility

Fully backwards compatible:

  • The bind parameter defaults to None across all method signatures.
  • The protobuf field (query_param_key = 15) is optional, so older frontends simply ignore it.
  • No existing behavior is changed when bind is not set — clearable=True and formatted_options are only meaningful when query param binding is active.
  • No changes to existing widget IDs or serialization format.

Security & Risk

  • Low risk: The bind parameter is validated through the existing register_widget infrastructure that checks for valid bind values and requires a key. Invalid bind values raise StreamlitInvalidBindValueError.
  • URL injection: Values written to the URL are the formatted option strings, which are controlled by the app developer's options list and format_func. Invalid URL values are filtered out and reverted to defaults — no arbitrary injection path.
  • No new dependencies or security-sensitive operations introduced.

Accessibility

No accessibility concerns. The changes are purely data-flow related (query parameter binding). No new UI elements, ARIA attributes, or interaction patterns are introduced. The existing ButtonGroup accessibility (button roles, disabled states, keyboard interaction) is unaffected.

Recommendations

  1. Consider adding typing tests: Add bind="query-params" assertions to lib/tests/streamlit/typing/pills_types.py and segmented_control_types.py to verify the new parameter is correctly typed in the public API.

  2. Consider consolidating some E2E tests: A few of the pills E2E tests that share the same setup (e.g., test_pills_query_param_clearable_empty and test_pills_query_param_single_truncates_multiple both seed with URL params and check the result) could be combined into a single aggregated scenario test to reduce browser loads, per the E2E best practices guidance. This is a minor optimization suggestion, not a blocker.

Verdict

APPROVED: Clean, well-tested implementation that correctly adds query parameter binding to st.pills and st.segmented_control, following established patterns from st.multiselect with comprehensive test coverage across all three test layers.


This is an automated AI review by opus-4.6-thinking.

@mayagbarnes mayagbarnes marked this pull request as ready for review February 19, 2026 07:59
@github-actions
Copy link
Copy Markdown
Contributor

Summary

This PR adds bind="query-params" support to st.pills and st.segmented_control, including backend wiring, frontend query-param registration, proto support, and broad test coverage across unit, typing, and e2e tests. The implementation follows existing query-binding patterns used by other widgets and correctly handles clearability, invalid URL values, multi-value URL format, and default-value URL cleanup.

Code Quality

The code structure is clean and consistent with existing widget implementations:

  • Backend integration in lib/streamlit/elements/widgets/button_group.py is centralized and reuses existing serde and option-validation flows instead of introducing special-case paths.
  • Frontend integration in frontend/lib/src/components/widgets/ButtonGroup/ButtonGroup.tsx cleanly composes with useBasicWidgetState and existing queryParamBinding conventions (string_array_value, repeated URL format).
  • API and type surface updates are consistent (bind overloads and docs, typing tests, and proto schema update).
  • No maintainability red flags found in the changed code paths.

Test Coverage

Coverage is strong and multi-layered:

  • Python unit tests validate bind key propagation, missing key rejection, and invalid bind value handling in lib/tests/streamlit/elements/button_group_test.py.
  • Frontend unit tests validate query binding registration/unregistration behavior in frontend/lib/src/components/widgets/ButtonGroup/ButtonGroup.test.tsx.
  • Typing tests validate new bind signatures for both widgets in lib/tests/streamlit/typing/pills_types.py and lib/tests/streamlit/typing/segmented_control_types.py.
  • E2E tests cover seeding, updates, invalid/empty values, default override behavior, deduplication, and multi-value URL semantics in e2e_playwright/st_pills_test.py and e2e_playwright/st_segmented_control_test.py.

The added tests are adequate for the behavior introduced.

Backwards Compatibility

No breaking changes identified:

  • bind is optional and defaults to None.
  • Existing widget behavior without bind is preserved.
  • Proto change is additive (optional string query_param_key = 15), which is backward compatible.

Security & Risk

No security concerns identified in the diff.

Regression risk appears low because:

  • The implementation follows established query-binding mechanisms already used by other widgets.
  • Edge-case handling for invalid and empty URL inputs is explicitly covered.

Accessibility

No accessibility regressions identified from this change set.

  • The frontend update is binding/state plumbing and does not alter interactive semantics, keyboard behavior, or labeling structure of the underlying controls.

Recommendations

  1. Optionally add one segmented-control e2e case for duplicate URL values (parallel to pills) to keep symmetry and future-proof against behavior drift.

Verdict

APPROVED: The changes are well-implemented, backward compatible, and sufficiently tested for merge.


This is an automated AI review by gpt-5.3-codex-high. Please verify the feedback and use your judgment.

@mayagbarnes mayagbarnes merged commit 9d30e09 into develop Feb 20, 2026
43 of 44 checks passed
@mayagbarnes mayagbarnes deleted the query-pills branch February 20, 2026 21:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change:feature PR contains new feature or enhancement implementation impact:users PR changes affect end users

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants