home / skills / streamlit / agent-skills / creating-streamlit-themes

This skill helps you craft professional, brand-aligned Streamlit themes by configuring config.toml, typography, colors, and sidebar styles.

npx playbooks add skill streamlit/agent-skills --skill creating-streamlit-themes

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
13.3 KB
---
name: creating-streamlit-themes
description: Creating and customizing Streamlit themes. Use when changing app colors, fonts, or appearance, or aligning apps to brand guidelines. Covers config.toml configuration, design principles, and CSS avoidance.
license: Apache-2.0
---

# Creating Streamlit themes

Build professional, brand-aligned themes using `.streamlit/config.toml`. This skill covers design principles and complete configuration for polished, cohesive themes.

## Theme file setup

Theme options go in Streamlit's `config.toml` under the `[theme]` section:

## Theme inheritance

Start from a built-in theme or external file:

```toml
[theme]
base = "light"                         # or "dark"
# base = "./my-base-theme.toml"        # Local file
# base = "https://example.com/theme.toml"  # Remote URL
```

When using `base`, you only need to override the values you want to change. Theme files referenced via `base` can only contain a single `[theme]` section—`[theme.light]` and `[theme.dark]` variants are not supported in external theme files.

## Color configuration

### Theme colors

```toml
[theme]
primaryColor = "#0969da"           # Buttons, links, active elements
backgroundColor = "#ffffff"        # Main content background
secondaryBackgroundColor = "#f6f8fa"  # Widget backgrounds, code blocks
textColor = "#1F2328"              # Body text

# Optional refinements
linkColor = "#0969da"              # Markdown links (defaults to primaryColor)
codeTextColor = "#1F2328"          # Inline code text
codeBackgroundColor = "#f6f8fa"    # Code block background
borderColor = "#d0d7de"            # Widget borders
```

**Design principle:** Choose a `primaryColor` dark enough to contrast with white text. Streamlit renders the text of primary buttons white against the primary color.

### Color palette

Define semantic colors for status indicators, markdown text coloring, and sparklines:

```toml
[theme]
redColor = "#cf222e"
orangeColor = "#bf8700"
yellowColor = "#dbab09"
greenColor = "#1a7f37"
blueColor = "#0969da"
violetColor = "#8250df"
grayColor = "#57606a"
```

Each color supports background and text variants (auto-derived if not set):

```toml
[theme]
greenColor = "#1a7f37"
greenBackgroundColor = "#dafbe1"   # Light tint for badges
greenTextColor = "#116329"         # Darkened for readability
```

### Chart colors

Define colors for Plotly, Altair, and Vega-Lite charts:

```toml
[theme]
# Categorical data (bars, pie slices, series)
chartCategoricalColors = ["#0969da", "#1a7f37", "#bf3989", "#8250df", "#cf222e", "#bf8700", "#57606a"]

# Sequential/gradient data (heatmaps) - exactly 10 colors required
chartSequentialColors = ["#f0f6fc", "#c8e1ff", "#79c0ff", "#58a6ff", "#388bfd", "#1f6feb", "#1158c7", "#0d419d", "#0a3069", "#04244a"]
```

### Dataframe styling

```toml
[theme]
dataframeBorderColor = "#d0d7de"
dataframeHeaderBackgroundColor = "#f6f8fa"
```

Ensure `textColor` is readable against `dataframeHeaderBackgroundColor`—headers use the main text color.

## Typography

### Font families

Use built-in fonts, load from Google Fonts, or define custom fonts from font files (see below):

```toml
[theme]
# Built-in options
font = "sans-serif"  # or "serif" or "monospace"

# Google Fonts
font = "Inter:https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"

# Font with spaces in name
font = "'IBM Plex Sans':https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&display=swap"
```

### Self-hosting custom fonts

Use `[[theme.fontFaces]]` tables to load fonts via Streamlit's static file serving. Font files must be placed in a `static/` directory and served through the app—they cannot be arbitrary local file paths.

**Before adding fonts to config.toml:** Verify the font files exist in the static directory.

```toml
[[theme.fontFaces]]
family = "CustomFont"
url = "app/static/CustomFont-Regular.woff2"
weight = 400

[[theme.fontFaces]]
family = "CustomFont"
url = "app/static/CustomFont-Bold.woff2"
weight = 700

[theme]
font = "CustomFont"
```

**Attributes:** `family` (name), `url` (path to OTF/TTF/WOFF/WOFF2), `weight` (400, "200 800", or "bold"), `style` ("normal"/"italic"/"oblique"), `unicodeRange` (e.g., "U+0000-00FF").

Changes to `fontFaces` require a server restart.

### Heading and code fonts

```toml
[theme]
headingFont = "Inter:https://fonts.googleapis.com/css2?family=Inter:wght@600;700&display=swap"
codeFont = "'JetBrains Mono':https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap"
```

### Font sizing and weight

```toml
[theme]
baseFontSize = 14                  # Root size in pixels (default: 16)
baseFontWeight = 400               # Normal weight
codeFontSize = "0.875rem"          # Relative to base, or use "13px"
codeFontWeight = 400

# Heading hierarchy (h1 through h6), or use a single value for all
headingFontSizes = ["32px", "24px", "20px", "16px", "14px", "12px"]
headingFontWeights = [600, 600, 600, 500, 500, 500]
```

### Link styling

```toml
[theme]
linkUnderline = false              # Remove underlines for cleaner look
```

## Border and radius

```toml
[theme]
baseRadius = "8px"                 # All components (none/small/medium/large/full/px/rem)
buttonRadius = "8px"               # Buttons specifically (defaults to baseRadius)
showWidgetBorder = true            # Show borders on unfocused widgets
showSidebarBorder = true           # Show divider between sidebar and content
```

**Radius keywords:** `"none"` (0), `"small"` (4px), `"medium"` (8px), `"large"` (12px), `"full"` (pill shape).

## Sidebar customization

Style the sidebar independently:

```toml
[theme.sidebar]
backgroundColor = "#f6f8fa"
secondaryBackgroundColor = "#eaeef2"
codeBackgroundColor = "#eaeef2"
textColor = "#1F2328"
borderColor = "#d0d7de"
primaryColor = "#0969da"           # Active elements in sidebar
```

## Light and dark modes

Define separate themes for each mode:

```toml
[theme.light]
primaryColor = "#0969da"
backgroundColor = "#ffffff"
secondaryBackgroundColor = "#f6f8fa"
textColor = "#1F2328"

[theme.dark]
primaryColor = "#58a6ff"
backgroundColor = "#0d1117"
secondaryBackgroundColor = "#161b22"
textColor = "#e6edf3"

[theme.light.sidebar]
backgroundColor = "#f6f8fa"

[theme.dark.sidebar]
backgroundColor = "#010409"
```

Users can switch between modes in the app settings menu only if both `[theme.light]` and `[theme.dark]` are defined. A custom theme with just `[theme]` locks the app to a single mode.

## Detecting current theme

Use `st.context.theme.base` to adapt your app to the active theme. Useful for:

- Adjusting specific chart colors for better contrast
- Swapping logos or images (e.g., dark logo on light, light logo on dark)
- Styling third-party components that don't auto-adapt
- Applying conditional CSS or custom styling

```python
if st.context.theme.base == "dark":
    # Do something for dark mode
```

## Design principles

### Color contrast

Ensure WCAG AA compliance (4.5:1 ratio for text):
- Light themes: Dark text (#1F2328) on light backgrounds (#ffffff)
- Dark themes: Light text (#e6edf3) on dark backgrounds (#0d1117)
- Primary colors must contrast with white button text

### Color harmony

Build cohesive palettes using these approaches:

**Monochromatic:** Single hue with varying lightness (e.g., shadcn's zinc grays)
```toml
primaryColor = "#18181B"
textColor = "#09090B"
borderColor = "#E4E4E7"
grayColor = "#71717A"
```

**Brand accent:** Neutral base with one brand color (e.g., Stripe's purple)
```toml
primaryColor = "#635bff"           # Brand purple
backgroundColor = "#ffffff"
textColor = "#425466"              # Neutral gray
```

**Complementary:** Brand primary with supporting accent colors
```toml
primaryColor = "#29B5E8"           # Brand blue (Snowflake)
textColor = "#11567F"              # Darker blue for text
greenColor = "#36B37E"             # Success states
redColor = "#DE350B"               # Error states
```

### Typography guidelines

- **Body text:** 14-16px, weight 400
- **Headings:** Decreasing scale from h1 (28-40px) to h6 (12-14px)
- **Code:** Monospace font, slightly smaller than body (0.85-0.875rem)
- **Font pairing:** Use the same font for body and headings for consistency, or pair complementary fonts (e.g., serif headings with sans-serif body). Code should always use a distinct monospace font.

### Visual hierarchy

Create depth with background layers:
```
Main content:        #ffffff (lightest)
Secondary elements:  #f6f8fa (slightly darker)
Sidebar:             #f6f8fa or contrasting brand color
Code blocks:         #f6f8fa (matches secondary or distinct)
```

## Example: Snowflake brand theme

Clean, professional theme with brand blue accents:

```toml
[theme]
primaryColor = "#29B5E8"
backgroundColor = "#ffffff"
secondaryBackgroundColor = "#f4f9fc"
codeBackgroundColor = "#e8f4f8"
textColor = "#11567F"
linkColor = "#29B5E8"
borderColor = "#d0e8f2"
showWidgetBorder = true
showSidebarBorder = true
baseRadius = "8px"
buttonRadius = "8px"

font = "'Inter':https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
codeFont = "'JetBrains Mono':https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap"
codeFontSize = "13px"
codeTextColor = "#11567F"
baseFontSize = 14
baseFontWeight = 400
headingFontSizes = ["32px", "24px", "20px", "16px", "14px", "12px"]
headingFontWeights = [600, 600, 600, 500, 500, 500]
linkUnderline = false

chartCategoricalColors = ["#29B5E8", "#11567F", "#71C8E5", "#174D6A", "#A5DDF2", "#0E4D6B", "#52B8D9"]

blueColor = "#29B5E8"
greenColor = "#36B37E"
yellowColor = "#FFAB00"
redColor = "#DE350B"
violetColor = "#6554C0"

dataframeBorderColor = "#d0e8f2"
dataframeHeaderBackgroundColor = "#e8f4f8"

[theme.sidebar]
backgroundColor = "#11567F"
secondaryBackgroundColor = "#174D6A"
codeBackgroundColor = "#0E4D6B"
textColor = "#ffffff"
borderColor = "#1E6D94"
```

## Example: VS Code dark theme

Developer-focused dark theme with syntax-inspired colors:

```toml
[theme]
base = "dark"
primaryColor = "#0078d4"
backgroundColor = "#1e1e1e"
secondaryBackgroundColor = "#252526"
codeBackgroundColor = "#1e1e1e"
textColor = "#cccccc"
linkColor = "#3794ff"
borderColor = "#3c3c3c"
showWidgetBorder = true
showSidebarBorder = true
baseRadius = "4px"
buttonRadius = "4px"

font = "'Segoe UI', 'Open Sans':https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700&display=swap"
codeFont = "'Fira Code':https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500&display=swap"
codeFontSize = "13px"
codeTextColor = "#d4d4d4"
baseFontSize = 14
baseFontWeight = 400
headingFontSizes = ["28px", "22px", "18px", "16px", "14px", "12px"]
headingFontWeights = [600, 600, 600, 600, 600, 600]
linkUnderline = false

chartCategoricalColors = ["#0078d4", "#4ec9b0", "#dcdcaa", "#ce9178", "#c586c0", "#569cd6", "#6a9955"]

blueColor = "#569cd6"
greenColor = "#6a9955"
yellowColor = "#dcdcaa"
orangeColor = "#ce9178"
violetColor = "#c586c0"

[theme.sidebar]
backgroundColor = "#252526"
secondaryBackgroundColor = "#333333"
codeBackgroundColor = "#1e1e1e"
borderColor = "#3c3c3c"
```

## Common mistakes

### Primary color too light

```toml
# BAD: White text on yellow is unreadable
primaryColor = "#FFEB3B"

# GOOD: Use a darker shade
primaryColor = "#F59E0B"
```

### Insufficient contrast

```toml
# BAD: Light gray text on white
textColor = "#CCCCCC"
backgroundColor = "#FFFFFF"

# GOOD: Dark text on light background
textColor = "#1F2328"
backgroundColor = "#FFFFFF"
```

### Mismatched backgrounds

```toml
# BAD: Secondary lighter than primary
backgroundColor = "#f6f8fa"
secondaryBackgroundColor = "#ffffff"

# GOOD: Secondary should be darker/distinct
backgroundColor = "#ffffff"
secondaryBackgroundColor = "#f6f8fa"
```

### Forgetting sidebar contrast

When using a dark sidebar with a light main section, adjust all sidebar colors—not just `textColor`:

```toml
# BAD: Only changed backgroundColor
[theme.sidebar]
backgroundColor = "#11567F"

# GOOD: Adjust all colors for dark sidebar
[theme.sidebar]
backgroundColor = "#11567F"
secondaryBackgroundColor = "#174D6A"
textColor = "#ffffff"
borderColor = "#1E6D94"
...
```

## Avoid custom CSS

Custom CSS breaks with Streamlit updates and is hard to maintain:

```python
# BAD: Will break with updates
st.markdown("""
<style>
.stButton button { background-color: #FF4B4B; }
</style>
""", unsafe_allow_html=True)

# GOOD: Use config.toml instead
```

If you must use CSS, use `key=` to create targetable classes:

```python
st.button("Submit", key="submit")
# Generates: .st-key-submit

st.html("""<style>.st-key-submit button { width: 100%; }</style>""")
```

**Only use CSS as a last resort.**

## Development workflow

Most theme options update live after saving `config.toml` and rerunning. Font-related options (`fontFaces`) require a server restart.

Test your theme with: buttons (primary contrast), forms (borders, focus), dataframes (headers), code blocks, charts, and sidebar.

## Related skills

- [improving-streamlit-design](../improving-streamlit-design/SKILL.md) - Visual polish with icons, badges, spacing

## References

- [Theming overview](https://docs.streamlit.io/develop/concepts/configuration/theming)
- [Colors and borders](https://docs.streamlit.io/develop/concepts/configuration/theming-customize-colors-and-borders)
- [Fonts](https://docs.streamlit.io/develop/concepts/configuration/theming-customize-fonts)
- [config.toml reference](https://docs.streamlit.io/develop/api-reference/configuration/config.toml)
- [st.context](https://docs.streamlit.io/develop/api-reference/caching-and-state/st.context)

Overview

This skill teaches how to create and customize Streamlit themes using .streamlit/config.toml to produce polished, brand-aligned apps. It covers color palettes, typography, chart and dataframe styling, sidebar variants, light/dark mode, and recommended design principles. Practical examples and common mistakes help you build accessible, maintainable themes without relying on brittle CSS.

How this skill works

You author theme settings inside the [theme] section of config.toml (or define [theme.light] and [theme.dark] variants). The theme controls primary colors, background layers, semantic status colors, chart palettes, fonts, radii, and sidebar appearance. The app reads these values at runtime; font file changes require a server restart and the st.context.theme.base value lets your code adapt to the active mode.

When to use it

  • Align app appearance with brand guidelines
  • Change colors, fonts, or component radii across an app
  • Provide separate light and dark variants that users can switch between
  • Standardize chart palettes and dataframe styling consistently
  • Avoid ad-hoc CSS hacks that break with Streamlit updates

Best practices

  • Set primaryColor dark enough for white button text to remain readable
  • Ensure WCAG AA contrast for body and heading text against backgrounds
  • Prefer base inheritance: start from a built-in theme and override only what changes
  • Self-host custom fonts via static/ and declare [[theme.fontFaces]]; restart server after changes
  • Define both [theme.light] and [theme.dark] to enable user mode switching; otherwise the app is locked
  • Test buttons, forms, dataframes, charts, and sidebar in both modes before release

Example use cases

  • Create a Snowflake-like brand theme with blue accents and white content background
  • Build a developer VS Code dark theme with syntax-style chart palettes and compact radii
  • Self-host a custom corporate font family for consistent headings and body text across apps
  • Provide light/dark logo swapping in code using st.context.theme.base
  • Define chartCategoricalColors and chartSequentialColors to ensure consistent visuals in Plotly/Altair charts

FAQ

Can I use arbitrary local font file paths in config.toml?

No. Font files must be served by the app (for example placed under static/) and referenced via a URL accessible to Streamlit; then declare them with [[theme.fontFaces]].

Will theme changes update immediately?

Most config.toml theme changes apply live after saving and rerunning the app. Font face changes require restarting the Streamlit server.