10 Essential Streamlit Design Tips: Building Professional Dashboards That Don’t Look Like Streamlit
Streamlit has revolutionized how we build data applications in Python. Its genius lies in its simplicity — no HTML, no JavaScript, just…
10 Essential Streamlit Design Tips: Building Professional Dashboards That Don’t Look Like Streamlit

Streamlit has revolutionized how we build data applications in Python. Its genius lies in its simplicity — no HTML, no JavaScript, just pure Python. Within minutes, you can transform a Jupyter notebook into a fully functional web app and deploy it to Streamlit Cloud, Heroku, or Railway without breaking a sweat.
But here’s the thing: Streamlit prioritizes functionality over aesthetics. And that’s by design.
Most Streamlit apps are MVPs, internal tools, or proof-of-concepts built by data scientists who care more about getting insights to stakeholders than pixel-perfect designs. The default widgets are functional but basic. The layouts are simple but rigid. And let’s be honest — most Streamlit apps look… like Streamlit apps.
But it doesn’t have to be this way.
With a few strategic design decisions and modern features introduced in recent versions, you can build Streamlit applications that are not only functional but genuinely impressive. Apps that make people stop and ask: “Wait, you built this with Streamlit?”
This article presents 10 actionable design tips with complete code examples, context on why they matter, and practical insights from building production Streamlit applications. By the end, you’ll have a comprehensive toolkit for creating dashboards that look as good as they work.
Before You Start: Update Your Streamlit
Critical: Make sure you’re running Streamlit 1.48.0 or newer. Streamlit releases updates every few weeks, and many features in this article didn’t exist a year ago. The Streamlit you remember might be very different from today’s version.
To upgrade:
pip install --upgrade streamlit
Check your version:
streamlit --version
Now, let’s dive into the tips that will transform your dashboards.
1. 🎯 Adding Hover Tooltips with the help Parameter
Why This Matters
Cluttered interfaces overwhelm users. Long explanations break visual flow. Tooltips solve both problems — they provide context on demand without sacrificing screen space.
The Psychology
Users don’t read instructions until they’re confused. Tooltips appear exactly when users need them (when they hover), making them more effective than static text. Plus, tooltips render client-side and don’t trigger reruns, so they’re performance-friendly.
Implementation
Almost every Streamlit widget supports the help parameter. Here's how to use it effectively:
import streamlit as st
st.title("User Registration Form")
# Text input with tooltip
username = st.text_input(
"Username",
help="Choose a unique username. Letters, numbers, and underscores only."
)
# Slider with contextual help
age = st.slider(
"Age",
min_value=18,
max_value=100,
value=25,
help="You must be at least 18 years old to register."
)
# Selectbox with business context
department = st.selectbox(
"Department",
options=["Engineering", "Sales", "Marketing", "HR"],
help="Your department determines which dashboards you can access."
)
# Checkbox with legal context
terms = st.checkbox(
"I agree to the terms and conditions",
help="Click here to read our full terms of service and privacy policy."
)

Output Result:
Each widget displays a small ℹ️ icon. Hovering reveals helpful tooltips that guide users without cluttering your interface.
Pro Tip: Use tooltips to explain business logic, data definitions, or calculation methods. They’re perfect for bridging the gap between technical teams and business users.
Works With: st.button, st.checkbox, st.multiselect, st.radio, st.number_input, st.date_input, st.pills, and most other widgets.
2. 📊 Integrating Interactive Plotly Charts
Why This Matters
Matplotlib is not for dashboards. There, I said it. While Matplotlib is excellent for static publication-quality charts, dashboards demand interactivity. Users need to zoom, pan, hover, and explore data dynamically.
Plotly provides professional, interactive visualizations that transform data exploration from passive viewing to active discovery.
The Business Case
Interactive charts increase user engagement by 300–500% compared to static images. Users spend more time exploring data, discover insights independently, and ask better questions.
Implementation
Here’s a sophisticated Plotly sunburst chart showing hierarchical sales data:
import streamlit as st
import plotly.express as px
import pandas as pd
# Sample hierarchical data
df = pd.DataFrame({
'category': ['Electronics', 'Electronics', 'Electronics', 'Clothing', 'Clothing', 'Food', 'Food'],
'subcategory': ['Laptops', 'Phones', 'Tablets', 'Men', 'Women', 'Beverages', 'Snacks'],
'sales': [45000, 38000, 22000, 31000, 42000, 15000, 18000],
})
# Create interactive sunburst
fig = px.sunburst(
df,
path=['category', 'subcategory'],
values='sales',
title='Sales Distribution by Category',
color='sales',
color_continuous_scale='RdYlGn',
hover_data={'sales': ':$,.0f'}
)
# Customize layout
fig.update_layout(
font=dict(family="Arial", size=14),
height=600,
margin=dict(t=50, l=0, r=0, b=0)
)
st.plotly_chart(fig, use_container_width=True)

Advanced: Custom Hover Templates
import plotly.graph_objects as go
import streamlit as st
import plotly.express as px
import pandas as pd
# Create time series with custom hover
df_time = pd.DataFrame({
'date': pd.date_range('2024-01-01', periods=12, freq='M'),
'revenue': [125000, 132000, 128000, 145000, 158000, 162000,
175000, 188000, 192000, 205000, 218000, 235000],
'target': [130000, 135000, 140000, 145000, 150000, 160000,
170000, 180000, 190000, 200000, 210000, 220000]
})
fig = go.Figure()
# Revenue line
fig.add_trace(go.Scatter(
x=df_time['date'],
y=df_time['revenue'],
mode='lines+markers',
name='Actual Revenue',
line=dict(color='#667eea', width=3),
marker=dict(size=8, color='#764ba2'),
hovertemplate='<b>%{x|%B %Y}</b><br>' +
'Revenue: $%{y:,.0f}<br>' +
'<extra></extra>'
))
# Target line
fig.add_trace(go.Scatter(
x=df_time['date'],
y=df_time['target'],
mode='lines',
name='Target',
line=dict(color='#FF6B6B', width=2, dash='dash'),
hovertemplate='<b>%{x|%B %Y}</b><br>' +
'Target: $%{y:,.0f}<br>' +
'<extra></extra>'
))
fig.update_layout(
template='plotly_white',
hovermode='x unified',
title='Revenue vs Target Performance',
font=dict(family='Arial', size=14),
showlegend=True,
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1)
)
st.plotly_chart(fig, use_container_width=True)

Output Result:
A beautiful, interactive chart where users can hover for detailed information, zoom into specific time periods, and click legend items to toggle data series.
Pro Tip: Always use use_container_width=True to make charts responsive. Set hovermode='x unified' to show all series data at once when hovering.
Other Great Plotly Charts for Dashboards:
- Treemaps for hierarchical data
- Waterfall charts for cumulative effects
- Choropleth maps for geographic data
- Sankey diagrams for flow analysis
- Candlestick charts for financial data
3. 🗂️ Organizing Your App with Strategic Sidebar Usage
Why This Matters
Dashboards fail when users can’t find what they need. Proper organization through sidebars and navigation creates intuitive user experiences that scale with complexity.
The Two Layout Patterns
Professional dashboards typically use one of two patterns:
Pattern 1: Top Navigation + Sidebar Filters
- Best for: 2–5 main sections with many filters
- Navigation: Horizontal tabs at top
- Sidebar: Contains all filters and controls
- Use case: Single-domain dashboards (e.g., Sales Analytics)
Pattern 2: Sidebar Navigation + Top Filters
- Best for: 6+ main sections with fewer filters
- Navigation: Vertical menu in sidebar
- Filters: Horizontal bar at top
- Use case: Multi-domain dashboards (e.g., Company-wide Analytics)
Implementation: Pattern 1 (Top Navigation + Sidebar Filters)
import streamlit as st
import pandas as pd
# Configure page
st.set_page_config(layout="wide", page_title="Sales Analytics")
# Define pages
page_home = st.Page("home.py", title="Overview", icon="🏠")
page_sales = st.Page("sales.py", title="Sales", icon="💰")
page_customers = st.Page("customers.py", title="Customers", icon="👥")
page_products = st.Page("products.py", title="Products", icon="📦")
# Top navigation
pg = st.navigation(
[page_home, page_sales, page_customers, page_products],
position="top"
)
# Sidebar filters
with st.sidebar:
st.header("⚙️ Filters")
# Date range
date_range = st.date_input(
"Date Range",
value=(pd.Timestamp('2024-01-01'), pd.Timestamp('2024-12-31')),
help="Select the time period for analysis"
)
st.divider()
# Region selection
regions = st.multiselect(
"Regions",
options=["North America", "Europe", "Asia", "South America"],
default=["North America", "Europe"],
help="Filter data by geographic region"
)
# Product category
categories = st.multiselect(
"Product Categories",
options=["Electronics", "Clothing", "Food", "Home & Garden"],
help="Filter by product category"
)
st.divider()
# Performance threshold
min_revenue = st.slider(
"Minimum Revenue ($)",
min_value=0,
max_value=100000,
value=10000,
step=5000,
help="Show only items above this revenue threshold"
)
# Run selected page
pg.run()

Make sure you add all files like sales.py, home.py etc
Implementation: Pattern 2 (Sidebar Navigation + Top Filters)
import streamlit as st
import pandas as pd
st.set_page_config(layout="wide", page_title="Enterprise Dashboard")
# Sidebar navigation
with st.sidebar:
st.image("logo.png", width=200) # Your company logo
st.title("Navigation")
# Define pages with Material icons
page_overview = st.Page("overview.py", title="Overview", icon=":material/dashboard:")
page_sales = st.Page("sales.py", title="Sales", icon=":material/sell:")
page_marketing = st.Page("marketing.py", title="Marketing", icon=":material/campaign:")
page_finance = st.Page("finance.py", title="Finance", icon=":material/payments:")
page_hr = st.Page("hr.py", title="HR Analytics", icon=":material/groups:")
page_settings = st.Page("settings.py", title="Settings", icon=":material/settings:")
# Navigation
pg = st.navigation([
page_overview,
page_sales,
page_marketing,
page_finance,
page_hr,
page_settings
])
# Top filters bar
col1, col2, col3, col4 = st.columns(4)
with col1:
time_period = st.selectbox(
"Time Period",
options=["Last 7 Days", "Last 30 Days", "Last Quarter", "Last Year"],
index=1
)
with col2:
business_unit = st.selectbox(
"Business Unit",
options=["All Units", "Consumer", "Enterprise", "SMB"]
)
with col3:
metric_type = st.selectbox(
"Metric",
options=["Revenue", "Units Sold", "Profit Margin", "Customer Count"]
)
with col4:
comparison = st.selectbox(
"Compare To",
options=["Previous Period", "Same Period Last Year", "Budget"]
)
st.divider()
# Run page
pg.run()

Make sure you add all .py files
Output Result:
Pattern 1: Clean horizontal tabs at top with a feature-rich sidebar for detailed filtering. Pattern 2: Professional sidebar navigation with company branding and quick filters across the top.
Pro Tip: Use st.divider() to create visual separation between filter groups. It improves scanability dramatically.
4. 🎨 Branding Your App with a Sidebar Logo
Why This Matters
A logo transforms a tool into a product. It creates brand recognition, increases perceived professionalism, and makes your dashboard memorable.
Creating Your Logo
You can create your own logo using Canva or photoshop but now a days you can also use AI tools like Sora, Dall-e to create logo by simply putting prompt
Implementation
import streamlit as st
# Configure sidebar
with st.sidebar:
# Logo at the top
st.image(
"logo.png",
width=200,
)
# App title and tagline
st.markdown("""
<h2 style='text-align: center; color: #667eea; margin: 0;'>
LOGO
</h2>
<p style='text-align: center; color: #888; font-size: 14px;'>
TESTING LOGO
</p>
""", unsafe_allow_html=True)
st.divider()
# Navigation or filters below
st.subheader("📊 Quick Stats")
st.metric("Active Users", "1,234", "+12%")
st.metric("Revenue", "$45.6K", "+8%")

I have used steamlit logo from https://streamlit.io/brand
Output Result:
A professional sidebar with your branded logo, app name, and tagline that immediately communicates purpose and quality.
Pro Tip: Keep logos simple and readable at small sizes. Transparent backgrounds work best. Aim for 200–250px width in the sidebar.
5. ⚙️ Customizing Your App with config.toml
Why This Matters
Global theming ensures consistency. Rather than styling each widget individually, config.toml applies your brand colors, fonts, and design system app-wide.
The Power of config.toml
This file controls everything from colors to server behavior. It’s the difference between “another Streamlit app” and “our company’s analytics platform.”
Setup
Create .streamlit/config.toml in your project root:
[theme]
# Primary brand color (buttons, links, accents)
primaryColor = "#667eea"
# Main background
backgroundColor = "#ffffff"
# Secondary backgrounds (sidebar, containers)
secondaryBackgroundColor = "#f0f2f6"
# Text color
textColor = "#262730"
# Font family - now supports custom fonts!
font = "sans serif" # Options: "sans serif", "serif", "monospace", or custom fonts
# Additional styling
base = "light" # or "dark"
[server]
# Server configuration
headless = true
port = 8501
enableCORS = false
enableXsrfProtection = true
# File watcher (helpful for development)
fileWatcherType = "auto"
[browser]
# Browser configuration
gatherUsageStats = false
[runner]
# Execution configuration
magicEnabled = true
fastReruns = true
Advanced: Using Custom Fonts
Since Streamlit 1.46.0, you can use custom fonts including popular ones like Poppins!
[theme]
font = "Poppins" # Modern, clean sans-serif
# Other options: "Roboto", "Open Sans", "Lato", "Montserrat"
Complete Example
import streamlit as st
st.title("Themed Dashboard")
# These elements will automatically use your theme colors
st.button("Primary Button") # Uses primaryColor
st.info("This info box uses your theme colors")
st.success("Theme colors apply to all widgets!")
# Sidebar automatically uses secondaryBackgroundColor
with st.sidebar:
st.header("Sidebar Header")
st.write("The sidebar background comes from your theme")
Output Result:
A fully branded application where every widget, button, and element uses your company colors and fonts automatically.
Pro Tip: Create separate config.toml files for different environments (development, staging, production) and swap them during deployment.
Customize Everything:
- Colors (text, background, primary, secondary)
- Fonts (family, size, weight through custom CSS)
- Padding and spacing
- Border radius
- Server behavior
- Security settings
6. 🎭 Styling Individual Widgets with Custom CSS
Why This Matters
Sometimes global themes aren’t enough. You need a special call-to-action button, a branded input field, or a unique card design. The key parameter unlocks per-widget styling.
The Breakthrough (Streamlit 1.39+)
Before version 1.39, custom styling was hacky. Now, you can assign CSS classes to individual widgets using the key parameter, creating surgical precision in your designs.
Implementation
Step 1: Create assets/styles.css:
/* Custom primary button */
.st-key-primary_cta button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
color: white !important;
border: none !important;
border-radius: 12px !important;
padding: 12px 32px !important;
font-weight: 600 !important;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4) !important;
transition: all 0.3s ease !important;
}
.st-key-primary_cta button:hover {
transform: translateY(-2px) !important;
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.6) !important;
}
/* Custom text area with brand colors */
.st-key-custom_textarea textarea {
background-color: #f8f9ff !important;
border: 2px solid #667eea !important;
border-radius: 12px !important;
padding: 16px !important;
font-size: 14px !important;
}
.st-key-custom_textarea textarea:focus {
border-color: #764ba2 !important;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1) !important;
}
/* Success button variant */
.st-key-success_btn button {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%) !important;
color: white !important;
border-radius: 8px !important;
border: none !important;
}
/* Danger button variant */
.st-key-danger_btn button {
background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%) !important;
color: white !important;
border-radius: 8px !important;
border: none !important;
}
/* Custom input field */
.st-key-search_input input {
border-radius: 20px !important;
border: 2px solid #e0e0e0 !important;
padding: 12px 20px !important;
}
.st-key-search_input input:focus {
border-color: #667eea !important;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1) !important;
}
Step 2: Load CSS and use styled widgets:
import streamlit as st
# Load custom CSS
def load_css():
with open("assets/styles.css") as f:
st.markdown(f'<style>{f.read()}</style>', unsafe_allow_html=True)
load_css()
# Title
st.title("Custom Styled Components")
# Styled buttons
col1, col2, col3 = st.columns(3)
with col1:
if st.button("Primary CTA", key="primary_cta"):
st.success("Primary button clicked!")
with col2:
if st.button("Success Action", key="success_btn"):
st.success("Success button clicked!")
with col3:
if st.button("Delete Item", key="danger_btn"):
st.error("Danger button clicked!")
# Comparison: Default vs Custom
st.subheader("Button Comparison")
col_a, col_b = st.columns(2)
with col_a:
st.write("**Default Streamlit Button:**")
st.button("Default Button")
with col_b:
st.write("**Custom Styled Button:**")
st.button("Custom Button", key="primary_cta")
st.divider()
# Custom text area
st.subheader("Styled Text Input")
col_x, col_y = st.columns(2)
with col_x:
st.write("**Default Text Area:**")
st.text_area("Feedback", placeholder="Enter your feedback...")
with col_y:
st.write("**Custom Styled Text Area:**")
st.text_area("Feedback", key="custom_textarea", placeholder="Enter your feedback...")
st.divider()
# Custom search input
search = st.text_input(
"Search",
key="search_input",
placeholder="🔍 Search dashboards...",
label_visibility="collapsed"
)
Output Result:
Beautiful gradient buttons that elevate on hover, branded text areas with custom borders, and professional input fields that match your design system.
Critical: Custom styles via key parameter override both default Streamlit styles and config.toml settings. Use strategically for emphasis.
Pro Tip: Create a library of reusable keys (primary_cta, secondary_btn, danger_btn) and use them consistently across your app for a cohesive design system.
7. 🧭 Enhancing Navigation with Material Icons
Why This Matters
Icons are a universal language. They reduce cognitive load, improve scanability, and make interfaces feel modern and professional. Material Icons from Google are the industry standard for web applications.
The Evolution
Streamlit 1.36.0 introduced icon support in navigation. While emojis work, Material Icons provide the polished, professional aesthetic users expect from enterprise applications.
Accessing Material Icons
Browse 2,500+ icons at Google Fonts Material Symbols
Popular Categories:
- Navigation:
home,dashboard,menu,settings - Data:
analytics,trending_up,pie_chart,bar_chart - Actions:
add,delete,edit,save,download - Status:
check_circle,error,warning,info
Implementation: Navigation with Material Icons
import streamlit as st
st.set_page_config(layout="wide", page_title="Enterprise Analytics")
# Define pages with Material icons
sales_dashboard = st.Page(
"sales.py",
title="sales Dashboard",
icon=":material/dashboard:"
)
hr_analytics = st.Page(
"hr.py",
title="HR Analytics",
icon=":material/analytics:"
)
products_reports = st.Page(
"products.py",
title="Customer Reports",
icon=":material/description:"
)
marketing_analytics = st.Page(
"marketing.py",
title="Marketing Management",
icon=":material/groups:"
)
page_settings = st.Page(
"settings.py",
title="Settings",
icon=":material/settings:"
)
# Sidebar navigation
with st.sidebar:
st.image("logo.png", width=180)
st.divider()
pg = st.navigation([
sales_dashboard,
hr_analytics,
products_reports,
marketing_analytics,
page_settings
])
pg.run()
Beyond Navigation: Icons Everywhere
# Buttons with icons
if st.button(":material/download: Download Report"):
st.success("Report downloaded!")
# Headers with icons
st.subheader(":material/trending_up: Revenue Growth")
# Metrics with icons
col1, col2, col3 = st.columns(3)
with col1:
st.metric(":material/people: Active Users", "1,234", "+12%")
with col2:
st.metric(":material/shopping_cart: Orders", "567", "+8%")
with col3:
st.metric(":material/attach_money: Revenue", "$45.6K", "+15%")
# Pills with icons
departments = st.pills(
"Department",
options=[
":material/code: Engineering",
":material/campaign: Marketing",
":material/sell: Sales",
":material/support_agent: Support"
],
selection_mode="multi"
)
# Icon-only buttons (minimalist)
col_a, col_b, col_c, col_d = st.columns(4)
with col_a:
st.button(":material/edit:", key="edit_btn")
with col_b:
st.button(":material/delete:", key="delete_btn")
with col_c:
st.button(":material/share:", key="share_btn")
with col_d:
st.button(":material/download:", key="download_btn")

Output Result:
A modern, icon-rich interface that looks like a professional SaaS application rather than a data science prototype.
Pro Tip: Use filled icons (:material/icon_name:) for active states and outlined versions (:material/icon_name_outlined:) for inactive states.
Icon Naming Convention:
:material/icon_name: # Standard
:material/icon_name_outlined: # Outlined variant
:material/icon_name_rounded: # Rounded variant
:material/icon_name_sharp: # Sharp variant
8. 💊 Using st.pills as an Alternative to st.multiselect
Why This Matters
Visibility drives engagement. Dropdown menus hide options, requiring an extra click. Pills display all choices immediately, reducing interaction cost and improving decision-making speed.
When to Use Pills
Use st.pills when:
- 8 or fewer options
- Options are frequently changed
- Visual scanning is important
- You want a modern, app-like feel
Use st.multiselect when:
- 9+ options
- Options are rarely changed
- Space is limited
- Search functionality is needed
The Psychology
Studies show that visible options are selected 40–60% more often than hidden ones. Pills also create a more tactile, interactive feeling — users feel like they’re “pressing buttons” rather than “filling forms.”
Implementation
import streamlit as st
st.title("Filter Comparison: Multiselect vs Pills")
# Example 1: Traditional multiselect (hidden options)
st.subheader("1. Traditional Multiselect (Dropdown)")
selected_depts_dropdown = st.multiselect(
"Select Departments",
options=["Sales", "Marketing", "Finance", "HR", "Product", "Design", "Legal", "IT"],
help="Click to see options"
)
st.write(f"Selected: {selected_depts_dropdown}")
st.divider()
# Example 2: Pills (all options visible)
st.subheader("2. Pills Widget (All Visible)")
selected_depts_pills = st.pills(
"Select Departments",
options=["Sales", "Marketing", "Finance", "HR", "Product", "Design", "Legal", "IT"],
selection_mode="multi",
help="All options visible at once"
)
st.write(f"Selected: {selected_depts_pills}")
st.divider()
# Example 3: Pills with default selections
st.subheader("3. Pills with Defaults")
selected_with_default = st.pills(
"Select Departments",
options=["Sales", "Marketing", "Finance", "HR", "Product", "Design", "Legal", "IT"],
selection_mode="multi",
default=["Sales", "Marketing"], # Pre-selected
help="Sales and Marketing are selected by default"
)
st.write(f"Selected: {selected_with_default}")
st.divider()
# Example 4: Single selection pills (radio alternative)
st.subheader("4. Single Selection Pills")
priority = st.pills(
"Priority Level",
options=["🔴 Critical", "🟡 High", "🟢 Medium", "🔵 Low"],
selection_mode="single", # Only one can be selected
help="Choose one priority level"
)
st.write(f"Priority: {priority}")
st.divider()
# Real-world example: Dashboard filters
st.subheader("5. Real-World Use Case: Dashboard Filters")
col1, col2 = st.columns(2)
with col1:
time_period = st.pills(
"Time Period",
options=["Today", "Week", "Month", "Quarter", "Year"],
selection_mode="single",
default="Month"
)
with col2:
metric_type = st.pills(
"Metrics",
options=["Revenue", "Users", "Conversion", "Retention"],
selection_mode="multi",
default=["Revenue", "Users"]
)
st.info(f"Showing {metric_type} for {time_period}")
Bonus: Comparing Pills vs Segmented Control
st.subheader("Pills vs Segmented Control")
col_x, col_y = st.columns(2)
with col_x:
st.write("**Pills (More Visual)**")
view_pills = st.pills(
"View",
options=["Grid", "List", "Table"],
selection_mode="single",
label_visibility="collapsed"
)
with col_y:
st.write("**Segmented Control (More Compact)**")
view_segmented = st.segmented_control(
"View",
options=["Grid", "List", "Table"],
selection_mode="single",
label_visibility="collapsed"
)

Output Result:
Pills create an engaging, modern interface where users can see all options at once. The visual contrast between selected (filled) and unselected (outlined) pills provides instant feedback.
Pro Tip: Use default parameter to pre-select common choices. This guides users toward recommended options while still allowing flexibility.
Performance Note: Pills don’t trigger reruns until the user clicks “Apply” if you wrap them in a form, making them perfect for complex filter interfaces.
9. 📐 Building Layouts with Horizontal Flex Containers
Why This Matters
Traditional columns lock you into rigid grids. Flex containers provide the layout flexibility that modern dashboards demand — dynamic alignment, custom gaps, and responsive behavior.
The Revolution (Streamlit 1.48.0)
This is brand new. Horizontal flex containers finally give Streamlit developers the same layout control that web developers have had for years with CSS Flexbox.
Traditional Columns vs Flex Containers
Traditional Columns:
- ❌ Fixed number of elements
- ❌ Always spans full width
- ❌ Limited alignment control
- ✅ Simple and predictable
Flex Containers:
- ✅ Dynamic element count
- ✅ Custom widths and gaps
- ✅ Flexible alignment (left, center, right, space-between)
- ✅ Vertical alignment control
Implementation
import streamlit as st
st.title("Layout Comparison: Columns vs Flex Containers")
# Example 1: Traditional Columns
st.subheader("1. Traditional Columns (Fixed Width)")
col1, col2, col3 = st.columns(3)
with col1:
st.button("Button 1")
with col2:
st.button("Button 2")
with col3:
st.button("Button 3")
st.caption("↑ Columns always span the full width, dividing space equally")
st.divider()
# Example 2: Flex Container with Center Alignment
st.subheader("2. Flex Container (Centered)")
with st.container():
col1, col2, col3 = st.columns([1, 2, 1]) # Center column is wider
with col2:
# Create horizontal container
with st.container(border=False):
# Flex layout: elements only take needed space
btn_col1, btn_col2, btn_col3 = st.columns([1, 1, 1], gap="small")
with btn_col1:
st.button("Left")
with btn_col2:
st.button("Center")
with btn_col3:
st.button("Right")
st.caption("↑ Buttons are centered and only take the space they need")
st.divider()
# Example 3: Flex Container with Custom Gaps
st.subheader("3. Flex Container with Custom Gaps")
# Small gap
st.write("**Small Gap:**")
c1, c2, c3, c4 = st.columns(4, gap="small")
with c1:
st.metric("Metric 1", "100")
with c2:
st.metric("Metric 2", "200")
with c3:
st.metric("Metric 3", "300")
with c4:
st.metric("Metric 4", "400")
# Large gap
st.write("**Large Gap:**")
c1, c2, c3, c4 = st.columns(4, gap="large")
with c1:
st.metric("Metric 1", "100")
with c2:
st.metric("Metric 2", "200")
with c3:
st.metric("Metric 3", "300")
with c4:
st.metric("Metric 4", "400")
st.divider()
# Example 4: Responsive Dashboard Header
st.subheader("4. Real-World: Dashboard Header")
# Logo and actions in one row
header_col1, header_col2 = st.columns([3, 1])
with header_col1:
st.title("📊 Analytics Dashboard")
with header_col2:
# Right-aligned action buttons
action1, action2, action3 = st.columns(3, gap="small")
with action1:
st.button(":material/refresh:", key="refresh")
with action2:
st.button(":material/download:", key="download")
with action3:
st.button(":material/settings:", key="settings")
st.divider()
# Example 5: Complex Layout with Nested Flex
st.subheader("5. Complex Nested Layout")
# Outer container
outer1, outer2 = st.columns([2, 1], gap="medium")
with outer1:
st.markdown("### Main Content")
# Nested flex inside
inner1, inner2 = st.columns(2, gap="small")
with inner1:
st.info("Chart goes here")
with inner2:
st.info("Metrics go here")
with outer2:
st.markdown("### Sidebar")
st.success("Filters go here")
st.warning("Controls go here")
Advanced: Using gap Parameter
# Gap options
st.columns(3, gap="small") # Minimal spacing
st.columns(3, gap="medium") # Default spacing
st.columns(3, gap="large") # Maximum spacing

Output Result:
Flex containers provide professional, responsive layouts with precise control over spacing, alignment, and element positioning — just like modern web applications.
Pro Tip: Use gap="small" for tightly grouped related elements (like button groups) and gap="large" for distinct sections that need visual separation.
When to Use Flex Over Columns:
- Action button groups
- Navigation bars
- Card layouts
- Mixed-width elements
- Responsive designs
10. 📊 Displaying KPIs Like a Pro
Why This Matters
KPIs are the heartbeat of dashboards. They should be instantly scannable, visually distinct, and actionable. The default st.metric is functional but basic—let's elevate it.
Three Approaches to KPIs
- st.metric — Native Streamlit (simple, fast)
- HTML/CSS Cards — Custom design (flexible, branded)
- Plotly Indicators — Advanced visuals (gauges, charts)
Approach 1: Enhanced st.metric
import streamlit as st
st.subheader("Standard Metrics")
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric(
label="Revenue",
value="$125.4K",
delta="+12.5%",
help="Total revenue this month vs last month"
)
with col2:
st.metric(
label="Active Users",
value="1,234",
delta="+8.3%",
delta_color="normal"
)
with col3:
st.metric(
label="Conversion Rate",
value="3.2%",
delta="+0.5%",
delta_color="normal"
)
with col4:
st.metric(
label="Churn Rate",
value="2.1%",
delta="-0.3%",
delta_color="inverse" # Red for decrease is good
)

Approach 2: Custom HTML/CSS Cards
def metric_card(title, value, delta, delta_color="green", icon="📊"):
"""Create a custom metric card with gradient background"""
# Determine delta styling
if delta_color == "green":
delta_bg = "linear-gradient(135deg, #11998e 0%, #38ef7d 100%)"
elif delta_color == "red":
delta_bg = "linear-gradient(135deg, #eb3349 0%, #f45c43 100%)"
else:
delta_bg = "linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
card_html = f"""
<div style='
background: white;
padding: 24px;
border-radius: 16px;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
border-left: 4px solid #667eea;
margin: 8px 0;
'>
<div style='display: flex; justify-content: space-between; align-items: center;'>
<span style='font-size: 48px;'>{icon}</span>
<div style='
background: {delta_bg};
color: white;
padding: 6px 12px;
border-radius: 20px;
font-size: 14px;
font-weight: 600;
'>{delta}</div>
</div>
<h3 style='
color: #888;
font-size: 14px;
font-weight: 500;
margin: 16px 0 8px 0;
text-transform: uppercase;
letter-spacing: 0.5px;
'>{title}</h3>
<p style='
color: #262730;
font-size: 36px;
font-weight: 700;
margin: 0;
'>{value}</p>
</div>
"""
st.markdown(card_html, unsafe_allow_html=True)
# Usage
st.subheader("Custom Metric Cards")
col1, col2, col3, col4 = st.columns(4)
with col1:
metric_card("Revenue", "$125.4K", "+12.5%", "green", "💰")
with col2:
metric_card("Users", "1,234", "+8.3%", "green", "👥")
with col3:
metric_card("Orders", "567", "+15.2%", "green", "🛒")
with col4:
metric_card("Churn", "2.1%", "-0.3%", "red", "📉")

Approach 3: Plotly Indicator Gauge
import plotly.graph_objects as go
def create_gauge(value, title, max_value=100, color="#667eea"):
"""Create a Plotly gauge chart"""
fig = go.Figure(go.Indicator(
mode="gauge+number+delta",
value=value,
title={'text': title, 'font': {'size': 20}},
delta={'reference': max_value * 0.8, 'increasing': {'color': "#38ef7d"}},
gauge={
'axis': {'range': [None, max_value], 'tickwidth': 1},
'bar': {'color': color},
'bgcolor': "white",
'borderwidth': 2,
'bordercolor': "#e0e0e0",
'steps': [
{'range': [0, max_value * 0.6], 'color': '#FFE4E1'},
{'range': [max_value * 0.6, max_value * 0.8], 'color': '#FFE4B5'},
{'range': [max_value * 0.8, max_value], 'color': '#90EE90'}
],
'threshold': {
'line': {'color': "red", 'width': 4},
'thickness': 0.75,
'value': max_value * 0.9
}
}
))
fig.update_layout(
height=250,
margin=dict(l=10, r=10, t=50, b=10),
font={'family': "Arial"}
)
return fig
# Usage
st.subheader("Gauge Charts")
col1, col2, col3 = st.columns(3)
with col1:
st.plotly_chart(
create_gauge(87, "Customer Satisfaction", 100, "#667eea"),
use_container_width=True
)
with col2:
st.plotly_chart(
create_gauge(73, "Server Health", 100, "#11998e"),
use_container_width=True
)
with col3:
st.plotly_chart(
create_gauge(92, "Sales Target", 100, "#eb3349"),
use_container_width=True
)

Advanced: Trend Sparklines in Metrics
import plotly.graph_objects as go
import pandas as pd
import numpy as np
import streamlit as st
def metric_with_sparkline(label, value, delta, data):
"""Metric card with embedded sparkline chart"""
# Create mini sparkline
fig = go.Figure()
fig.add_trace(go.Scatter(
y=data,
mode='lines',
fill='tozeroy',
line=dict(color='#667eea', width=2),
fillcolor='rgba(102, 126, 234, 0.1)'
))
fig.update_layout(
height=80,
margin=dict(l=0, r=0, t=0, b=0),
xaxis=dict(visible=False),
yaxis=dict(visible=False),
showlegend=False,
plot_bgcolor='rgba(0,0,0,0)',
paper_bgcolor='rgba(0,0,0,0)'
)
col_metric, col_chart = st.columns([1, 2])
with col_metric:
st.metric(label, value, delta)
with col_chart:
st.plotly_chart(fig, use_container_width=True, config={'displayModeBar': False})
# Usage
st.subheader("Metrics with Trend Lines")
# Generate sample trend data
revenue_trend = np.random.randn(30).cumsum() + 100
users_trend = np.random.randn(30).cumsum() + 50
col1, col2 = st.columns(2)
with col1:
metric_with_sparkline("Revenue", "$125.4K", "+12.5%", revenue_trend)
with col2:
metric_with_sparkline("Active Users", "1,234", "+8.3%", users_trend)

Output Result:
Professional KPI displays ranging from clean native metrics to branded custom cards to sophisticated gauge charts with thresholds and sparkline trends.
Pro Tip: Use delta_color="inverse" for metrics where decreases are positive (like churn rate, bounce rate, or costs).
When to Use Each:
- st.metric: Quick dashboards, internal tools, fast development
- HTML/CSS Cards: Branded dashboards, client-facing apps, marketing
- Plotly Gauges: Executive dashboards, SLA monitoring, target tracking
🎁 Bonus Tips for Production-Ready Dashboards
1. Performance: Caching Strategies
import streamlit as st
import pandas as pd
import time
# Cache data that doesn't change often
@st.cache_data(ttl=3600) # Cache for 1 hour
def load_data():
"""Load data from database or API"""
time.sleep(2) # Simulate slow load
return pd.read_csv("data.csv")
# Cache expensive resources
@st.cache_resource
def load_model():
"""Load ML model (only once per session)"""
import joblib
return joblib.load("model.pkl")
# Usage
df = load_data() # Only loads once per hour
model = load_model() # Only loads once per server restart
When to Use:
@st.cache_data: DataFrames, lists, dictionaries, API responses@st.cache_resource: ML models, database connections, large objects
2. State Management: Persistent User Selections
# Initialize session state
if 'page_views' not in st.session_state:
st.session_state.page_views = 0
if 'user_selections' not in st.session_state:
st.session_state.user_selections = {}
# Increment counter
st.session_state.page_views += 1
st.write(f"Page views this session: {st.session_state.page_views}")
# Store user preferences
if st.button("Save Preferences"):
st.session_state.user_selections = {
'theme': 'dark',
'language': 'en',
'notifications': True
}
st.success("Preferences saved!")
3. Downloads: Export Data and Reports
import io
from datetime import datetime
# CSV download
csv = df.to_csv(index=False).encode('utf-8')
st.download_button(
label="📥 Download as CSV",
data=csv,
file_name=f'report_{datetime.now().strftime("%Y%m%d")}.csv',
mime='text/csv'
)
# Excel download
buffer = io.BytesIO()
with pd.ExcelWriter(buffer, engine='openpyxl') as writer:
df.to_excel(writer, sheet_name='Data', index=False)
buffer.seek(0)
st.download_button(
label="📥 Download as Excel",
data=buffer,
file_name=f'report_{datetime.now().strftime("%Y%m%d")}.xlsx',
mime='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
# PDF download (requires additional libraries)
# from fpdf import FPDF
# pdf = generate_pdf_report(df)
# st.download_button("📥 Download PDF", pdf, "report.pdf", "application/pdf")
4. Dynamic Updates: Real-time Dashboards
import time
# Create placeholder
placeholder = st.empty()
# Update dynamically
for i in range(100):
# Update the same space
placeholder.metric(
"Live Counter",
f"{i}",
f"+{i-50}%" if i > 50 else f"{i-50}%"
)
time.sleep(0.1)
placeholder.success("Update complete!")
5. Error Handling: User-Friendly Messages
try:
# Risky operation
result = process_data(user_input)
st.success("✅ Operation completed successfully!")
st.dataframe(result)
except FileNotFoundError:
st.error("❌ File not found. Please check the file path.")
st.info("💡 Tip: Make sure the file exists in the data/ directory.")
except ValueError as e:
st.error(f"❌ Invalid data: {str(e)}")
st.warning("⚠️ Please check your input values and try again.")
except Exception as e:
st.error("❌ An unexpected error occurred.")
st.exception(e) # Shows detailed error in expander
st.info("💡 Please contact support if this persists.")
6. Loading States: Better UX During Processing
with st.spinner("🔄 Processing your request..."):
time.sleep(2)
data = complex_operation()
st.success("✅ Processing complete!")
# Or with progress bar
progress_bar = st.progress(0, text="Initializing...")
for i in range(100):
time.sleep(0.01)
progress_bar.progress(i + 1, text=f"Processing... {i+1}%")
progress_bar.empty()
7. Forms: Batch User Input
with st.form("user_form"):
st.subheader("User Registration")
name = st.text_input("Full Name")
email = st.text_input("Email")
age = st.number_input("Age", min_value=18, max_value=100)
department = st.selectbox("Department", ["Sales", "Marketing", "Engineering"])
# Form only submits when button is clicked
submitted = st.form_submit_button("Submit")
if submitted:
st.success(f"Welcome, {name}!")
st.json({
"name": name,
"email": email,
"age": age,
"department": department
})
🎯 Key Takeaways
Building professional Streamlit dashboards is about strategic design choices, not complex code. Here’s your action plan:
Immediate Wins (< 30 minutes)
- ✅ Update to Streamlit 1.48.0+
- ✅ Add tooltips to all widgets (
helpparameter) - ✅ Create a
config.tomlwith your brand colors - ✅ Replace Matplotlib with Plotly charts
Medium Effort (1–2 hours)
- ✅ Implement sidebar organization
- ✅ Add Material Icons to navigation
- ✅ Use
st.pillsfor visible filters - ✅ Create custom metric cards
Advanced Polish (2–4 hours)
- ✅ Build custom CSS theme in
styles.css - ✅ Design and add a logo
- ✅ Implement flex containers for complex layouts
- ✅ Add caching and state management
📚 Essential Resources
Official Documentation
- Streamlit Docs: https://docs.streamlit.io
- API Reference: https://docs.streamlit.io/library/api-reference
- Cheat Sheet: https://docs.streamlit.io/library/cheatsheet
- config.toml Guide: https://docs.streamlit.io/library/advanced-features/configuration
Design Resources
- Material Icons: https://fonts.google.com/icons
- CSS Gradients: https://cssgradient.io
- Color Palettes: https://coolors.co
- Google Fonts: https://fonts.google.com
Visualization Libraries
- Plotly Python: https://plotly.com/python/
- Plotly Express: https://plotly.com/python/plotly-express/
- Altair: https://altair-viz.github.io
- Bokeh: https://docs.bokeh.org
Community & Learning
- Streamlit Forum: https://discuss.streamlit.io
- Streamlit Gallery: https://streamlit.io/gallery
- Streamlit Components: https://streamlit.io/components
- GitHub Examples: https://github.com/streamlit/streamlit/tree/develop/examples
Deployment
- Streamlit Cloud: https://streamlit.io/cloud
- Docker Deployment: https://docs.streamlit.io/knowledge-base/tutorials/deploy/docker
- AWS/GCP/Azure Guides: https://docs.streamlit.io/knowledge-base/tutorials/deploy
💭 Final Thoughts
The best Streamlit apps don’t look like Streamlit apps.
They look like products. They feel intentional. They solve real problems with thoughtful design.
You now have the tools:
- Tooltips for clean interfaces
- Plotly for interactive insights
- Sidebars for organization
- Logos for branding
- Themes for consistency
- Custom CSS for uniqueness
- Material Icons for professionalism
- Pills for better UX
- Flex containers for flexibility
- KPI cards for impact
메타데이터
- post_id
- 1465e16bc4bf
- slug
- 10-essential-streamlit-design-tips-building-professional-dashboards-that-dont-look-like-streamlit-1465e16bc4bf
- url
- https://medium.com/@mihirs202/10-essential-streamlit-design-tips-building-professional-dashboards-that-dont-look-like-streamlit-1465e16bc4bf
- canonical_url
- https://medium.com/@mihirs202/10-essential-streamlit-design-tips-building-professional-dashboards-that-dont-look-like-streamlit-1465e16bc4bf
- author_url
- https://medium.com/@mihirs202
- status
- ok
- fetched_at
- 2026-08-17 20:08:37