New layout options for Streamlit

Streamlit is all about simplicity. It’s pure Python. Your script runs from top to bottom. Your app renders from top to bottom too. Perfect, right? Well...not quite. Users noted that our thinking was a bit too vertical. The group griped about grids. The community clamored for columns. Fervent friends favored flexibility. You get the idea.

So move aside, vertical layout. Make a little space for... horizontal layout! And a bunch more layout primitives. And some syntactic goodies too. In fact, today, we're introducing four new layout features giving you much more control over your app’s presentation.

  • st.beta_columns: Side-by-side columns where you can insert Streamlit elements
  • st.beta_expander: An expand/collapse widget to selectively show stuff
  • st.beta_container: The fundamental building block of layout
  • with column1: st.write("hi!"): Syntax sugar to specify which container to use

Go horizontal with columns

st.beta_columns acts similarly to our beloved st.sidebar, except now you can put the columns anywhere in your app. Just declare each column as a new variable, and then you can add in ANY element or component available from the Streamlit library.

Use columns to compare things side-by-side:

col1, col2 = st.beta_columns(2)
original =
col1.image(original, use_column_width=True)
grayscale = original.convert('LA')
col2.image(grayscale, use_column_width=True)

In fact, by calling st.beta_columns inside a loop, you get a grid layout!

st.title("Let's create a table!")
for i in range(1, 10):
    cols = st.beta_columns(4)
    cols[1].write(f'{i * i}')
    cols[2].write(f'{i * i * i}')
    cols[3].write('x' * i)

You can even get quite complex (which can be great for wide monitors!) Here's an example that uses variable-width columns in conjunction with the wide-mode layout:

# Use the full page instead of a narrow central column
# Space out the maps so the first one is 2x the size of the other three
c1, c2, c3, c4 = st.beta_columns((2, 1, 1, 1))

And just in case you were wondering: yes, columns are beautiful across devices and automatically resize for mobile and different browser widths.

Clean things up with expanders

Now that we've maximized horizontal space, try st.beta_expander, to maximize your vertical space! Some of you may have been using st.checkbox for this before, and expander is a prettier, more performant replacement 🙂

It's a great way to hide your secondary controls, or provide longer explanations that users can toggle!

Adding a new concept: containers!

If you squint a bit, st.beta_columns, st.beta_expander, and st.sidebar look kind of similar. They all return Python objects, which allow you to call all the Streamlit functions.  We've given these objects a new name: containers. And since it would be nice to create containers directly, you can!

st.beta_container is a building block that helps you organize your app. Just like st.empty, st.beta_container lets you set aside some space, and then later write things to it out of order. But while subsequent calls to the same st.empty replace the item inside it, subsequent calls to the same st.beta_container append to it. Once again, this works just like the st.sidebar you've come to know and love.

Organize your code with... with

Finally, we're introducing a new syntax to help you manage all these new containers: with container. How does it work? Well, instead of making function calls directly on the container...

my_expander = st.beta_expander()
my_expander.write('Hello there!')
clicked = my_expander.button('Click me!')

Use the container as a Context Manager and call functions from the st. namespace!

my_expander = st.beta_expander()
with my_expander:
    'Hello there!'
    clicked = st.button('Click me!')

Why? This way, you can compose your own widgets in pure Python, and reuse them in different containers!

def my_widget(key):
    st.subheader('Hello there!')
    clicked = st.button("Click me " + key)
# This works in the main area
clicked = my_widget("first")
# And within an expander
my_expander = st.beta_expander("Expand", expanded=True)
with my_expander:
    clicked = my_widget("second")
# AND in st.sidebar!
with st.sidebar:
    clicked = my_widget("third")

One last thing: the with syntax lets you put your Custom Components inside any container you like. Here's one that embeds the Streamlit Ace editor in a column right next to the app itself — so a user can edit the code and see the changes LIVE!

That's all, folks!

To start playing with layout today, simply upgrade to the latest version of Streamlit (v0.68).

$ pip install streamlit --upgrade

Coming up are updates with padding, alignment, responsive design, and UI customization. Stay tuned for that, but most importantly, let us know what YOU want from layout. Questions? Suggestions? Or just have a neat app you want to show off? Join us on the Streamlit community forum — we can't wait to see what you create 🎈


Documentation GitHub Changelog


A shoutout to the Streamlit Community and Creators, whose feedback really shaped the implementation of Layout: Jesse, José, Charly, and Synode — and a special callout to Fanilo for going the extra mile to find bugs, suggest APIs, and overall try out a bunch of our prototypes. Thank you all so much ❤️

This is a companion discussion topic for the original entry at

Can’t believe what I am seeing now. Have been waiting for some of these options. You guys did a great work! :clap: :clap: :clap: :clap: :clap: :clap:


Wow! I have been waiting for this, and now it is here.:+1: :+1:
Prior to this, Streamlit solved half of my problems, now it solved all my problems!!! I better start looking for more problems

Hi Guys,

Amazing job done. However, is it possible to also have rows like we have columns?

The problem is, I have different content in different columns which can differ in length. However, I would like the row to have same height across all columns.

How can that be done?


Hi @Anshuman_Bhadauria,

You can do it using beta_container,

Try running this script to explore the layout,

import streamlit as st
from string import ascii_uppercase, digits
from random import choices

img_base = "{0}.png"

colors = (''.join(choices(ascii_uppercase[:6] + digits, k=6)) for _ in range(100))

with st.beta_container():
    for col in st.beta_columns(3):
        col.image(img_base.format(next(colors)), use_column_width=True)

with st.beta_container():
    for col in st.beta_columns(4):
        col.image(img_base.format(next(colors)), use_column_width=True)

with st.beta_container():
    for col in st.beta_columns(10):
        col.image(img_base.format(next(colors)), use_column_width=True)

Hope it helps!

1 Like

Hi Ash.

Thank you for the reply.

However, in the example you present, the row height remains same across columns because you are using the same image.

What if they are of different size?

In my case, different columns can have different amount of data, and thus the row height is different for each column.

Also, as I understand, containers don’t work like rows right?


It works :slight_smile:

  1. Yes you are right, right now you can’t define the height of containers or the alignment of elements inside the containers. You can expect something like this in future releases I guess.

  2. Yes, containers don’t work like rows. But if you create containers on the root layout it works like rows as they are created one after the other. So it can serve the functionality of rows.

1 Like


love this option!

I implemented it like this on four columns

If you select something in the third “row”, another widget will appear in the second column.

My problem is that, from time to time, for no apparent reason the widget on the third row “expands” (becoming longer) and becomes not clickable. I happens randomly, and it generally goes back to normal by reloading the code.

I has tried initializing the column variables with st.empty() but it does not seem to make a big difference. Again happens from time to time.




Hi Fabio, sorry you’re experiencing this! It definitely sounds like a bug. Those images are super valuable for helping us understand what you’re seeing – do you also have a code snippet that demonstrates the issue, so we can reproduce it and pin down what’s going on?

Hi Austin,

thanks for your response.

The error is sporadic, but this morning it did it again :slight_smile:

I am trying to figure out what sequence makes it misbehave. Now it happens all the time. Link is here When you choose a file in the left dropdown on the first row, it shows our dropdown in the third row that sometimes is wider than it should.

The code. I initialize the columns

col31,col32,empty3,empty4 = st.beta_columns(colWidthArray)

then I try to use st.empty for the columns I do not need at the beginning (I have tried with and without st.empty but it does not seem to make a difference, not sure it makes sense to use it here).

and then I set up my widget. The second widget only appears if the users puts a number in the first.

The first widget is used to choose a column to drill down. The user can drilldown up to 4 rows, so the widget can be generated four times with different key. So this is the code that actually instantiates the mis-behaving widget.

chosenDrilldownRowLabel=chosenDrilldownRowLabel+" #"+str(number)
                                            choiceArray,index=0, key="chosenDrilldownRow"+str(number)) 

Please not that if you click on the left side, where the widget is supposed to be it works. While if you click on the black arrow on the right it does not.

Happy to answer any other question



Hi again,

I thought that this might be help. As mentioned when you select a row number in the misbehaving drop down widget an second widget will appear.

As you can see with the red lines part of the first widget is under the second one.



Hi again,

Maybe this helps. I added an expander with some explanation info under the misbehaving widget. When the page opens, if the widget is too wide also the expander will be too wide.

However if you open the expander both the widget and the expander will go back to their correct dimensions and keep them.



Hi Fabio. The layout looks so cool. I’m trying to develop some similar version you had. Can I get the code to see how you implemented these new concepts?


nothing special here. It is streamlit that is amazing.
I just initialize my four columns

col1,col2,col3,col4 = st.beta_columns(colWidthArray)

and then fill them up with my widgets and info…

  with col:
        info = st.beta_expander("+")
        with info:
              Takes one of more columns out of the dataset so these columns do not appear in the results.

Hope this helps! :slight_smile:


1 Like

Do and st.multiselect work with columns?

options = ["a", "b", "c", "d"]

col1, col2 = st.beta_columns(2)

col1.level_radio =
    "Blah blah:", options, index=2

col2.section_filters = st.multiselect(
    "Blah:", sorted(list(options)), sorted(list(options))

are not side by side

update, it works if you use with:

with col1:
    level_radio =
        "Blah blah:", options, index=2

with col2:
    section_filters = st.multiselect(
        "Blah:", sorted(list(options)), sorted(list(options))

Ah! @ww-carl-anderson beat me to it! Here is the example I worked up for you:

# Lets make columns with widgets in
st.write('Lets put widgets side by side!')
col1, col2 = st.beta_columns(2)
options = ['a','b','c','d']

with col1:
    level_radio ="blah blah", options)

with col2:
    filters = st.multiselect('blah', options)

the output becomes:

1 Like

You could use:

options = [“a”, “b”, “c”, “d”]

col1, col2 = st.beta_columns(2)

level_radio =
“Blah blah:”, options, index=2

section_filters = col2.multiselect(
“Blah:”, sorted(list(options)), sorted(list(options))

1 Like

Hi Guys,

So the row height is maintained with widgets but with text in column and widget in another it breaks the roz height.

Any way to fix this?