Repository: streamlit/streamlit
Pull request: Allow `None` as index for `st.selectbox`
## Describe your changes

This PR adds support for setting `None` as the index in `st.selectbox`. Using `index=None` will render an empty selectbox with no option selected, which returns `None` as long as the user hasn't selected an option:

<img width="718" alt="image" src="https://github.com/streamlit/streamlit/assets/2852129/47e38536-7457-48b8-acc5-f94db03dd058">

- Spec: https://www.notion.so/snowflake-corp/Spec-v2-b00de8d56f99464693e7ddfdea0674e9

## GitHub Issue Link (if applicable)

- Closes #949

## Testing Plan

- Unit Tests: ✅
- E2E Tests: ✅

---

**Contribution License Agreement**

By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license.


Problem statement:
st.selectbox - No selection per default 
### Problem

I believe it is currently not possible to use a selectbox that has nothing selected per default. This may be undesirable in some cases.

### Solution

Replace the `index`parameter with a `default` parameter just like the multiselect item does. Problem: This is a breaking change.

Possibly better: Instead of throwing an error when the provided index is not within the list, return `None` and show an empty selection in the UI instead of throwing an error. Problem: No explicit error is being raised in case the index out of bound in an unwanted scenario.

Possibly possibly better: An index of `-1` indicates an empty selection and `None` is returned. This might be a good balance since negative values are currently forbidden.

---

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.**

Hints from issue discussion:
Here is a workaround:
Example: display a map only if a valid zoom level is selected:
```
map_zoom = st.selectbox('Zoom level', 
                        ['<select>', 12, 11, 10, 9, 8, 7],
                        0)  # default value = index 0
if map_zoom != '<select>':    
        st.map(data, zoom=map_zoom)
```
That's correct there's no actual way to have an empty selectbox, but for now you could do something like this using an empty string plus the `format_func` or even using just the empty string

```
import streamlit as st

selected = st.selectbox('Select one option:', ['', 'First one', 'Second one'], format_func=lambda x: 'Select an option' if x == '' else x)

if selected:
    st.success('Yay! 🎉')
else:
    st.warning('No option is selected')

```
A clean solution would probably need to have a dedicated default value that cannot be mistaken for any actual value ( `` could possibly be a valid option to select). I didn't think of the `format_func` to actually format this exceptional value and thus had to use a string, but it's a very good point.

For reference, you'll find below the wrapper I wrote in December to handle a very similar use case.

```python
DEFAULT = '< PICK A VALUE >'

def selectbox_with_default(text, values, default=DEFAULT, sidebar=False):
    func = st.sidebar.selectbox if sidebar else st.selectbox
    return func(text, np.insert(np.array(values, object), 0, default))

```

I'd then call it from the `app.py` code with
```python
res = selectbox_with_default('some title', some_values)
if res == DEFAULT:
    st.warning("Please fill all the fields !")
    raise StopException

```


I would find it very useful to be able to have an selection box without a default value selected!
declare the variable before the select box

res=some_values
res = selectbox_with_default('some title', some_values)

[Here](https://extras.streamlit.app/No-Default%20Selectbox) is a third-party solution which has this behavior. This is part of the [streamlit-extras](https://github.com/arnaudmiribel/streamlit-extras) package.

```python
from streamlit_extras.no_default_selectbox import selectbox

result = selectbox("Select an option", ["A", "B", "C"])
st.write("Result:", result)
```

This returns `None` until the user selects one of the options.
We have this on our roadmap. No concrete date yet but I'd love to get it out soon-ish. 
Any idea in case of Striemlit selectbox component how we can handle showing the section option from start instead show based on the option select in select box..
We're working on this right now. It will work by setting `index=None`. See a preview here:

https://github.com/streamlit/streamlit/assets/127552870/f25119e3-c16d-4246-85e9-a0321af8d974




@jrieke "We're working on this right now. It will work by setting `index=None`. See a preview here:" which version is this working in?
As I tried the same with version 1.24.0 as below but it's not working throws an error.
`
option = st.selectbox(
            'How would you like to be contacted?',
            ('Email', 'Home phone', 'Mobile phone'),index=None)

        st.write('You selected:', option)
`

Commit message: Fix test

Patch to review:
diff --git a/e2e/scripts/st_selectbox.py b/e2e/scripts/st_selectbox.py
index 8ba9860fefdc..46b53f98c2a0 100644
--- a/e2e/scripts/st_selectbox.py
+++ b/e2e/scripts/st_selectbox.py
@@ -23,7 +23,7 @@
 i2 = st.selectbox("selectbox 2", options, 0, format_func=lambda x: x.capitalize())
 st.write("value 2:", i2)
 
-i3 = st.selectbox("selectbox 3", [])
+i3: None = st.selectbox("selectbox 3", [])
 st.write("value 3:", i3)
 
 more_options = [
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-callback_help[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-callback_help[dark_theme-chromium].png
new file mode 100644
index 000000000000..ab62d96948b7
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-callback_help[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-callback_help[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-callback_help[dark_theme-firefox].png
new file mode 100644
index 000000000000..50507fc446a3
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-callback_help[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-callback_help[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-callback_help[dark_theme-webkit].png
new file mode 100644
index 000000000000..bde300940b23
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-callback_help[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-callback_help[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-callback_help[light_theme-chromium].png
new file mode 100644
index 000000000000..998978a8bba5
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-callback_help[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-callback_help[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-callback_help[light_theme-firefox].png
new file mode 100644
index 000000000000..3ac972374d16
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-callback_help[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-callback_help[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-callback_help[light_theme-webkit].png
new file mode 100644
index 000000000000..59a4c51819b8
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-callback_help[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-clearable_input[chromium].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-clearable_input[chromium].png
new file mode 100644
index 000000000000..daad5c7b6dca
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-clearable_input[chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-clearable_input[firefox].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-clearable_input[firefox].png
new file mode 100644
index 000000000000..a46c6fc1eb45
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-clearable_input[firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-clearable_input[webkit].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-clearable_input[webkit].png
new file mode 100644
index 000000000000..743a08ab7fde
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-clearable_input[webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-collapsed_label[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-collapsed_label[dark_theme-chromium].png
new file mode 100644
index 000000000000..4106d105fa61
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-collapsed_label[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-collapsed_label[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-collapsed_label[dark_theme-firefox].png
new file mode 100644
index 000000000000..422f9990029b
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-collapsed_label[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-collapsed_label[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-collapsed_label[dark_theme-webkit].png
new file mode 100644
index 000000000000..b69e44f232f9
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-collapsed_label[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-collapsed_label[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-collapsed_label[light_theme-chromium].png
new file mode 100644
index 000000000000..ebedc65ba563
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-collapsed_label[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-collapsed_label[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-collapsed_label[light_theme-firefox].png
new file mode 100644
index 000000000000..b2ce3eb07496
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-collapsed_label[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-collapsed_label[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-collapsed_label[light_theme-webkit].png
new file mode 100644
index 000000000000..b075243a5505
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-collapsed_label[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-dataframe_options[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-dataframe_options[dark_theme-chromium].png
new file mode 100644
index 000000000000..4528bfe61bf6
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-dataframe_options[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-dataframe_options[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-dataframe_options[dark_theme-firefox].png
new file mode 100644
index 000000000000..f9071ce21cae
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-dataframe_options[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-dataframe_options[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-dataframe_options[dark_theme-webkit].png
new file mode 100644
index 000000000000..d9147fa55b30
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-dataframe_options[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-dataframe_options[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-dataframe_options[light_theme-chromium].png
new file mode 100644
index 000000000000..298863905fd4
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-dataframe_options[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-dataframe_options[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-dataframe_options[light_theme-firefox].png
new file mode 100644
index 000000000000..a507ffbf4ff2
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-dataframe_options[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-dataframe_options[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-dataframe_options[light_theme-webkit].png
new file mode 100644
index 000000000000..3574ef85634b
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-dataframe_options[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-default[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-default[dark_theme-chromium].png
new file mode 100644
index 000000000000..73d81282faee
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-default[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-default[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-default[dark_theme-firefox].png
new file mode 100644
index 000000000000..2c50f9b75101
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-default[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-default[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-default[dark_theme-webkit].png
new file mode 100644
index 000000000000..f38e1ab6ac8b
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-default[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-default[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-default[light_theme-chromium].png
new file mode 100644
index 000000000000..023fb60c4733
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-default[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-default[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-default[light_theme-firefox].png
new file mode 100644
index 000000000000..a712046994f7
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-default[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-default[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-default[light_theme-webkit].png
new file mode 100644
index 000000000000..80a7b9509d77
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-default[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-disabled[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-disabled[dark_theme-chromium].png
new file mode 100644
index 000000000000..2139ced384d2
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-disabled[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-disabled[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-disabled[dark_theme-firefox].png
new file mode 100644
index 000000000000..d4ea7bb0b1d6
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-disabled[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-disabled[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-disabled[dark_theme-webkit].png
new file mode 100644
index 000000000000..fbc94e6fd157
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-disabled[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-disabled[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-disabled[light_theme-chromium].png
new file mode 100644
index 000000000000..459ae1b7ac34
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-disabled[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-disabled[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-disabled[light_theme-firefox].png
new file mode 100644
index 000000000000..afc31459b090
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-disabled[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-disabled[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-disabled[light_theme-webkit].png
new file mode 100644
index 000000000000..35b7929049fb
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-disabled[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection[dark_theme-chromium].png
new file mode 100644
index 000000000000..54623bcdcc38
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection[dark_theme-firefox].png
new file mode 100644
index 000000000000..6c73f376bb14
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection[dark_theme-webkit].png
new file mode 100644
index 000000000000..85af98d4decc
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection[light_theme-chromium].png
new file mode 100644
index 000000000000..5947af6d6ec5
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection[light_theme-firefox].png
new file mode 100644
index 000000000000..e393ee492794
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection[light_theme-webkit].png
new file mode 100644
index 000000000000..87c54bb7f76a
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection_placeholder[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection_placeholder[dark_theme-chromium].png
new file mode 100644
index 000000000000..cdf28b861437
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection_placeholder[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection_placeholder[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection_placeholder[dark_theme-firefox].png
new file mode 100644
index 000000000000..51df28714399
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection_placeholder[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection_placeholder[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection_placeholder[dark_theme-webkit].png
new file mode 100644
index 000000000000..b8b2d2e07ec7
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection_placeholder[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection_placeholder[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection_placeholder[light_theme-chromium].png
new file mode 100644
index 000000000000..84ad9af71418
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection_placeholder[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection_placeholder[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection_placeholder[light_theme-firefox].png
new file mode 100644
index 000000000000..c70061a56caa
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection_placeholder[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection_placeholder[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection_placeholder[light_theme-webkit].png
new file mode 100644
index 000000000000..e9436a6b73a8
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-empty_selection_placeholder[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-formatted_options[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-formatted_options[dark_theme-chromium].png
new file mode 100644
index 000000000000..351c7c571f66
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-formatted_options[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-formatted_options[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-formatted_options[dark_theme-firefox].png
new file mode 100644
index 000000000000..99cc29b73a9f
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-formatted_options[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-formatted_options[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-formatted_options[dark_theme-webkit].png
new file mode 100644
index 000000000000..0ebe725ec8b7
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-formatted_options[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-formatted_options[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-formatted_options[light_theme-chromium].png
new file mode 100644
index 000000000000..5799524a894c
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-formatted_options[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-formatted_options[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-formatted_options[light_theme-firefox].png
new file mode 100644
index 000000000000..b1bd67288742
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-formatted_options[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-formatted_options[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-formatted_options[light_theme-webkit].png
new file mode 100644
index 000000000000..28eae5b03384
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-formatted_options[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-fuzzy_matching[chromium].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-fuzzy_matching[chromium].png
new file mode 100644
index 000000000000..bfddbd202dde
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-fuzzy_matching[chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-fuzzy_matching[firefox].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-fuzzy_matching[firefox].png
new file mode 100644
index 000000000000..65a4c06a7447
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-fuzzy_matching[firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-fuzzy_matching[webkit].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-fuzzy_matching[webkit].png
new file mode 100644
index 000000000000..5087db4a8e6c
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-fuzzy_matching[webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-hidden_label[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-hidden_label[dark_theme-chromium].png
new file mode 100644
index 000000000000..250405e70b1c
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-hidden_label[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-hidden_label[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-hidden_label[dark_theme-firefox].png
new file mode 100644
index 000000000000..0998f2935a86
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-hidden_label[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-hidden_label[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-hidden_label[dark_theme-webkit].png
new file mode 100644
index 000000000000..ba781f868f90
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-hidden_label[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-hidden_label[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-hidden_label[light_theme-chromium].png
new file mode 100644
index 000000000000..e8d8634ba4f5
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-hidden_label[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-hidden_label[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-hidden_label[light_theme-firefox].png
new file mode 100644
index 000000000000..f7fa85ec1f44
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-hidden_label[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-hidden_label[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-hidden_label[light_theme-webkit].png
new file mode 100644
index 000000000000..c5b5a5b95b35
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-hidden_label[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-more_options[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-more_options[dark_theme-chromium].png
new file mode 100644
index 000000000000..70b3672f79c8
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-more_options[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-more_options[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-more_options[dark_theme-firefox].png
new file mode 100644
index 000000000000..97b644773a90
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-more_options[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-more_options[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-more_options[dark_theme-webkit].png
new file mode 100644
index 000000000000..260f4499ec19
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-more_options[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-more_options[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-more_options[light_theme-chromium].png
new file mode 100644
index 000000000000..885e541ec319
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-more_options[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-more_options[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-more_options[light_theme-firefox].png
new file mode 100644
index 000000000000..f54ed601f620
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-more_options[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-more_options[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-more_options[light_theme-webkit].png
new file mode 100644
index 000000000000..b2f2d68e6863
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-more_options[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-no_options[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-no_options[dark_theme-chromium].png
new file mode 100644
index 000000000000..a261833072bb
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-no_options[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-no_options[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-no_options[dark_theme-firefox].png
new file mode 100644
index 000000000000..8f215912549f
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-no_options[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-no_options[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-no_options[dark_theme-webkit].png
new file mode 100644
index 000000000000..a96c59de252c
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-no_options[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-no_options[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-no_options[light_theme-chromium].png
new file mode 100644
index 000000000000..8925c48d8ccd
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-no_options[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-no_options[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-no_options[light_theme-firefox].png
new file mode 100644
index 000000000000..2f67215a8f81
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-no_options[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-no_options[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-no_options[light_theme-webkit].png
new file mode 100644
index 000000000000..78a9271c3c8a
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-no_options[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-selection_dropdown[chromium].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-selection_dropdown[chromium].png
new file mode 100644
index 000000000000..922fd62603ca
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-selection_dropdown[chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-selection_dropdown[firefox].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-selection_dropdown[firefox].png
new file mode 100644
index 000000000000..9cc657d6fd9b
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-selection_dropdown[firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-selection_dropdown[webkit].png b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-selection_dropdown[webkit].png
new file mode 100644
index 000000000000..784bc8b75ad1
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_selectbox_test/st_selectbox-selection_dropdown[webkit].png differ
diff --git a/e2e_playwright/st_selectbox.py b/e2e_playwright/st_selectbox.py
new file mode 100644
index 000000000000..8e453072946f
--- /dev/null
+++ b/e2e_playwright/st_selectbox.py
@@ -0,0 +1,92 @@
+# 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 = ("male", "female")
+i1 = st.selectbox("selectbox 1 (default)", options)
+st.write("value 1:", i1)
+
+i2 = st.selectbox(
+    "selectbox 2 (formatted options)", options, 1, format_func=lambda x: x.capitalize()
+)
+st.write("value 2:", i2)
+
+i3: None = st.selectbox("selectbox 3 (no options)", [])
+st.write("value 3:", i3)
+
+more_options = [
+    "e2e/scripts/components_iframe.py",
+    "e2e/scripts/st_warning.py",
+    "This is a very very very long option label that should be truncated when it is showing in the dropdown menu.",
+    "e2e/scripts/st_container.py",
+    "e2e/scripts/st_dataframe_sort_column.py",
+    "e2e/scripts/app_hotkeys.py",
+    "e2e/scripts/st_info.py",
+    "e2e/scripts/st_echo.py",
+    "e2e/scripts/st_json.py",
+    "e2e/scripts/st_experimental_get_query_params.py",
+    "e2e/scripts/st_markdown.py",
+    "e2e/scripts/st_color_picker.py",
+    "e2e/scripts/st_expander.py",
+]
+i4 = st.selectbox("selectbox 4 (more options)", more_options, 0)
+st.write("value 4:", i4)
+
+i5 = st.selectbox("selectbox 5 (disabled)", options, disabled=True)
+st.write("value 5:", i5)
+
+i6 = st.selectbox("selectbox 6 (hidden label)", options, label_visibility="hidden")
+st.write("value 6:", i6)
+
+i7 = st.selectbox(
+    "selectbox 7 (collapsed label)", options, label_visibility="collapsed"
+)
+st.write("value 7:", i7)
+
+if runtime.exists():
+
+    def on_change():
+        st.session_state.selectbox_changed = True
+        st.text("Selectbox widget callback triggered")
+
+    st.selectbox(
+        "selectbox 8 (with callback, help)",
+        options,
+        1,
+        key="selectbox8",
+        on_change=on_change,
+        help="Help text",
+    )
+    st.write("value 8:", st.session_state.selectbox8)
+    st.write("selectbox changed:", "selectbox_changed" in st.session_state)
+
+i9 = st.selectbox("selectbox 9 (empty selection)", options, index=None)
+st.write("value 9:", i9)
+
+i10 = st.selectbox(
+    "selectbox 10 (empty, custom placeholder)",
+    options,
+    index=None,
+    placeholder="Select one of the options...",
+)
+st.write("value 10:", i10)
+
+i11 = st.selectbox(
+    "selectbox 11 (options from dataframe)", pd.DataFrame({"foo": list(options)})
+)
+st.write("value 11:", i11)
diff --git a/e2e_playwright/st_selectbox_test.py b/e2e_playwright/st_selectbox_test.py
new file mode 100644
index 000000000000..3abeb001282f
--- /dev/null
+++ b/e2e_playwright/st_selectbox_test.py
@@ -0,0 +1,190 @@
+# 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_selectbox_widget_rendering(
+    themed_app: Page, assert_snapshot: ImageCompareFunction
+):
+    """Test that the selectbox widgets are correctly rendered via screenshot matching."""
+    selectbox_widgets = themed_app.get_by_test_id("stSelectbox")
+    expect(selectbox_widgets).to_have_count(11)
+
+    assert_snapshot(selectbox_widgets.nth(0), name="st_selectbox-default")
+    assert_snapshot(selectbox_widgets.nth(1), name="st_selectbox-formatted_options")
+    assert_snapshot(selectbox_widgets.nth(2), name="st_selectbox-no_options")
+    assert_snapshot(selectbox_widgets.nth(3), name="st_selectbox-more_options")
+    assert_snapshot(selectbox_widgets.nth(4), name="st_selectbox-disabled")
+    assert_snapshot(selectbox_widgets.nth(5), name="st_selectbox-hidden_label")
+    assert_snapshot(selectbox_widgets.nth(6), name="st_selectbox-collapsed_label")
+    assert_snapshot(selectbox_widgets.nth(7), name="st_selectbox-callback_help")
+    assert_snapshot(selectbox_widgets.nth(8), name="st_selectbox-empty_selection")
+    assert_snapshot(
+        selectbox_widgets.nth(9), name="st_selectbox-empty_selection_placeholder"
+    )
+    assert_snapshot(selectbox_widgets.nth(10), name="st_selectbox-dataframe_options")
+
+
+def test_selectbox_has_correct_initial_values(app: Page):
+    """Test that st.selectbox returns the correct initial values."""
+    markdown_elements = app.get_by_test_id("stMarkdown")
+    expect(markdown_elements).to_have_count(12)
+
+    expected = [
+        "value 1: male",
+        "value 2: female",
+        "value 3: None",
+        "value 4: e2e/scripts/components_iframe.py",
+        "value 5: male",
+        "value 6: male",
+        "value 7: male",
+        "value 8: female",
+        "selectbox changed: False",
+        "value 9: None",
+        "value 10: None",
+        "value 11: male",
+    ]
+
+    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_handles_option_selection(app: Page, assert_snapshot: ImageCompareFunction):
+    """Test that selection of an option via the dropdown works correctly."""
+    app.get_by_test_id("stSelectbox").nth(3).locator("input").click()
+
+    # Take a snapshot of the selection dropdown:
+    selection_dropdown = app.locator('[data-baseweb="popover"]').first
+    assert_snapshot(selection_dropdown, name="st_selectbox-selection_dropdown")
+    # Select last option:
+    selection_dropdown.locator("li").nth(1).click()
+    # Check that selection worked:
+    expect(app.get_by_test_id("stMarkdown").nth(3)).to_have_text(
+        "value 4: e2e/scripts/st_warning.py", use_inner_text=True
+    )
+
+
+def test_handles_option_selection_via_typing(app: Page):
+    """Test that selection of an option via typing works correctly."""
+    selectbox_input = app.get_by_test_id("stSelectbox").nth(3).locator("input")
+
+    # Type an option:
+    selectbox_input.type("e2e/scripts/st_warning.py")
+    selectbox_input.press("Enter")
+
+    # Check that selection worked:
+    expect(app.get_by_test_id("stMarkdown").nth(3)).to_have_text(
+        "value 4: e2e/scripts/st_warning.py", use_inner_text=True
+    )
+
+
+def test_shows_correct_options_via_fuzzy_search(
+    app: Page, assert_snapshot: ImageCompareFunction
+):
+    """Test that the fuzzy matching of options via typing works correctly."""
+    selectbox_input = app.get_by_test_id("stSelectbox").nth(3).locator("input")
+
+    # Start typing:
+    selectbox_input.type("exp")
+
+    # Check filtered options
+    selection_dropdown = app.locator('[data-baseweb="popover"]').first
+    assert_snapshot(selection_dropdown, name="st_selectbox-fuzzy_matching")
+
+
+def test_empty_selectbox_behaves_correctly(
+    app: Page, assert_snapshot: ImageCompareFunction
+):
+    """Test that st.selectbox behaves correctly when empty (no initial selection)."""
+    # Enter 10 in the first empty input:
+    empty_selectbox_input = app.get_by_test_id("stSelectbox").locator("input").nth(8)
+
+    # Type an option:
+    empty_selectbox_input.type("male")
+    empty_selectbox_input.press("Enter")
+
+    expect(app.get_by_test_id("stMarkdown").nth(9)).to_have_text(
+        "value 9: male", use_inner_text=True
+    )
+
+    assert_snapshot(
+        app.get_by_test_id("stSelectbox").nth(8), name="st_selectbox-clearable_input"
+    )
+
+    empty_selectbox_input.focus()
+    empty_selectbox_input.press("Escape")
+
+    # Should be empty again:
+    expect(app.get_by_test_id("stMarkdown").nth(9)).to_have_text(
+        "value 9: None", use_inner_text=True
+    )
+
+
+def test_keeps_value_on_selection_close(app: Page):
+    """Test that the fuzzy matching of options via typing works correctly."""
+    app.get_by_test_id("stSelectbox").nth(3).locator("input").click()
+
+    # Take a snapshot of the selection dropdown:
+    expect(app.locator('[data-baseweb="popover"]').first).to_be_visible()
+
+    # Click outside to close the dropdown:
+    app.get_by_test_id("stMarkdown").first.click()
+
+    # Check if value is still initial value:
+    expect(app.get_by_test_id("stMarkdown").nth(3)).to_have_text(
+        "value 4: e2e/scripts/components_iframe.py", use_inner_text=True
+    )
+
+
+def test_handles_callback_on_change_correctly(app: Page):
+    """Test that it correctly calls the callback on change."""
+    # Check initial state:
+    expect(app.get_by_test_id("stMarkdown").nth(7)).to_have_text(
+        "value 8: female", use_inner_text=True
+    )
+    expect(app.get_by_test_id("stMarkdown").nth(8)).to_have_text(
+        "selectbox changed: False", use_inner_text=True
+    )
+
+    app.get_by_test_id("stSelectbox").nth(7).locator("input").click()
+
+    # Take a snapshot of the selection dropdown:
+    selection_dropdown = app.locator('[data-baseweb="popover"]').first
+    # Select last option:
+    selection_dropdown.locator("li").first.click()
+
+    # Check that selection worked:
+    expect(app.get_by_test_id("stMarkdown").nth(7)).to_have_text(
+        "value 8: male", use_inner_text=True
+    )
+    expect(app.get_by_test_id("stMarkdown").nth(8)).to_have_text(
+        "selectbox changed: True", use_inner_text=True
+    )
+
+    # Change different date input to trigger delta path change
+    empty_selectbox_input = app.get_by_test_id("stSelectbox").locator("input").first
+
+    # Type an option:
+    empty_selectbox_input.type("female")
+    empty_selectbox_input.press("Enter")
+
+    expect(app.get_by_test_id("stMarkdown").first).to_have_text(
+        "value 1: female", use_inner_text=True
+    )
+    expect(app.get_by_test_id("stMarkdown").nth(7)).to_have_text(
+        "value 8: male", use_inner_text=True
+    )
diff --git a/frontend/app/src/components/StreamlitDialog/SettingsDialog.tsx b/frontend/app/src/components/StreamlitDialog/SettingsDialog.tsx
index 0413545e3425..5da266e1cc52 100644
--- a/frontend/app/src/components/StreamlitDialog/SettingsDialog.tsx
+++ b/frontend/app/src/components/StreamlitDialog/SettingsDialog.tsx
@@ -174,10 +174,10 @@ export class SettingsDialog extends PureComponent<Props, UserSettings> {
     this.changeSingleSetting(e.target.name, e.target.checked)
   }
 
-  private handleThemeChange = (index: number): void => {
+  private handleThemeChange = (index: number | null): void => {
     const { activeTheme: oldTheme, availableThemes }: LibContextProps =
       this.context
-    const newTheme = availableThemes[index]
+    const newTheme = availableThemes[index ?? 0]
 
     this.props.metricsMgr.enqueue("themeChanged", {
       oldThemeName: oldTheme.name,
diff --git a/frontend/lib/src/components/elements/Markdown/Markdown.tsx b/frontend/lib/src/components/elements/Markdown/Markdown.tsx
index c0a932c4f327..00bab744812e 100644
--- a/frontend/lib/src/components/elements/Markdown/Markdown.tsx
+++ b/frontend/lib/src/components/elements/Markdown/Markdown.tsx
@@ -37,7 +37,7 @@ export default function Markdown({
 }: MarkdownProps): ReactElement {
   const styleProp = { width }
   return (
-    <div className="stMarkdown" style={styleProp}>
+    <div className="stMarkdown" style={styleProp} data-testid="stMarkdown">
       {element.help ? (
         <StyledLabelHelpWrapper>
           <StreamlitMarkdown
diff --git a/frontend/lib/src/components/shared/Dropdown/Selectbox.test.tsx b/frontend/lib/src/components/shared/Dropdown/Selectbox.test.tsx
index a93a1aff55f8..ab8f8238101f 100644
--- a/frontend/lib/src/components/shared/Dropdown/Selectbox.test.tsx
+++ b/frontend/lib/src/components/shared/Dropdown/Selectbox.test.tsx
@@ -20,7 +20,8 @@ import { shallow, mount } from "@streamlit/lib/src/test_util"
 
 import { Select as UISelect } from "baseui/select"
 import { LabelVisibilityOptions } from "@streamlit/lib/src/util/utils"
-import Selectbox, { Props, fuzzyFilterSelectOptions } from "./Selectbox"
+import { Selectbox, Props, fuzzyFilterSelectOptions } from "./Selectbox"
+import { mockTheme } from "@streamlit/lib/src/mocks/mockTheme"
 
 jest.mock("@streamlit/lib/src/WidgetStateManager")
 
@@ -31,6 +32,7 @@ const getProps = (props: Partial<Props> = {}): Props => ({
   width: 0,
   disabled: false,
   onChange: jest.fn(),
+  theme: mockTheme.emotion,
   placeholder: "Select...",
   ...props,
 })
diff --git a/frontend/lib/src/components/shared/Dropdown/Selectbox.tsx b/frontend/lib/src/components/shared/Dropdown/Selectbox.tsx
index a7052d03d7e3..e3324c1ee034 100644
--- a/frontend/lib/src/components/shared/Dropdown/Selectbox.tsx
+++ b/frontend/lib/src/components/shared/Dropdown/Selectbox.tsx
@@ -18,11 +18,15 @@ import React from "react"
 import { isMobile } from "react-device-detect"
 import { ChevronDown } from "baseui/icon"
 import { Select as UISelect, OnChangeParams, Option } from "baseui/select"
-import { logWarning } from "@streamlit/lib/src/util/log"
-import VirtualDropdown from "@streamlit/lib/src/components/shared/Dropdown/VirtualDropdown"
+import { withTheme } from "@emotion/react"
 import { hasMatch, score } from "fzy.js"
 import _ from "lodash"
-import { LabelVisibilityOptions } from "@streamlit/lib/src/util/utils"
+
+import VirtualDropdown from "@streamlit/lib/src/components/shared/Dropdown/VirtualDropdown"
+import {
+  LabelVisibilityOptions,
+  isNullOrUndefined,
+} from "@streamlit/lib/src/util/utils"
 import { Placement } from "@streamlit/lib/src/components/shared/Tooltip"
 import TooltipIcon from "@streamlit/lib/src/components/shared/TooltipIcon"
 import {
@@ -30,17 +34,22 @@ import {
   StyledWidgetLabelHelp,
 } from "@streamlit/lib/src/components/widgets/BaseWidget"
 import { iconSizes } from "@streamlit/lib/src/theme/primitives"
+import { EmotionTheme } from "@streamlit/lib/src/theme"
+
+const NO_OPTIONS_MSG = "No options to select."
 
 export interface Props {
   disabled: boolean
   width?: number
-  value: number
-  onChange: (value: number) => void
+  value: number | null
+  onChange: (value: number | null) => void
   options: any[]
   label?: string | null
   labelVisibility?: LabelVisibilityOptions
   help?: string
   placeholder?: string
+  clearable?: boolean
+  theme: EmotionTheme
 }
 
 interface State {
@@ -50,7 +59,7 @@ interface State {
    * The value specified by the user via the UI. If the user didn't touch this
    * widget's UI, the default value is used.
    */
-  value: number
+  value: number | null
 }
 
 interface SelectOption {
@@ -78,7 +87,7 @@ export function fuzzyFilterSelectOptions(
     .value()
 }
 
-class Selectbox extends React.PureComponent<Props, State> {
+export class Selectbox extends React.PureComponent<Props, State> {
   public state: State = {
     isEmpty: false,
     value: this.props.value,
@@ -97,7 +106,7 @@ class Selectbox extends React.PureComponent<Props, State> {
 
   private onChange = (params: OnChangeParams): void => {
     if (params.value.length === 0) {
-      logWarning("No value selected!")
+      this.setState({ value: null }, () => this.props.onChange(null))
       return
     }
 
@@ -138,25 +147,24 @@ class Selectbox extends React.PureComponent<Props, State> {
 
   public render(): React.ReactNode {
     const style = { width: this.props.width }
-    const { label, labelVisibility, help, placeholder } = this.props
+    const { label, labelVisibility, help, placeholder, theme, clearable } =
+      this.props
     let { disabled, options } = this.props
 
-    let value = [
-      {
-        label:
-          options.length > 0
-            ? options[this.state.value]
-            : "No options to select.",
-        value: this.state.value.toString(),
-      },
-    ]
+    let value: Option[] = []
 
-    if (this.state.isEmpty) {
-      value = []
+    if (!isNullOrUndefined(this.state.value) && !this.state.isEmpty) {
+      value = [
+        {
+          label:
+            options.length > 0 ? options[this.state.value] : NO_OPTIONS_MSG,
+          value: this.state.value.toString(),
+        },
+      ]
     }
 
     if (options.length === 0) {
-      options = ["No options to select."]
+      options = [NO_OPTIONS_MSG]
       disabled = true
     }
 
@@ -172,7 +180,11 @@ class Selectbox extends React.PureComponent<Props, State> {
     const showKeyboardOnMobile = options.length > 10
 
     return (
-      <div className="row-widget stSelectbox" style={style}>
+      <div
+        className="row-widget stSelectbox"
+        data-testid="stSelectbox"
+        style={style}
+      >
         <WidgetLabel
           label={label}
           labelVisibility={labelVisibility}
@@ -185,7 +197,6 @@ class Selectbox extends React.PureComponent<Props, State> {
           )}
         </WidgetLabel>
         <UISelect
-          clearable={false}
           disabled={disabled}
           labelKey="label"
           aria-label={label || ""}
@@ -194,6 +205,8 @@ class Selectbox extends React.PureComponent<Props, State> {
           onClose={this.onClose}
           options={selectOptions}
           filterOptions={this.filterOptions}
+          clearable={clearable || false}
+          escapeClearsValue={clearable || false}
           value={value}
           valueKey="value"
           placeholder={placeholder}
@@ -204,7 +217,26 @@ class Selectbox extends React.PureComponent<Props, State> {
               }),
             },
             Dropdown: { component: VirtualDropdown },
-
+            ClearIcon: {
+              props: {
+                overrides: {
+                  Svg: {
+                    style: {
+                      color: theme.colors.darkGray,
+                      // Since the close icon is an SVG, and we can't control its viewbox nor its attributes,
+                      // Let's use a scale transform effect to make it bigger.
+                      // The width property only enlarges its bounding box, so it's easier to click.
+                      marginRight: "3px",
+                      transform: "scale(1.25)",
+                      width: theme.spacing.twoXL,
+                      ":hover": {
+                        fill: theme.colors.bodyText,
+                      },
+                    },
+                  },
+                },
+              },
+            },
             ControlContainer: {
               style: () => ({
                 // Baseweb requires long-hand props, short-hand leads to weird bugs & warnings.
@@ -278,4 +310,4 @@ class Selectbox extends React.PureComponent<Props, State> {
   }
 }
 
-export default Selectbox
+export default withTheme(Selectbox)
diff --git a/frontend/lib/src/components/widgets/Selectbox/Selectbox.test.tsx b/frontend/lib/src/components/widgets/Selectbox/Selectbox.test.tsx
index 2b2e6c256f5e..e596f60386ce 100644
--- a/frontend/lib/src/components/widgets/Selectbox/Selectbox.test.tsx
+++ b/frontend/lib/src/components/widgets/Selectbox/Selectbox.test.tsx
@@ -20,7 +20,8 @@ import { WidgetStateManager } from "@streamlit/lib/src/WidgetStateManager"
 
 import { Select as UISelect } from "baseui/select"
 import { Selectbox as SelectboxProto } from "@streamlit/lib/src/proto"
-import Selectbox, { Props } from "./Selectbox"
+import { Selectbox, Props } from "./Selectbox"
+import { mockTheme } from "@streamlit/lib/src/mocks/mockTheme"
 
 const getProps = (elementProps: Partial<SelectboxProto> = {}): Props => ({
   element: SelectboxProto.create({
@@ -32,6 +33,7 @@ const getProps = (elementProps: Partial<SelectboxProto> = {}): Props => ({
   }),
   width: 0,
   disabled: false,
+  theme: mockTheme.emotion,
   widgetMgr: new WidgetStateManager({
     sendRerunBackMsg: jest.fn(),
     formsDataChanged: jest.fn(),
diff --git a/frontend/lib/src/components/widgets/Selectbox/Selectbox.tsx b/frontend/lib/src/components/widgets/Selectbox/Selectbox.tsx
index d82f80d9ca35..d21561f1e717 100644
--- a/frontend/lib/src/components/widgets/Selectbox/Selectbox.tsx
+++ b/frontend/lib/src/components/widgets/Selectbox/Selectbox.tsx
@@ -15,6 +15,8 @@
  */
 
 import React from "react"
+
+import { withTheme } from "@emotion/react"
 import { Selectbox as SelectboxProto } from "@streamlit/lib/src/proto"
 import { FormClearHelper } from "@streamlit/lib/src/components/widgets/Form"
 import {
@@ -22,13 +24,18 @@ import {
   Source,
 } from "@streamlit/lib/src/WidgetStateManager"
 import UISelectbox from "@streamlit/lib/src/components/shared/Dropdown"
-import { labelVisibilityProtoValueToEnum } from "@streamlit/lib/src/util/utils"
+import {
+  labelVisibilityProtoValueToEnum,
+  isNullOrUndefined,
+} from "@streamlit/lib/src/util/utils"
+import { EmotionTheme } from "@streamlit/lib/src/theme"
 
 export interface Props {
   disabled: boolean
   element: SelectboxProto
   widgetMgr: WidgetStateManager
   width: number
+  theme: EmotionTheme
 }
 
 interface State {
@@ -36,21 +43,21 @@ interface State {
    * The value specified by the user via the UI. If the user didn't touch this
    * widget's UI, the default value is used.
    */
-  value: number
+  value: number | null
 }
 
-class Selectbox extends React.PureComponent<Props, State> {
+export class Selectbox extends React.PureComponent<Props, State> {
   private readonly formClearHelper = new FormClearHelper()
 
   public state: State = {
     value: this.initialValue,
   }
 
-  get initialValue(): number {
+  get initialValue(): number | null {
     // If WidgetStateManager knew a value for this widget, initialize to that.
     // Otherwise, use the default value from the widget protobuf.
     const storedValue = this.props.widgetMgr.getIntValue(this.props.element)
-    return storedValue !== undefined ? storedValue : this.props.element.default
+    return storedValue ?? this.props.element.default ?? null
   }
 
   public componentDidMount(): void {
@@ -79,7 +86,7 @@ class Selectbox extends React.PureComponent<Props, State> {
   private updateFromProtobuf(): void {
     const { value } = this.props.element
     this.props.element.setValue = false
-    this.setState({ value }, () => {
+    this.setState({ value: value ?? null }, () => {
       this.commitWidgetValue({ fromUi: false })
     })
   }
@@ -100,13 +107,13 @@ class Selectbox extends React.PureComponent<Props, State> {
   private onFormCleared = (): void => {
     this.setState(
       (_, prevProps) => {
-        return { value: prevProps.element.default }
+        return { value: prevProps.element.default ?? null }
       },
       () => this.commitWidgetValue({ fromUi: true })
     )
   }
 
-  private onChange = (value: number): void => {
+  private onChange = (value: number | null): void => {
     this.setState({ value }, () => this.commitWidgetValue({ fromUi: true }))
   }
 
@@ -114,6 +121,8 @@ class Selectbox extends React.PureComponent<Props, State> {
     const { options, help, label, labelVisibility, formId, placeholder } =
       this.props.element
     const { disabled, widgetMgr } = this.props
+    const clearable =
+      isNullOrUndefined(this.props.element.default) && !disabled
 
     // Manage our form-clear event handler.
     this.formClearHelper.manageFormClearListener(
@@ -135,9 +144,10 @@ class Selectbox extends React.PureComponent<Props, State> {
         value={this.state.value}
         help={help}
         placeholder={placeholder}
+        clearable={clearable}
       />
     )
   }
 }
 
-export default Selectbox
+export default withTheme(Selectbox)
diff --git a/lib/streamlit/elements/widgets/selectbox.py b/lib/streamlit/elements/widgets/selectbox.py
index f39c88b316ec..4f5c44877269 100644
--- a/lib/streamlit/elements/widgets/selectbox.py
+++ b/lib/streamlit/elements/widgets/selectbox.py
@@ -11,10 +11,11 @@
 # 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 __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, overload
 
 from streamlit.elements.form import current_form_id
 from streamlit.elements.utils import (
@@ -51,41 +52,80 @@
 @dataclass
 class SelectboxSerde(Generic[T]):
     options: Sequence[T]
-    index: int
+    index: int | None
 
-    def serialize(self, v: object) -> int:
+    def serialize(self, v: object) -> int | None:
+        if v is None:
+            return None
         if len(self.options) == 0:
             return 0
         return index_(self.options, v)
 
     def deserialize(
         self,
-        ui_value: Optional[int],
+        ui_value: int | None,
         widget_id: str = "",
-    ) -> Optional[T]:
-        idx: int = ui_value if ui_value is not None else self.index
-
-        return self.options[idx] if len(self.options) > 0 else None
+    ) -> T | None:
+        idx = ui_value if ui_value is not None else self.index
+        return self.options[idx] if idx is not None and len(self.options) > 0 else None
 
 
 class SelectboxMixin:
-    @gather_metrics("selectbox")
+    @overload
     def selectbox(
         self,
         label: str,
         options: OptionSequence[T],
         index: int = 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 arguments:
+        placeholder: str = "Select...",
+        disabled: bool = False,
+        label_visibility: LabelVisibility = "visible",
+    ) -> T:
+        pass
+
+    @overload
+    def selectbox(
+        self,
+        label: str,
+        options: OptionSequence[T],
+        index: None = None,
+        format_func: Callable[[Any], Any] = str,
+        key: Key | None = None,
+        help: str | None = None,
+        on_change: WidgetCallback | None = None,
+        args: WidgetArgs | None = None,
+        kwargs: WidgetKwargs | None = None,
         *,  # keyword-only arguments:
         placeholder: str = "Select...",
         disabled: bool = False,
         label_visibility: LabelVisibility = "visible",
-    ) -> Optional[T]:
+    ) -> T | None:
+        pass
+
+    @gather_metrics("selectbox")
+    def selectbox(
+        self,
+        label: str,
+        options: OptionSequence[T],
+        index: int | None = 0,
+        format_func: Callable[[Any], Any] = str,
+        key: Key | None = None,
+        help: str | None = None,
+        on_change: WidgetCallback | None = None,
+        args: WidgetArgs | None = None,
+        kwargs: WidgetKwargs | None = None,
+        *,  # keyword-only arguments:
+        placeholder: str = "Select...",
+        disabled: bool = False,
+        label_visibility: LabelVisibility = "visible",
+    ) -> T | None:
         r"""Display a select widget.
 
         Parameters
@@ -120,7 +160,9 @@ def selectbox(
             Labels for the select options. This 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.
+            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 the labels. It receives the option
             as an argument and its output will be cast to str.
@@ -139,10 +181,6 @@ def selectbox(
             An optional dict of kwargs to pass to the callback.
         placeholder : str
             A string to display when no options are selected. Defaults to 'Select...'.
-
-            A selectbox can't be empty, so a placeholder only displays while a
-            user's cursor is in a selectbox after manually deleting the current
-            selection. A future update will allow selectboxes to be empty.
         disabled : bool
             An optional boolean, which disables the selectbox if set to True.
             The default is False. This argument can only be supplied by keyword.
@@ -155,7 +193,7 @@ def selectbox(
         Returns
         -------
         any
-            The selected option
+            The selected option or ``None`` if no option is selected.
 
         Example
         -------
@@ -171,6 +209,23 @@ def selectbox(
            https://doc-selectbox.streamlit.app/
            height: 320px
 
+        To initialize an empty selectbox, use ``None`` as the index value:
+
+        >>> import streamlit as st
+        >>>
+        >>> option = st.selectbox(
+        ...    "How would you like to be contacted?",
+        ...    ("Email", "Home phone", "Mobile phone"),
+        ...    index=None,
+        ...    placeholder="Select contact method...",
+        ... )
+        >>>
+        >>> st.write('You selected:', option)
+
+        .. output::
+           https://doc-selectbox-empty.streamlit.app/
+           height: 320px
+
         """
         ctx = get_script_run_ctx()
         return self._selectbox(
@@ -193,19 +248,19 @@ def _selectbox(
         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 arguments:
         placeholder: str = "Select...",
         disabled: bool = False,
         label_visibility: LabelVisibility = "visible",
-        ctx: Optional[ScriptRunContext] = None,
-    ) -> Optional[T]:
+        ctx: ScriptRunContext | None = 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)
@@ -227,12 +282,12 @@ def _selectbox(
             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(
                 "Selectbox 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(
                 "Selectbox index must be between 0 and length of options"
             )
@@ -240,7 +295,8 @@ def _selectbox(
         selectbox_proto = SelectboxProto()
         selectbox_proto.id = id
         selectbox_proto.label = label
-        selectbox_proto.default = index
+        if index is not None:
+            selectbox_proto.default = index
         selectbox_proto.options[:] = [str(format_func(option)) for option in opt]
         selectbox_proto.form_id = current_form_id(self.dg)
         selectbox_proto.placeholder = placeholder
@@ -267,7 +323,9 @@ def _selectbox(
         )
 
         if widget_state.value_changed:
-            selectbox_proto.value = serde.serialize(widget_state.value)
+            serialized_value = serde.serialize(widget_state.value)
+            if serialized_value is not None:
+                selectbox_proto.value = serialized_value
             selectbox_proto.set_value = True
 
         self.dg._enqueue("selectbox", selectbox_proto)
diff --git a/lib/streamlit/testing/element_tree.py b/lib/streamlit/testing/element_tree.py
index 7e5787435524..d9f3b594daea 100644
--- a/lib/streamlit/testing/element_tree.py
+++ b/lib/streamlit/testing/element_tree.py
@@ -646,7 +646,7 @@ def widget_state(self) -> WidgetState:
 
 @dataclass(repr=False)
 class Selectbox(Widget, Generic[T]):
-    _value: T | None
+    _value: T | None | InitialValue
 
     proto: SelectboxProto = field(repr=False)
     options: list[str]
@@ -654,7 +654,7 @@ class Selectbox(Widget, Generic[T]):
     def __init__(self, proto: SelectboxProto, root: ElementTree):
         self.proto = proto
         self.root = root
-        self._value = None
+        self._value = InitialValue()
 
         self.type = "selectbox"
         self.id = proto.id
@@ -666,22 +666,25 @@ def __init__(self, proto: SelectboxProto, 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
+
         if len(self.options) == 0:
             return 0
         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) -> Selectbox[T]:
+    def set_value(self, v: T | None) -> Selectbox[T]:
         """
         Set the value of the selectbox.
         Implementation note: set_value not work correctly if `format_func` is also
@@ -691,10 +694,12 @@ def set_value(self, v: T) -> Selectbox[T]:
         self._value = v
         return self
 
-    def select(self, v: T) -> Selectbox[T]:
+    def select(self, v: T | None) -> Selectbox[T]:
         return self.set_value(v)
 
-    def select_index(self, index: int) -> Selectbox[T]:
+    def select_index(self, index: int | None) -> Selectbox[T]:
+        if index is None:
+            return self.set_value(None)
         return self.set_value(cast(T, self.options[index]))
 
     def widget_state(self) -> WidgetState:
@@ -704,7 +709,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/selectbox_test.py b/lib/tests/streamlit/elements/selectbox_test.py
index 5fce6aede9fe..1da07ee11fc1 100644
--- a/lib/tests/streamlit/elements/selectbox_test.py
+++ b/lib/tests/streamlit/elements/selectbox_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
 
 
@@ -40,6 +41,7 @@ def test_just_label(self):
             LabelVisibilityMessage.LabelVisibilityOptions.VISIBLE,
         )
         self.assertEqual(c.default, 0)
+        self.assertEqual(c.HasField("default"), True)
         self.assertEqual(c.disabled, False)
         self.assertEqual(c.placeholder, "Select...")
 
@@ -58,6 +60,17 @@ def test_valid_value(self):
         self.assertEqual(c.label, "the label")
         self.assertEqual(c.default, 1)
 
+    def test_none_index(self):
+        """Test that it can be called with None as index value."""
+        st.selectbox("the label", ("m", "f"), index=None)
+
+        c = self.get_delta_from_queue().new_element.selectbox
+        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_noneType_option(self):
         """Test NoneType option value."""
         current_value = st.selectbox("the label", (None, "selected"), 0)
@@ -188,3 +201,27 @@ def test_placeholder(self):
 
         c = self.get_delta_from_queue().new_element.selectbox
         self.assertEqual(c.placeholder, "Please select")
+
+
+class SelectboxInteractiveTest(InteractiveScriptTests):
+    def test_selectbox_interaction(self):
+        """Test interactions with an empty selectbox widget."""
+        script = self.script_from_string(
+            """
+        import streamlit as st
+        st.selectbox("the label", ("m", "f"), index=None)
+        """
+        )
+        sr = script.run()
+        selectbox = sr.selectbox[0]
+        assert selectbox.value is None
+
+        # Select option m
+        sr2 = selectbox.set_value("m").run()
+        selectbox = sr2.selectbox[0]
+        assert selectbox.value == "m"
+
+        # # Clear the value
+        sr3 = selectbox.set_value(None).run()
+        selectbox = sr3.selectbox[0]
+        assert selectbox.value is None
diff --git a/proto/streamlit/proto/Selectbox.proto b/proto/streamlit/proto/Selectbox.proto
index 0f67cc1c6602..7e469c304875 100644
--- a/proto/streamlit/proto/Selectbox.proto
+++ b/proto/streamlit/proto/Selectbox.proto
@@ -21,11 +21,11 @@ import "streamlit/proto/LabelVisibilityMessage.proto";
 message Selectbox {
   string id = 1;
   string label = 2;
-  int32 default = 3;
+  optional int32 default = 3;
   repeated string options = 4;
   string help = 5;
   string form_id = 6;
-  int32 value = 7;
+  optional int32 value = 7;
   bool set_value = 8;
   bool disabled = 9;
   LabelVisibilityMessage label_visibility = 10;


Code review comments:
diff_hunk: @@ -51,41 +52,80 @@
 @dataclass
 class SelectboxSerde(Generic[T]):
     options: Sequence[T]
-    index: int
+    index: int | None
 
-    def serialize(self, v: object) -> int:
+    def serialize(self, v: object) -> int | None:
+        if v is None:
+            return None
         if len(self.options) == 0:
             return 0
         return index_(self.options, v)
 
     def deserialize(
         self,
-        ui_value: Optional[int],
+        ui_value: int | None,
         widget_id: str = "",
-    ) -> Optional[T]:
-        idx: int = ui_value if ui_value is not None else self.index
-
-        return self.options[idx] if len(self.options) > 0 else None
+    ) -> T | None:
+        idx = ui_value if ui_value is not None else self.index
+        return self.options[idx] if idx is not None and len(self.options) > 0 else None
 
 
 class SelectboxMixin:
-    @gather_metrics("selectbox")
+    @overload
     def selectbox(
         self,
         label: str,
         options: OptionSequence[T],
         index: int = 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 arguments:
+        placeholder: str = "Select...",
+        disabled: bool = False,
+        label_visibility: LabelVisibility = "visible",
+    ) -> T:
+        pass
+
+    @overload
+    def selectbox(
+        self,
+        label: str,
+        options: OptionSequence[T],
+        index: None = None,
+        format_func: Callable[[Any], Any] = str,
+        key: Key | None = None,
+        help: str | None = None,
+        on_change: WidgetCallback | None = None,
+        args: WidgetArgs | None = None,
+        kwargs: WidgetKwargs | None = None,
         *,  # keyword-only arguments:
         placeholder: str = "Select...",
         disabled: bool = False,
         label_visibility: LabelVisibility = "visible",
-    ) -> Optional[T]:
+    ) -> T | None:
+        pass
path: lib/streamlit/elements/widgets/selectbox.py
text: @user1:
I think my comments on #7269 should also apply here:
* https://github.com/streamlit/streamlit/pull/7269#discussion_r1316611057
* https://github.com/streamlit/streamlit/pull/7269#discussion_r1316611561

@author:
Removed the overloads for `st.radio` and `st.selectbox` as mentioned in the comment in `st.radio`.