Repository: streamlit/streamlit

Issue:
Implement label_visibility with Checkbox
### Problem

Streamlit 1.13.0 released with `label_visibility`, but noticeably missing is the checkbox. The feature was designed to work with vertical alignment of widgets which have no impact on the checkbox. There may be reasons to have that.

### Solution

**MVP:** Users can continue to add `""`, but the checkbox is not accessible, and there may be unnecessary spacing.

**Preferred solution:** Add `label_visibility` to checkboxes.

### Additional context

Add any other context or screenshots about the feature request here. For example, did this FR come from https://discuss.streamlit.io or another site? Link the original source here!

---

Community voting on feature requests enables the Streamlit team to understand which features are most important to our users.

**If you'd like the Streamlit team to prioritize this feature request, please use the 👍 (thumbs up emoji) reaction in response to the initial post.**



Discussion:

Resolving patch:
diff --git a/e2e/scripts/st_checkbox.py b/e2e/scripts/st_checkbox.py
--- a/e2e/scripts/st_checkbox.py
+++ b/e2e/scripts/st_checkbox.py
@@ -38,3 +38,9 @@ def on_change():
 
 i6 = st.checkbox("checkbox 6", value=True, disabled=True)
 st.write("value 6:", i6)
+
+i7 = st.checkbox("checkbox 7", label_visibility="hidden")
+st.write("value 7:", i7)
+
+i8 = st.checkbox("checkbox 8", label_visibility="collapsed")
+st.write("value 8:", i8)
diff --git a/lib/streamlit/elements/checkbox.py b/lib/streamlit/elements/checkbox.py
--- a/lib/streamlit/elements/checkbox.py
+++ b/lib/streamlit/elements/checkbox.py
@@ -17,7 +17,11 @@
 from typing import TYPE_CHECKING, Optional, cast
 
 from streamlit.elements.form import current_form_id
-from streamlit.elements.utils import check_callback_rules, check_session_state_rules
+from streamlit.elements.utils import (
+    check_callback_rules,
+    check_session_state_rules,
+    get_label_visibility_proto_value,
+)
 from streamlit.proto.Checkbox_pb2 import Checkbox as CheckboxProto
 from streamlit.runtime.metrics_util import gather_metrics
 from streamlit.runtime.scriptrunner import ScriptRunContext, get_script_run_ctx
@@ -27,7 +31,7 @@
     WidgetKwargs,
     register_widget,
 )
-from streamlit.type_util import Key, to_key
+from streamlit.type_util import Key, LabelVisibility, maybe_raise_label_warnings, to_key
 
 if TYPE_CHECKING:
     from streamlit.delta_generator import DeltaGenerator
@@ -57,6 +61,7 @@ def checkbox(
         kwargs: Optional[WidgetKwargs] = None,
         *,  # keyword-only arguments:
         disabled: bool = False,
+        label_visibility: LabelVisibility = "visible",
     ) -> bool:
         """Display a checkbox widget.
 
@@ -85,6 +90,11 @@ def checkbox(
         disabled : bool
             An optional boolean, which disables the checkbox if set to True.
             The default is False. This argument can only be supplied by keyword.
+        label_visibility : "visible" or "hidden" or "collapsed"
+            The visibility of the label. If "hidden", the label doesn't show but there
+            is still empty space for it (equivalent to label="").
+            If "collapsed", both the label and the space are removed. Default is
+            "visible". This argument can only be supplied by keyword.
 
         Returns
         -------
@@ -114,6 +124,7 @@ def checkbox(
             args=args,
             kwargs=kwargs,
             disabled=disabled,
+            label_visibility=label_visibility,
             ctx=ctx,
         )
 
@@ -128,6 +139,7 @@ def _checkbox(
         kwargs: Optional[WidgetKwargs] = None,
         *,  # keyword-only arguments:
         disabled: bool = False,
+        label_visibility: LabelVisibility = "visible",
         ctx: Optional[ScriptRunContext] = None,
     ) -> bool:
         key = to_key(key)
@@ -136,6 +148,8 @@ def _checkbox(
             default_value=None if value is False else value, key=key
         )
 
+        maybe_raise_label_warnings(label, label_visibility)
+
         checkbox_proto = CheckboxProto()
         checkbox_proto.label = label
         checkbox_proto.default = bool(value)
@@ -160,6 +174,10 @@ def _checkbox(
         # This needs to be done after register_widget because we don't want
         # the following proto fields to affect a widget's ID.
         checkbox_proto.disabled = disabled
+        checkbox_proto.label_visibility.value = get_label_visibility_proto_value(
+            label_visibility
+        )
+
         if checkbox_state.value_changed:
             checkbox_proto.value = checkbox_state.value
             checkbox_proto.set_value = True


Test patch:
diff --git a/frontend/src/components/widgets/Checkbox/Checkbox.test.tsx b/frontend/src/components/widgets/Checkbox/Checkbox.test.tsx
--- a/frontend/src/components/widgets/Checkbox/Checkbox.test.tsx
+++ b/frontend/src/components/widgets/Checkbox/Checkbox.test.tsx
@@ -21,7 +21,10 @@ import { WidgetStateManager } from "src/lib/WidgetStateManager"
 import StreamlitMarkdown from "src/components/shared/StreamlitMarkdown"
 
 import { Checkbox as UICheckbox } from "baseui/checkbox"
-import { Checkbox as CheckboxProto } from "src/autogen/proto"
+import {
+  Checkbox as CheckboxProto,
+  LabelVisibilityMessage as LabelVisibilityMessageProto,
+} from "src/autogen/proto"
 import Checkbox, { OwnProps } from "./Checkbox"
 
 const getProps = (elementProps: Partial<CheckboxProto> = {}): OwnProps => ({
@@ -86,6 +89,30 @@ describe("Checkbox widget", () => {
     expect(wrapper.find(StreamlitMarkdown).props().isLabel).toBe(true)
   })
 
+  it("pass labelVisibility prop to StyledContent correctly when hidden", () => {
+    const props = getProps({
+      labelVisibility: {
+        value: LabelVisibilityMessageProto.LabelVisibilityOptions.HIDDEN,
+      },
+    })
+    const wrapper = mount(<Checkbox {...props} />)
+    expect(wrapper.find("StyledContent").prop("visibility")).toEqual(
+      LabelVisibilityMessageProto.LabelVisibilityOptions.HIDDEN
+    )
+  })
+
+  it("pass labelVisibility prop to StyledContent correctly when collapsed", () => {
+    const props = getProps({
+      labelVisibility: {
+        value: LabelVisibilityMessageProto.LabelVisibilityOptions.COLLAPSED,
+      },
+    })
+    const wrapper = mount(<Checkbox {...props} />)
+    expect(wrapper.find("StyledContent").prop("visibility")).toEqual(
+      LabelVisibilityMessageProto.LabelVisibilityOptions.COLLAPSED
+    )
+  })
+
   it("is unchecked by default", () => {
     const props = getProps()
     const wrapper = mount(<Checkbox {...props} />)
diff --git a/lib/tests/streamlit/elements/checkbox_test.py b/lib/tests/streamlit/elements/checkbox_test.py
--- a/lib/tests/streamlit/elements/checkbox_test.py
+++ b/lib/tests/streamlit/elements/checkbox_test.py
@@ -19,6 +19,9 @@
 from parameterized import parameterized
 
 import streamlit as st
+from streamlit.errors import StreamlitAPIException
+from streamlit.proto.LabelVisibilityMessage_pb2 import LabelVisibilityMessage
+from streamlit.type_util import _LOGGER
 from tests.delta_generator_test_case import DeltaGeneratorTestCase
 
 
@@ -37,6 +40,10 @@ def test_just_label(self):
         self.assertEqual(c.label, "the label")
         self.assertEqual(c.default, False)
         self.assertEqual(c.disabled, False)
+        self.assertEqual(
+            c.label_visibility.value,
+            LabelVisibilityMessage.LabelVisibilityOptions.VISIBLE,
+        )
 
     def test_just_disabled(self):
         """Test that it can be called with disabled param."""
@@ -99,3 +106,38 @@ def test_checkbox_help_dedents(self):
         self.assertEqual(c.label, "Checkbox label")
         self.assertEqual(c.default, True)
         self.assertEqual(c.help, "hello\n world\n")
+
+    @parameterized.expand(
+        [
+            ("visible", LabelVisibilityMessage.LabelVisibilityOptions.VISIBLE),
+            ("hidden", LabelVisibilityMessage.LabelVisibilityOptions.HIDDEN),
+            ("collapsed", LabelVisibilityMessage.LabelVisibilityOptions.COLLAPSED),
+        ]
+    )
+    def test_label_visibility(self, label_visibility_value, proto_value):
+        """Test that it can be called with label_visibility param."""
+        st.checkbox("the label", label_visibility=label_visibility_value)
+
+        c = self.get_delta_from_queue().new_element.checkbox
+        self.assertEqual(c.label, "the label")
+        self.assertEqual(c.label_visibility.value, proto_value)
+
+    def test_label_visibility_wrong_value(self):
+        with self.assertRaises(StreamlitAPIException) as e:
+            st.checkbox("the label", label_visibility="wrong_value")
+        self.assertEqual(
+            str(e.exception),
+            "Unsupported label_visibility option 'wrong_value'. Valid values are "
+            "'visible', 'hidden' or 'collapsed'.",
+        )
+
+    def test_empty_label_warning(self):
+        """Test that a warning is logged if st.checkbox was called with empty label."""
+
+        with self.assertLogs(_LOGGER) as logs:
+            st.checkbox(label="")
+
+        self.assertIn(
+            "`label` got an empty value. This is discouraged for accessibility reasons",
+            logs.records[0].msg,
+        )