Repository: streamlit/streamlit

Issue:
Feature request: Tabs
### Problem

Sorry if this has been discussed before (couldn't find anything here on on the forum). While building [my app](https://github.com/explosion/spaCy/blob/master/examples/streamlit_spacy.py), I thought it could be nice to have a tabbed UI component, e.g. to show different "views" of the data (visualisation, table view, JSON, more detailed docs etc.) If the user clicks a tab, that content is generated – otherwise it's not.

### Solution

API could maybe look something like this? Specifying the group name could be optional (default is the same group).

```python
with streamlit.tab("Tab one", "tabgroup1"):
    st.write("This is tab content and only run if user selects the tab")
with streamlit.tab("Tab two", "tabgroup1"):
    st.write("This is tab content and only run if user selects the tab")
```
### Additional context

Probably clear, but better to be explicit. By "tabs", I mean something like this:

<img width="683" alt="Screenshot 2019-10-02 at 13 05 22" src="https://user-images.githubusercontent.com/13643239/66039487-5256b980-e515-11e9-8b4f-f6f4f12312a2.png">




Discussion:
This is a very cool feature indeed. NLP apps can display a lot of information, and it's ugly and not user-friendly to show everything in one page. A tabbed layout would be very useful indeed. A tab that generates content once clicked is very useful in terms of performance as well.
And @ines 

I really like the api you are proposing. I have requested something similar for the app and grid layout here.

https://github.com/streamlit/streamlit/issues/486#issuecomment-548666166
Now that we can get and set query parameters at least in the nightly release via #1169, I think there's a hacky workaround that's possible for some use cases. You could write tabs with st.markdown() and have the links point to something like `/?tabchoice=1`, then get the value of query parameter `tabchoice` and so something like
```
if tabchoice == "1":
    # write stuff
```
I'm going to try this out soon so we'll see how it goes
Hi @benlindsay could you get it?
Sorry @Ciroye, haven't gotten around to this yet.
A related request: It would also be great if the each container created with the new st.beta_container / st.beta_column functionality could be 'tapped.' 

If the API would be what ines suggests, one could imagine something like

```
col1, col2 = st.beta_columns(2, include_tabs=[True, False])

with col1:
    with st.tab("Tab 1"):
        st.write("This is tab 1 in col 1")
    with st.tab("Tab 2"):
        st.write("This is tab 2 in col 1")
with col2:
    st.write("I am a container with no tabs")
```

although I have no experience with nested context managers!
Another killer feature for tabs would be the ability to choose whether they should 'prerender' like the content in a st.beta_expander() or whether they should only be rendered after selected, like it happens now if you e.g. use the 'pages' approach with a radio button pioneered by @MarcSkovMadsen on awesome-streamlit. I don't know if it would be better to have a boolean flag in the proposed st.tab() or whether it should be two seperate things, but it would be a very useful feature, especially if combined with the tabbed container approach I suggested above.
So I finally got around to trying the markdown + bootstrap + query parameters hack that was in my head and it sorta works. You'll lose all widget values because each tab link would refresh the whole page I think, so if you want to preserve variables you'd probably have to have all of them stored as a query parameter instead of just the tab values. Here's code that works for me on streamlit==0.80.0 and a little demo on my old, slow, computer:

```python
import streamlit as st

st.markdown(
    '<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">',
    unsafe_allow_html=True,
)
query_params = st.experimental_get_query_params()
tabs = ["Home", "About", "Contact"]
if "tab" in query_params:
    active_tab = query_params["tab"][0]
else:
    active_tab = "Home"

if active_tab not in tabs:
    st.experimental_set_query_params(tab="Home")
    active_tab = "Home"

li_items = "".join(
    f"""
    <li class="nav-item">
        <a class="nav-link{' active' if t==active_tab else ''}" href="/?tab={t}">{t}</a>
    </li>
    """
    for t in tabs
)
tabs_html = f"""
    <ul class="nav nav-tabs">
    {li_items}
    </ul>
"""

st.markdown(tabs_html, unsafe_allow_html=True)
st.markdown("<br>", unsafe_allow_html=True)

if active_tab == "Home":
    st.write("Welcome to my lovely page!")
    st.write("Feel free to play with this ephemeral slider!")
    st.slider(
        "Does this get preserved? You bet it doesn't!",
        min_value=0,
        max_value=100,
        value=50,
    )
elif active_tab == "About":
    st.write("This page was created as a hacky demo of tabs")
elif active_tab == "Contact":
    st.write("If you'd like to contact me, then please don't.")
else:
    st.error("Something has gone terribly wrong.")
```

![tabs](https://user-images.githubusercontent.com/9361152/115124200-ace42900-9f86-11eb-8525-a692ebdcbb9c.gif)

A more streamlit-native approach to tabs that treats tab selection as just another widget value to remember would be much more ideal, but this is a potential interim solution, especially if you don't need to preserve widget values between tabs.
Apparently there's also a solution with components by @karriebear: https://discuss.streamlit.io/t/tabs-or-nested-tabs-with-st-dataframe/4546/3
> So I finally got around to trying the markdown + bootstrap + query parameters hack that was in my head and it sorta works. You'll lose all widget values because each tab link would refresh the whole page I think, so if you want to preserve variables you'd probably have to have all of them stored as a query parameter instead of just the tab values. Here's code that works for me on streamlit==0.80.0 and a little demo on my old, slow, computer:
> 
> ```python
> import streamlit as st
> 
> st.markdown(
>     '<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">',
>     unsafe_allow_html=True,
> )
> query_params = st.experimental_get_query_params()
> tabs = ["Home", "About", "Contact"]
> if "tab" in query_params:
>     active_tab = query_params["tab"][0]
> else:
>     active_tab = "Home"
> 
> if active_tab not in tabs:
>     st.experimental_set_query_params(tab="Home")
>     active_tab = "Home"
> 
> li_items = "".join(
>     f"""
>     <li class="nav-item">
>         <a class="nav-link{' active' if t==active_tab else ''}" href="/?tab={t}">{t}</a>
>     </li>
>     """
>     for t in tabs
> )
> tabs_html = f"""
>     <ul class="nav nav-tabs">
>     {li_items}
>     </ul>
> """
> 
> st.markdown(tabs_html, unsafe_allow_html=True)
> st.markdown("<br>", unsafe_allow_html=True)
> 
> if active_tab == "Home":
>     st.write("Welcome to my lovely page!")
>     st.write("Feel free to play with this ephemeral slider!")
>     st.slider(
>         "Does this get preserved? You bet it doesn't!",
>         min_value=0,
>         max_value=100,
>         value=50,
>     )
> elif active_tab == "About":
>     st.write("This page was created as a hacky demo of tabs")
> elif active_tab == "Contact":
>     st.write("If you'd like to contact me, then please don't.")
> else:
>     st.error("Something has gone terribly wrong.")
> ```
> 
> ![tabs](https://user-images.githubusercontent.com/9361152/115124200-ace42900-9f86-11eb-8525-a692ebdcbb9c.gif)
> 
> A more streamlit-native approach to tabs that treats tab selection as just another widget value to remember would be much more ideal, but this is a potential interim solution, especially if you don't need to preserve widget values between tabs.

Hi thank you for sharing this example. However when we deploy to sahre it is giving error : 404 page not found. Did you try deploy for sharing? 
No i haven't tried streamlit sharing at all yet. I don't know enough about the specifics of that platform to have a guess at why this would give a 404 error
> No i haven't tried streamlit sharing at all yet. I don't know enough about the specifics of that platform to have a guess at why this would give a 404 error

Okey thank you for response @benlindsay I will open a new issue. 

Resolving patch:
diff --git a/e2e/scripts/st_tabs.py b/e2e/scripts/st_tabs.py
new file mode 100644
--- /dev/null
+++ b/e2e/scripts/st_tabs.py
@@ -0,0 +1,36 @@
+# Copyright 2018-2022 Streamlit Inc.
+#
+# 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
+
+tab1, tab2, tab3 = st.tabs(["Tab 1", "Tab 2", "Tab 3"])
+
+with tab1:
+    st.write("tab1")
+    st.text_input("Text input")
+
+with tab2:
+    st.write("tab2")
+    st.number_input("Number input")
+
+with tab3:
+    st.write("tab3")
+    st.date_input("Date input")
+
+with st.expander("Expander", expanded=True):
+    many_tabs = st.tabs([f"Tab {i}" for i in range(25)])
+
+sidebar_tab1, sidebar_tab2 = st.sidebar.tabs(["Foo", "Bar"])
+sidebar_tab1.write("I am in the sidebar")
+sidebar_tab2.write("I'm also in the sidebar")
diff --git a/lib/streamlit/__init__.py b/lib/streamlit/__init__.py
--- a/lib/streamlit/__init__.py
+++ b/lib/streamlit/__init__.py
@@ -123,6 +123,7 @@ def _update_logger() -> None:
 checkbox = _main.checkbox
 code = _main.code
 columns = _main.columns
+tabs = _main.tabs
 container = _main.container
 dataframe = _main.dataframe
 date_input = _main.date_input
diff --git a/lib/streamlit/elements/layouts.py b/lib/streamlit/elements/layouts.py
--- a/lib/streamlit/elements/layouts.py
+++ b/lib/streamlit/elements/layouts.py
@@ -171,6 +171,93 @@ def column_proto(normalized_weight: float) -> BlockProto:
         total_weight = sum(weights)
         return [row._block(column_proto(w / total_weight)) for w in weights]
 
+    def tabs(self, tabs: Sequence[str]) -> Sequence["DeltaGenerator"]:
+        """Insert containers seperated into tabs.
+
+        Inserts a number of multi-element containers as tabs.
+        Tabs are a navigational element that allows users to easily
+        move between groups of related content.
+
+        To add elements to the returned containers, you can use "with" notation
+        (preferred) or just call methods directly on the returned object. See
+        examples below.
+
+        .. warning::
+            All the content of every tab is always sent to and rendered on the frontend.
+            Conditional rendering is currently not supported.
+
+        Parameters
+        ----------
+        tabs : list of strings
+            Creates a tab for each string in the list. The string is used as the name of the tab.
+            The first tab is selected by default.
+
+        Returns
+        -------
+        list of containers
+            A list of container objects.
+
+        Examples
+        --------
+
+        You can use `with` notation to insert any element into a tab:
+
+        >>> tab1, tab2, tab3 = st.tabs(["Cat", "Dog", "Owl"])
+        >>>
+        >>> with tab1:
+        ...    st.header("A cat")
+        ...    st.image("https://static.streamlit.io/examples/cat.jpg", width=200)
+        ...
+        >>> with tab2:
+        ...    st.header("A dog")
+        ...    st.image("https://static.streamlit.io/examples/dog.jpg", width=200)
+        ...
+        >>> with tab3:
+        ...    st.header("An owl")
+        ...    st.image("https://static.streamlit.io/examples/owl.jpg", width=200)
+
+        .. output ::
+            https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/layout.tabs1.py
+            height: 700px
+
+        Or you can just call methods directly in the returned objects:
+
+        >>> tab1, tab2 = st.tabs(["📈 Chart", "🗃 Data"])
+        >>> data = np.random.randn(10, 1)
+        >>>
+        >>> tab1.subheader("A tab with a chart")
+        >>> tab1.line_chart(data)
+        >>>
+        >>> tab2.subheader("A tab with the data")
+        >>> tab2.write(data)
+
+
+        .. output ::
+            https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/layout.tabs2.py
+            height: 700px
+
+        """
+        if not tabs:
+            raise StreamlitAPIException(
+                "The input argument to st.tabs must contain atleast one tab label."
+            )
+
+        if any(isinstance(tab, str) == False for tab in tabs):
+            raise StreamlitAPIException(
+                "The tabs input list to st.tabs is only allowed to contain strings."
+            )
+
+        def tab_proto(label: str) -> BlockProto:
+            tab_proto = BlockProto()
+            tab_proto.tab.label = label
+            tab_proto.allow_empty = True
+            return tab_proto
+
+        block_proto = BlockProto()
+        block_proto.tab_container.SetInParent()
+        tab_container = self.dg._block(block_proto)
+        return tuple(tab_container._block(tab_proto(tab_label)) for tab_label in tabs)
+
     def expander(self, label: str, expanded: bool = False) -> "DeltaGenerator":
         """Insert a multi-element container that can be expanded/collapsed.
 


Test patch:
diff --git a/frontend/src/components/elements/Tabs/Tabs.test.tsx b/frontend/src/components/elements/Tabs/Tabs.test.tsx
new file mode 100644
--- /dev/null
+++ b/frontend/src/components/elements/Tabs/Tabs.test.tsx
@@ -0,0 +1,76 @@
+/**
+ * @license
+ * Copyright 2018-2022 Streamlit Inc.
+ *
+ * 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 React from "react"
+import { mount } from "src/lib/test_util"
+import { BlockNode } from "src/lib/AppNode"
+import { Block as BlockProto } from "src/autogen/proto"
+
+import { Tabs as UITabs } from "baseui/tabs-motion"
+import Tabs, { Props } from "./Tabs"
+
+function makeTab(label: string, children: BlockNode[] = []): BlockNode {
+  return new BlockNode(
+    children,
+    new BlockProto({ allowEmpty: true, tab: { label } })
+  )
+}
+
+function makeTabsNode(tabs: number): BlockNode {
+  return new BlockNode(
+    Array.from({ length: tabs }, (_element, index) => makeTab(`Tab ${index}`)),
+    new BlockProto({ allowEmpty: true })
+  )
+}
+
+const getProps = (props?: Partial<Props>): Props =>
+  Object({
+    widgetsDisabled: false,
+    node: makeTabsNode(5),
+    isStale: false,
+    ...props,
+  })
+
+describe("st.tabs", () => {
+  it("renders without crashing", () => {
+    const wrapper = mount(<Tabs {...getProps()} />)
+    expect(wrapper.find(UITabs).exists()).toBe(true)
+    expect(wrapper.find("StyledTab").length).toBe(5)
+  })
+
+  it("sets the tab labels correctly", () => {
+    const wrapper = mount(<Tabs {...getProps()} />)
+
+    expect(wrapper.find("StyledTab").length).toBe(5)
+    wrapper.find("StyledTab").forEach((option, index) => {
+      expect(option.text()).toBe(`Tab ${index}`)
+    })
+  })
+
+  it("can be disabled", () => {
+    const wrapper = mount(<Tabs {...getProps({ widgetsDisabled: true })} />)
+
+    wrapper.find("StyledTab").forEach((option, index) => {
+      if (index === 0) {
+        // the selected tab does not have the disabled prop as true in baseweb
+        expect(option.prop("disabled")).toBe(false)
+      } else {
+        expect(option.prop("disabled")).toBe(true)
+      }
+    })
+  })
+})
diff --git a/lib/tests/streamlit/layouts_test.py b/lib/tests/streamlit/layouts_test.py
--- a/lib/tests/streamlit/layouts_test.py
+++ b/lib/tests/streamlit/layouts_test.py
@@ -1,4 +1,6 @@
 import streamlit as st
+from streamlit import StreamlitAPIException
+
 from tests import testutil
 
 
@@ -83,3 +85,41 @@ def test_just_label(self):
         expander_block = self.get_delta_from_queue()
         self.assertEqual(expander_block.add_block.expandable.label, "label")
         self.assertEqual(expander_block.add_block.expandable.expanded, False)
+
+
+class TabsTest(testutil.DeltaGeneratorTestCase):
+    def test_tab_required(self):
+        """Test that atleast one tab is required."""
+        with self.assertRaises(TypeError):
+            tabs = st.tabs()
+
+        with self.assertRaises(StreamlitAPIException):
+            tabs = st.tabs([])
+
+    def test_only_label_strings_allowed(self):
+        """Test that only strings are allowed as tab labels."""
+        with self.assertRaises(StreamlitAPIException):
+            tabs = st.tabs(["tab1", True])
+
+        with self.assertRaises(StreamlitAPIException):
+            tabs = st.tabs(["tab1", 10])
+
+    def test_returns_all_expected_tabs(self):
+        """Test that all labels are added in correct order."""
+        tabs = st.tabs([f"tab {i}" for i in range(5)])
+
+        self.assertEqual(len(tabs), 5)
+
+        for tab in tabs:
+            with tab:
+                pass
+
+        all_deltas = self.get_all_deltas_from_queue()
+
+        horizontal_block = all_deltas[0]
+        tabs_block = all_deltas[1:]
+        # 6 elements will be created: 1 horizontal block, 5 tabs
+        self.assertEqual(len(all_deltas), 6)
+        self.assertEqual(len(tabs_block), 5)
+        for index, tabs_block in enumerate(tabs_block):
+            self.assertEqual(tabs_block.add_block.tab.label, f"tab {index}")