Repository: streamlit/streamlit

Issue:
Invisible tabs
### Summary

Unexpected tab behavior when their creation depends on some condition (e.g.: a checkbox).

### Steps to reproduce

[![Open in Streamlit Cloud](https://static.streamlit.io/badges/streamlit_badge_black_white.svg)](https://issues.streamlitapp.com/?issue=gh-5454)

```
import streamlit as st

checkboxes = {k: st.checkbox(f"Checkbox {k}", value=True) for k in range(3)}

tabs = st.tabs([f"Tab {k}" for k, v in checkboxes.items() if v])

for tab, text in zip(tabs, checkboxes):
    with tab:
        st.write(f"Text {text}")
```

Steps to reproduce:

1. Switch to Tab 2
2. Click on Checkbox 1 (uncheck)

**Expected behavior:**

Move page focus to the left, to keep it on Tab 2.

**Actual behavior:**

Page focus is on some invisible tab on the right (see image below).

### Debug info

- Streamlit version: 1.13.1
- Python version: 3.9.10
- Using Conda
- OS version: Windows 10
- Browser version: Chrome Version 105.0.5195.127 (Official Build) (64-bit)

### Additional information

<img width="280" alt="bug-tab" src="https://user-images.githubusercontent.com/15000098/193208904-6a5033cd-fd6a-4f8f-a0b8-ec732768a8b1.png">




Discussion:
@samuelefiorini Thanks for reporting this issue. I was able to reproduce the issue [here](https://issues.streamlitapp.com/?issue=gh-5454). This definitely requires a fix! 

Resolving patch:
diff --git a/e2e_playwright/st_tabs_selection.py b/e2e_playwright/st_tabs_selection.py
new file mode 100644
--- /dev/null
+++ b/e2e_playwright/st_tabs_selection.py
@@ -0,0 +1,129 @@
+# 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 streamlit as st
+
+st.subheader("Control Panel", divider="blue")
+
+if "tabs" not in st.session_state:
+    st.session_state["tabs"] = ["Tab 1", "Tab 2"]
+if "add_tab" not in st.session_state:
+    st.session_state["add_tab"] = False
+if "remove_1" not in st.session_state:
+    st.session_state["remove_1"] = False
+if "remove_2" not in st.session_state:
+    st.session_state["remove_2"] = False
+if "change" not in st.session_state:
+    st.session_state["change"] = False
+
+
+def on_click_1():
+    st.session_state.add_tab = True
+
+
+def on_click_2():
+    st.session_state.remove_1 = True
+
+
+def on_click_3():
+    st.session_state.remove_2 = True
+
+
+def on_click_4():
+    st.session_state.change = True
+    on_click_1()
+    on_click_2()
+    on_click_3()
+
+
+def on_click_5():
+    on_click_4()
+
+
+def reset():
+    st.session_state.clear()
+
+
+col1, col2, col3, col4, col5 = st.columns([0.8, 1, 1, 1.2, 1], gap="small")
+with col1:
+    add_tab = st.button(
+        "Add Tab 3",
+        on_click=on_click_1,
+        disabled=st.session_state.add_tab,
+        use_container_width=True,
+    )
+with col2:
+    remove_1 = st.button(
+        "Remove Tab 1",
+        on_click=on_click_2,
+        disabled=st.session_state.remove_1,
+        use_container_width=True,
+    )
+with col3:
+    remove_2 = st.button(
+        "Remove Tab 2",
+        on_click=on_click_3,
+        disabled=st.session_state.remove_2,
+        use_container_width=True,
+    )
+with col4:
+    change_some = st.button(
+        "Change Tab 1 & 3",
+        on_click=on_click_4,
+        disabled=st.session_state.change,
+        use_container_width=True,
+    )
+    change = st.button(
+        "Change All Tabs",
+        on_click=on_click_5,
+        disabled=st.session_state.change,
+        use_container_width=True,
+    )
+with col5:
+    st.button("**Reset Tabs**", on_click=reset)
+
+st.subheader("Tabs Example", divider="green")
+
+if add_tab:
+    st.session_state.tabs.append("Tab 3")
+
+if remove_1:
+    index = st.session_state.tabs.index("Tab 1")
+    st.session_state.tabs.pop(index)
+
+if remove_2:
+    index = st.session_state.tabs.index("Tab 2")
+    st.session_state.tabs.pop(index)
+
+if change:
+    if "Tab 1" in st.session_state.tabs:
+        st.session_state.tabs[st.session_state.tabs.index("Tab 1")] = "Tab A"
+    if "Tab 2" in st.session_state.tabs:
+        st.session_state.tabs[st.session_state.tabs.index("Tab 2")] = "Tab B"
+    if "Tab 3" in st.session_state.tabs:
+        st.session_state.tabs[st.session_state.tabs.index("Tab 3")] = "Tab C"
+
+if change_some:
+    if "Tab 1" in st.session_state.tabs:
+        st.session_state.tabs[st.session_state.tabs.index("Tab 1")] = "Tab A"
+    if "Tab 3" in st.session_state.tabs:
+        st.session_state.tabs[st.session_state.tabs.index("Tab 3")] = "Tab C"
+
+
+tabs = st.tabs(st.session_state.tabs)
+
+for tabs_index, tab in enumerate(tabs):
+    with tab:
+        st.write(f"You are in Tab {tabs_index + 1}")
+        st.slider(f"Slider {tabs_index + 1}", 0, 10, 5, key=tab)


Test patch:
diff --git a/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-change-all-names[chromium].png b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-change-all-names[chromium].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-change-all-names[chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-change-all-names[firefox].png b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-change-all-names[firefox].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-change-all-names[firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-change-all-names[webkit].png b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-change-all-names[webkit].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-change-all-names[webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-change-some-names[chromium].png b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-change-some-names[chromium].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-change-some-names[chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-change-some-names[firefox].png b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-change-some-names[firefox].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-change-some-names[firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-change-some-names[webkit].png b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-change-some-names[webkit].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-change-some-names[webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-default[chromium].png b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-default[chromium].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-default[chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-default[firefox].png b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-default[firefox].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-default[firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-default[webkit].png b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-default[webkit].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-default[webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-remove-selected[chromium].png b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-remove-selected[chromium].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-remove-selected[chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-remove-selected[firefox].png b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-remove-selected[firefox].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-remove-selected[firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-remove-selected[webkit].png b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-remove-selected[webkit].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-remove-selected[webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-selection-add-tab[chromium].png b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-selection-add-tab[chromium].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-selection-add-tab[chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-selection-add-tab[firefox].png b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-selection-add-tab[firefox].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-selection-add-tab[firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-selection-add-tab[webkit].png b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-selection-add-tab[webkit].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-selection-add-tab[webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-selection-remove-tab[chromium].png b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-selection-remove-tab[chromium].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-selection-remove-tab[chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-selection-remove-tab[firefox].png b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-selection-remove-tab[firefox].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-selection-remove-tab[firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-selection-remove-tab[webkit].png b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-selection-remove-tab[webkit].png
new file mode 100644
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_tabs_selection_test/tabs-selection-remove-tab[webkit].png differ
diff --git a/e2e_playwright/st_tabs_selection_test.py b/e2e_playwright/st_tabs_selection_test.py
new file mode 100644
--- /dev/null
+++ b/e2e_playwright/st_tabs_selection_test.py
@@ -0,0 +1,115 @@
+# 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, wait_for_app_run
+
+EXPANDER_HEADER_IDENTIFIER = ".streamlit-expanderHeader"
+
+
+def test_default_selection_first_tab(app: Page, assert_snapshot: ImageCompareFunction):
+    """Test st.tabs has first tab selected as default."""
+    assert_snapshot(app.locator(".stTabs"), name="tabs-default")
+
+
+def test_maintains_selection_when_other_tab_added(
+    app: Page, assert_snapshot: ImageCompareFunction
+):
+    """Test st.tabs maintains selected tab if additional tab added."""
+    control_buttons = app.locator(".stButton")
+    tab_buttons = app.locator(".stTabs button[role=tab]")
+    # Select Tab 2
+    tab_buttons.nth(1).click()
+    # Select Add Tab 3 button
+    control_buttons.nth(0).click()
+    # Wait for tabs to properly load
+    wait_for_app_run(app)
+    assert_snapshot(app.locator(".stTabs"), name="tabs-selection-add-tab")
+
+
+def test_maintains_selection_when_other_tab_removed(
+    app: Page, assert_snapshot: ImageCompareFunction
+):
+    """Test st.tabs maintains selected tab if non-selected tab removed."""
+    control_buttons = app.locator(".stButton")
+    # Reset Tabs
+    control_buttons.nth(5).click()
+    wait_for_app_run(app)
+    # Add Tab 3
+    control_buttons.nth(0).click()
+    # Select Tab 3
+    tab_buttons = app.locator(".stTabs button[role=tab]")
+    tab_buttons.nth(2).click()
+    # Select Remove Tab 1 button
+    control_buttons.nth(1).click()
+    # Wait for tabs to properly load
+    wait_for_app_run(app)
+    assert_snapshot(app.locator(".stTabs"), name="tabs-selection-remove-tab")
+
+
+def test_resets_selection_when_selected_tab_removed(
+    app: Page, assert_snapshot: ImageCompareFunction
+):
+    """Test st.tabs resets selected tab to 1 if previously selected tab removed."""
+    control_buttons = app.locator(".stButton")
+    # Reset Tabs
+    control_buttons.nth(5).click()
+    wait_for_app_run(app)
+    # Select Tab 2
+    tab_buttons = app.locator(".stTabs button[role=tab]")
+    tab_buttons.nth(1).click()
+    # Select Remove Tab 2 button
+    control_buttons.nth(2).click()
+    # Wait for tabs to properly load
+    wait_for_app_run(app)
+    assert_snapshot(app.locator(".stTabs"), name="tabs-remove-selected")
+
+
+def test_maintains_selection_when_same_name_exists(
+    app: Page, assert_snapshot: ImageCompareFunction
+):
+    """Test when tabs names change, keeps selected tab if matching label still exists."""
+    control_buttons = app.locator(".stButton")
+    # Reset Tabs
+    control_buttons.nth(5).click()
+    wait_for_app_run(app)
+    # Add Tab 3
+    control_buttons.nth(0).click()
+    # Select Tab 2
+    tab_buttons = app.locator(".stTabs button[role=tab]")
+    tab_buttons.nth(1).click()
+    # Change Tab 1 & 3 Names
+    control_buttons.nth(3).click()
+    # Wait for tabs to properly load
+    wait_for_app_run(app)
+    assert_snapshot(app.locator(".stTabs"), name="tabs-change-some-names")
+
+
+def test_resets_selection_when_tab_names_change(
+    app: Page, assert_snapshot: ImageCompareFunction
+):
+    """Test when tabs names change, reset selected tab if no matching label exists."""
+    control_buttons = app.locator(".stButton")
+    # Reset Tabs
+    control_buttons.nth(5).click()
+    wait_for_app_run(app)
+    # Select Tab 2
+    tab_buttons = app.locator(".stTabs button[role=tab]")
+    tab_buttons.nth(1).click()
+    # Change All Tab Names
+    control_buttons.nth(4).click()
+    # Wait for tabs to properly load
+    wait_for_app_run(app)
+    assert_snapshot(app.locator(".stTabs"), name="tabs-change-all-names")