Repository: streamlit/streamlit

Issue:
Radio button without pre-selection
This option is possible as I saw in other platforms. It would be cool if you also add this option. 

---

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:
Yes, this would a convenient feature. I currently insert a blank element as the 1st radio button element, as a current workaround.
@kiansina Thanks for your suggestion! This sounds like a useful enhancement 👍  I will forward this feature request to our product team.

For other users reading this enhancement: please upvote the issue (👍) if you also want to have this implemented!
As a temporary solution I am using hacked checkboxes. No preselected box and only one can be picked at a time. Hope you find it useful 😄 .

```python
import streamlit as st

def disable_other_checkboxes(*other_checkboxes_keys):
    for checkbox_key in other_checkboxes_keys:
        st.session_state[checkbox_key] = False


option_1 = st.checkbox(
    "Option_1",
    key="op1",
    on_change=disable_other_checkboxes,
    args=("op2", "op3", "op4"),
)
option_2 = st.checkbox(
    "Option_2",
    key="op2",
    on_change=disable_other_checkboxes,
    args=("op1", "op3", "op4"),
)
option_3 = st.checkbox(
    "Option_3",
    key="op3",
    on_change=disable_other_checkboxes,
    args=("op2", "op1", "op4"),
)
option_4 = st.checkbox(
    "Option_4",
    key="op4",
    on_change=disable_other_checkboxes,
    args=("op2", "op3", "op1"),
)

st.write(option_1, option_2, option_3, option_4)
```
<img width="408" alt="Screenshot 2022-08-31 at 18 25 55" src="https://user-images.githubusercontent.com/41453558/187730074-8b8e095b-1ee9-4ccf-a6b5-550d79137127.png">

Can you all describe what exactly you want this for? This would help us decide if we want to support it or not.
My use case would be for a survey where we want to make sure users have responded to a question. I was planning on tracking if they've responded using session state but that doesn't account for the situation where the pre-selected option is what the user would actually like to respond to. 

Basically just trying to hack a way to get "required" questions when using `st.forms`
Hey all, just wanted to give a quick update. This issue is on our radar. The blocker for it is that (unfortunately!) the UI framework we use under the hood (BaseWeb) doesn’t support radio buttons without a default selection. So if we want to fix this, we’d need to either use a different UI framework (lots of work) or build our own component (also lots of work). It’s definitely possible we’ll do this at some point but for right now it seems like too big of a time investment.

Resolving patch:
diff --git a/e2e_playwright/st_radio.py b/e2e_playwright/st_radio.py
new file mode 100644
--- /dev/null
+++ b/e2e_playwright/st_radio.py
@@ -0,0 +1,97 @@
+# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pandas as pd
+
+import streamlit as st
+from streamlit import runtime
+
+options = ("female", "male")
+markdown_options = (
+    "**bold text**",
+    "*italics text*",
+    "~strikethrough text~",
+    "shortcode: :blush:",
+    # link should not work in radio options
+    "[link text](www.example.com)",
+    "`code text`",
+    ":red[red] :blue[blue] :green[green] :violet[violet] :orange[orange]",
+)
+
+i1 = st.radio("radio 1 (default)", options)
+st.write("value 1:", i1)
+
+i2 = st.radio(
+    "radio 2 (Formatted options)",
+    options,
+    1,
+    format_func=lambda x: x.capitalize(),
+)
+st.write("value 2:", i2)
+
+i3 = st.radio("radio 3 (no options)", [])
+st.write("value 3:", i3)
+
+i4 = st.radio("radio 4 (disabled)", options, disabled=True)
+st.write("value 4:", i4)
+
+i5 = st.radio("radio 5 (horizontal)", options, horizontal=True)
+st.write("value 5:", i5)
+
+i6 = st.radio("radio 6 (options from dataframe)", pd.DataFrame({"foo": list(options)}))
+st.write("value 6:", i6)
+
+i7 = st.radio("radio 7 (hidden label)", options, label_visibility="hidden")
+st.write("value 7:", i7)
+
+i8 = st.radio("radio 8 (collapsed label)", options, label_visibility="collapsed")
+st.write("value 8:", i8)
+
+i9 = st.radio("radio 9 (markdown options)", options=markdown_options)
+st.write("value 9:", i9)
+
+i10 = st.radio(
+    "radio 10 (with captions)",
+    ["A", "B", "C", "D", "E", "F", "G"],
+    captions=markdown_options,
+)
+st.write("value 10:", i10)
+
+i11 = st.radio(
+    "radio 11 (horizontal, captions)",
+    ["yes", "maybe", "no"],
+    captions=["Opt in", "", "Opt out"],
+    horizontal=True,
+)
+st.write("value 11:", i11)
+
+if runtime.exists():
+
+    def on_change():
+        st.session_state.radio_changed = True
+        st.text("Radio widget callback triggered")
+
+    st.radio(
+        "radio 12 (with callback, help)",
+        options,
+        1,
+        key="radio12",
+        on_change=on_change,
+        help="help text",
+    )
+    st.write("value 12:", st.session_state.radio12)
+    st.write("radio changed:", "radio_changed" in st.session_state)
+
+i13 = st.radio("radio 13 (empty selection)", options, index=None)
+st.write("value 13:", i13)
diff --git a/lib/streamlit/elements/widgets/radio.py b/lib/streamlit/elements/widgets/radio.py
--- a/lib/streamlit/elements/widgets/radio.py
+++ b/lib/streamlit/elements/widgets/radio.py
@@ -12,9 +12,11 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from __future__ import annotations
+
 from dataclasses import dataclass
 from textwrap import dedent
-from typing import TYPE_CHECKING, Any, Callable, Generic, Optional, Sequence, cast
+from typing import TYPE_CHECKING, Any, Callable, Generic, Sequence, cast
 
 from streamlit.elements.form import current_form_id
 from streamlit.elements.utils import (
@@ -51,23 +53,26 @@
 @dataclass
 class RadioSerde(Generic[T]):
     options: Sequence[T]
-    index: int
+    index: int | None
+
+    def serialize(self, v: object) -> int | None:
+        if v is None:
+            return None
 
-    def serialize(self, v: object) -> int:
-        if len(self.options) == 0:
-            return 0
-        return index_(self.options, v)
+        return 0 if len(self.options) == 0 else index_(self.options, v)
 
     def deserialize(
         self,
-        ui_value: Optional[int],
+        ui_value: int | None,
         widget_id: str = "",
-    ) -> Optional[T]:
+    ) -> T | None:
         idx = ui_value if ui_value is not None else self.index
 
         return (
             self.options[idx]
-            if len(self.options) > 0 and self.options[idx] is not None
+            if idx is not None
+            and len(self.options) > 0
+            and self.options[idx] is not None
             else None
         )
 
@@ -78,19 +83,19 @@ def radio(
         self,
         label: str,
         options: OptionSequence[T],
-        index: int = 0,
+        index: int | None = 0,
         format_func: Callable[[Any], Any] = str,
-        key: Optional[Key] = None,
-        help: Optional[str] = None,
-        on_change: Optional[WidgetCallback] = None,
-        args: Optional[WidgetArgs] = None,
-        kwargs: Optional[WidgetKwargs] = None,
+        key: Key | None = None,
+        help: str | None = None,
+        on_change: WidgetCallback | None = None,
+        args: WidgetArgs | None = None,
+        kwargs: WidgetKwargs | None = None,
         *,  # keyword-only args:
         disabled: bool = False,
         horizontal: bool = False,
-        captions: Optional[Sequence[str]] = None,
+        captions: Sequence[str] | None = None,
         label_visibility: LabelVisibility = "visible",
-    ) -> Optional[T]:
+    ) -> T | None:
         r"""Display a radio button widget.
 
         Parameters
@@ -126,8 +131,10 @@ def radio(
             described in the ``label`` parameter and will be cast to str
             internally by default. For pandas.DataFrame, the first column is
             selected.
-        index : int
-            The index of the preselected option on first render.
+        index : int or None
+            The index of the preselected option on first render. If ``None``,
+            will initialize empty and return ``None`` until the user selects an option.
+            Defaults to 0 (the first option).
         format_func : function
             Function to modify the display of radio options. It receives
             the raw option as an argument and should output the label to be
@@ -166,7 +173,7 @@ def radio(
         Returns
         -------
         any
-            The selected option.
+            The selected option or ``None`` if no option is selected.
 
         Example
         -------
@@ -186,6 +193,22 @@ def radio(
            https://doc-radio.streamlit.app/
            height: 300px
 
+        To initialize an empty radio widget, use ``None`` as the index value:
+
+        >>> import streamlit as st
+        >>>
+        >>> genre = st.radio(
+        ...     "What's your favorite movie genre",
+        ...     [":rainbow[Comedy]", "***Drama***", "Documentary :movie_camera:"],
+        ...     index=None,
+        ... )
+        >>>
+        >>> st.write("You selected:", genre)
+
+        .. output::
+           https://doc-radio-empty.streamlit.app/
+           height: 300px
+
         """
         ctx = get_script_run_ctx()
         return self._radio(
@@ -209,20 +232,20 @@ def _radio(
         self,
         label: str,
         options: OptionSequence[T],
-        index: int = 0,
+        index: int | None = 0,
         format_func: Callable[[Any], Any] = str,
-        key: Optional[Key] = None,
-        help: Optional[str] = None,
-        on_change: Optional[WidgetCallback] = None,
-        args: Optional[WidgetArgs] = None,
-        kwargs: Optional[WidgetKwargs] = None,
+        key: Key | None = None,
+        help: str | None = None,
+        on_change: WidgetCallback | None = None,
+        args: WidgetArgs | None = None,
+        kwargs: WidgetKwargs | None = None,
         *,  # keyword-only args:
         disabled: bool = False,
         horizontal: bool = False,
         label_visibility: LabelVisibility = "visible",
-        captions: Optional[Sequence[str]] = None,
-        ctx: Optional[ScriptRunContext],
-    ) -> Optional[T]:
+        captions: Sequence[str] | None = None,
+        ctx: ScriptRunContext | None,
+    ) -> T | None:
         key = to_key(key)
         check_callback_rules(self.dg, on_change)
         check_session_state_rules(default_value=None if index == 0 else index, key=key)
@@ -243,17 +266,17 @@ def _radio(
             page=ctx.page_script_hash if ctx else None,
         )
 
-        if not isinstance(index, int):
+        if not isinstance(index, int) and index is not None:
             raise StreamlitAPIException(
                 "Radio Value has invalid type: %s" % type(index).__name__
             )
 
-        if len(opt) > 0 and not 0 <= index < len(opt):
+        if index is not None and len(opt) > 0 and not 0 <= index < len(opt):
             raise StreamlitAPIException(
                 "Radio index must be between 0 and length of options"
             )
 
-        def handle_captions(caption: Optional[str]) -> str:
+        def handle_captions(caption: str | None) -> str:
             if caption is None:
                 return ""
             elif isinstance(caption, str):
@@ -266,7 +289,8 @@ def handle_captions(caption: Optional[str]) -> str:
         radio_proto = RadioProto()
         radio_proto.id = id
         radio_proto.label = label
-        radio_proto.default = index
+        if index is not None:
+            radio_proto.default = index
         radio_proto.options[:] = [str(format_func(option)) for option in opt]
         radio_proto.form_id = current_form_id(self.dg)
         radio_proto.horizontal = horizontal
@@ -296,7 +320,10 @@ def handle_captions(caption: Optional[str]) -> str:
         )
 
         if widget_state.value_changed:
-            radio_proto.value = serde.serialize(widget_state.value)
+            if widget_state.value is not None:
+                serialized_value = serde.serialize(widget_state.value)
+                if serialized_value is not None:
+                    radio_proto.value = serialized_value
             radio_proto.set_value = True
 
         self.dg._enqueue("radio", radio_proto)


Test patch:
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[dark_theme-chromium].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[dark_theme-firefox].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[dark_theme-webkit].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[light_theme-chromium].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[light_theme-firefox].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[light_theme-webkit].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[dark_theme-chromium].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[dark_theme-firefox].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[dark_theme-webkit].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[light_theme-chromium].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[light_theme-firefox].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[light_theme-webkit].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[dark_theme-chromium].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[dark_theme-firefox].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[dark_theme-webkit].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[light_theme-chromium].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[light_theme-firefox].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[light_theme-webkit].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[dark_theme-chromium].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[dark_theme-firefox].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[dark_theme-webkit].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[light_theme-chromium].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[light_theme-firefox].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[light_theme-webkit].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[dark_theme-chromium].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[dark_theme-firefox].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[dark_theme-webkit].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[light_theme-chromium].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[light_theme-firefox].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[light_theme-webkit].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[dark_theme-chromium].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[dark_theme-firefox].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[dark_theme-webkit].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[light_theme-chromium].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[light_theme-firefox].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[light_theme-webkit].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[dark_theme-chromium].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[dark_theme-firefox].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[dark_theme-webkit].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[light_theme-chromium].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[light_theme-firefox].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[light_theme-webkit].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[dark_theme-chromium].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[dark_theme-firefox].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[dark_theme-webkit].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[light_theme-chromium].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[light_theme-firefox].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[light_theme-webkit].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[dark_theme-chromium].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[dark_theme-firefox].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[dark_theme-webkit].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[light_theme-chromium].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[light_theme-firefox].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[light_theme-webkit].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[dark_theme-chromium].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[dark_theme-firefox].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[dark_theme-webkit].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[light_theme-chromium].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[light_theme-firefox].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[light_theme-webkit].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[dark_theme-chromium].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[dark_theme-firefox].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[dark_theme-webkit].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[light_theme-chromium].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[light_theme-firefox].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[light_theme-webkit].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[dark_theme-chromium].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[dark_theme-firefox].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[dark_theme-webkit].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[light_theme-chromium].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[light_theme-firefox].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[light_theme-webkit].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[dark_theme-chromium].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[dark_theme-firefox].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[dark_theme-webkit].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[light_theme-chromium].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[light_theme-firefox].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[light_theme-webkit].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[light_theme-webkit].png differ
diff --git a/e2e_playwright/st_radio_test.py b/e2e_playwright/st_radio_test.py
new file mode 100644
--- /dev/null
+++ b/e2e_playwright/st_radio_test.py
@@ -0,0 +1,124 @@
+# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from playwright.sync_api import Page, expect
+
+from e2e_playwright.conftest import ImageCompareFunction
+
+
+def test_radio_widget_rendering(
+    themed_app: Page, assert_snapshot: ImageCompareFunction
+):
+    """Test that the radio widgets are correctly rendered via screenshot matching."""
+    radio_widgets = themed_app.get_by_test_id("stRadio")
+    expect(radio_widgets).to_have_count(13)
+
+    assert_snapshot(radio_widgets.nth(0), name="st_radio-default")
+    assert_snapshot(radio_widgets.nth(1), name="st_radio-formatted_options")
+    assert_snapshot(radio_widgets.nth(2), name="st_radio-no_options")
+    assert_snapshot(radio_widgets.nth(3), name="st_radio-disabled")
+    assert_snapshot(radio_widgets.nth(4), name="st_radio-horizontal")
+    assert_snapshot(radio_widgets.nth(5), name="st_radio-dataframe_options")
+    assert_snapshot(radio_widgets.nth(6), name="st_radio-hidden_label")
+    assert_snapshot(radio_widgets.nth(7), name="st_radio-collapsed_label")
+    assert_snapshot(radio_widgets.nth(8), name="st_radio-markdown_options")
+    assert_snapshot(radio_widgets.nth(9), name="st_radio-captions")
+    assert_snapshot(radio_widgets.nth(10), name="st_radio-horizontal_captions")
+    assert_snapshot(radio_widgets.nth(11), name="st_radio-callback_help")
+    assert_snapshot(radio_widgets.nth(12), name="st_radio-empty_selection")
+
+
+def test_radio_has_correct_default_values(app: Page):
+    """Test that st.radio returns the correct initial values."""
+    markdown_elements = app.get_by_test_id("stMarkdown")
+    expect(markdown_elements).to_have_count(14)
+
+    expected = [
+        "value 1: female",
+        "value 2: male",
+        "value 3: None",
+        "value 4: female",
+        "value 5: female",
+        "value 6: female",
+        "value 7: female",
+        "value 8: female",
+        "value 9: bold text",
+        "value 10: A",
+        "value 11: yes",
+        "value 12: male",
+        "radio changed: False",
+        "value 13: None",
+    ]
+
+    for markdown_element, expected_text in zip(markdown_elements.all(), expected):
+        expect(markdown_element).to_have_text(expected_text, use_inner_text=True)
+
+
+def test_set_value_correctly_when_click(app: Page):
+    """Test that st.radio returns the correct values when the selection is changed."""
+    for index, element in enumerate(app.get_by_test_id("stRadio").all()):
+        if index != 3:  # skip disabled widget
+            element.locator('label[data-baseweb="radio"]').last.click(force=True)
+
+    expected = [
+        "value 1: male",
+        "value 2: male",
+        "value 3: None",
+        "value 4: female",
+        "value 5: male",
+        "value 6: male",
+        "value 7: male",
+        "value 8: male",
+        "value 9: red blue green violet orange",
+        "value 10: G",
+        "value 11: no",
+        "value 12: male",
+        "radio changed: False",
+        "value 13: male",
+    ]
+
+    for markdown_element, expected_text in zip(
+        app.get_by_test_id("stMarkdown").all(), expected
+    ):
+        expect(markdown_element).to_have_text(expected_text, use_inner_text=True)
+
+
+def test_calls_callback_on_change(app: Page):
+    """Test that it correctly calls the callback on change."""
+    radio_widget = app.get_by_test_id("stRadio").nth(11)
+
+    radio_widget.locator('label[data-baseweb="radio"]').first.click(force=True)
+
+    expect(app.get_by_test_id("stMarkdown").nth(11)).to_have_text(
+        "value 12: female",
+        use_inner_text=True,
+    )
+    expect(app.get_by_test_id("stMarkdown").nth(12)).to_have_text(
+        "radio changed: True",
+        use_inner_text=True,
+    )
+
+    # Change different date input to trigger delta path change
+    first_date_input_field = app.get_by_test_id("stRadio").first
+    first_date_input_field.locator('label[data-baseweb="radio"]').last.click(force=True)
+
+    expect(app.get_by_test_id("stMarkdown").first).to_have_text(
+        "value 1: male", use_inner_text=True
+    )
+
+    # Test if value is still correct after delta path change
+    expect(app.get_by_test_id("stMarkdown").nth(11)).to_have_text(
+        "value 12: female",
+        use_inner_text=True,
+    )
diff --git a/frontend/lib/src/components/shared/Radio/Radio.test.tsx b/frontend/lib/src/components/shared/Radio/Radio.test.tsx
--- a/frontend/lib/src/components/shared/Radio/Radio.test.tsx
+++ b/frontend/lib/src/components/shared/Radio/Radio.test.tsx
@@ -101,6 +101,7 @@ describe("Radio widget", () => {
     const radioOptions = screen.getAllByRole("radio")
     expect(radioOptions).toHaveLength(3)
 
+    // @ts-expect-error
     const checked = radioOptions[props.value]
     expect(checked).toBeChecked()
   })
diff --git a/frontend/lib/src/components/widgets/Radio/Radio.test.tsx b/frontend/lib/src/components/widgets/Radio/Radio.test.tsx
--- a/frontend/lib/src/components/widgets/Radio/Radio.test.tsx
+++ b/frontend/lib/src/components/widgets/Radio/Radio.test.tsx
@@ -91,6 +91,7 @@ describe("Radio widget", () => {
     const radioOptions = screen.getAllByRole("radio")
     expect(radioOptions).toHaveLength(3)
 
+    // @ts-expect-error
     const checked = radioOptions[props.element.default]
     expect(checked).toBeChecked()
   })
@@ -192,6 +193,7 @@ describe("Radio widget", () => {
     props.widgetMgr.submitForm("form")
 
     // Our widget should be reset, and the widgetMgr should be updated
+    // @ts-expect-error
     const defaultValue = radioOptions[props.element.default]
     expect(defaultValue).toBeChecked()
 
diff --git a/lib/streamlit/testing/element_tree.py b/lib/streamlit/testing/element_tree.py
--- a/lib/streamlit/testing/element_tree.py
+++ b/lib/streamlit/testing/element_tree.py
@@ -62,6 +62,7 @@
 T = TypeVar("T")
 
 
+@dataclass
 class InitialValue:
     """This class is used to represent the initial value of a widget."""
 
@@ -594,7 +595,7 @@ def decrement(self) -> NumberInput:
 
 @dataclass(repr=False)
 class Radio(Widget, Generic[T]):
-    _value: T | None
+    _value: T | None | InitialValue
 
     proto: RadioProto
     options: list[str]
@@ -603,7 +604,7 @@ class Radio(Widget, Generic[T]):
     def __init__(self, proto: RadioProto, root: ElementTree):
         self.proto = proto
         self.root = root
-        self._value = None
+        self._value = InitialValue()
 
         self.type = "radio"
         self.id = proto.id
@@ -616,20 +617,22 @@ def __init__(self, proto: RadioProto, root: ElementTree):
         self.key = user_key_from_widget_id(self.id)
 
     @property
-    def index(self) -> int:
+    def index(self) -> int | None:
+        if self.value is None:
+            return None
         return self.options.index(str(self.value))
 
     @property
-    def value(self) -> T:
+    def value(self) -> T | None:
         """The currently selected value from the options."""
-        if self._value is not None:
+        if not isinstance(self._value, InitialValue):
             return self._value
         else:
             state = self.root.session_state
             assert state
             return cast(T, state[self.id])
 
-    def set_value(self, v: T) -> Radio[T]:
+    def set_value(self, v: T | None) -> Radio[T]:
         self._value = v
         return self
 
@@ -640,7 +643,8 @@ def widget_state(self) -> WidgetState:
         """
         ws = WidgetState()
         ws.id = self.id
-        ws.int_value = self.index
+        if self.index is not None:
+            ws.int_value = self.index
         return ws
 
 
diff --git a/lib/tests/streamlit/elements/radio_test.py b/lib/tests/streamlit/elements/radio_test.py
--- a/lib/tests/streamlit/elements/radio_test.py
+++ b/lib/tests/streamlit/elements/radio_test.py
@@ -23,6 +23,7 @@
 import streamlit as st
 from streamlit.errors import StreamlitAPIException
 from streamlit.proto.LabelVisibilityMessage_pb2 import LabelVisibilityMessage
+from streamlit.testing.script_interactions import InteractiveScriptTests
 from tests.delta_generator_test_case import DeltaGeneratorTestCase
 
 
@@ -41,6 +42,7 @@ def test_just_label(self):
         )
         self.assertEqual(c.default, 0)
         self.assertEqual(c.disabled, False)
+        self.assertEqual(c.HasField("default"), True)
         self.assertEqual(c.captions, [])
 
     def test_just_disabled(self):
@@ -50,6 +52,17 @@ def test_just_disabled(self):
         c = self.get_delta_from_queue().new_element.radio
         self.assertEqual(c.disabled, True)
 
+    def test_none_value(self):
+        """Test that it can be called with None as index value."""
+        st.radio("the label", ("m", "f"), index=None)
+
+        c = self.get_delta_from_queue().new_element.radio
+        self.assertEqual(c.label, "the label")
+        # If a proto property is null is not determined by this value,
+        # but by the check via the HasField method:
+        self.assertEqual(c.default, 0)
+        self.assertEqual(c.HasField("default"), False)
+
     def test_horizontal(self):
         """Test that it can be called with horizontal param."""
         st.radio("the label", ("m", "f"), horizontal=True)
@@ -235,3 +248,28 @@ def test_some_captions(self):
         self.assertEqual(c.label, "the label")
         self.assertEqual(c.default, 0)
         self.assertEqual(c.captions, ["first caption", "", "", "last caption"])
+
+
+class RadioInteractiveTest(InteractiveScriptTests):
+    def test_radio_interaction(self):
+        """Test interactions with an empty radio widget."""
+        script = self.script_from_string(
+            """
+        import streamlit as st
+
+        st.radio("the label", ("m", "f"), index=None)
+        """
+        )
+        sr = script.run()
+        radio = sr.radio[0]
+        assert radio.value is None
+
+        # Select option m
+        sr2 = radio.set_value("m").run()
+        radio = sr2.radio[0]
+        assert radio.value == "m"
+
+        # # Clear the value
+        sr3 = radio.set_value(None).run()
+        radio = sr3.radio[0]
+        assert radio.value is None