with one click
gradio-themes
// Build and customise Gradio themes. Use when creating, editing, or publishing Python-based Gradio themes that control colours, typography, spacing, shadows, and dark mode.
// Build and customise Gradio themes. Use when creating, editing, or publishing Python-based Gradio themes that control colours, typography, spacing, shadows, and dark mode.
| name | gradio-themes |
| description | Build and customise Gradio themes. Use when creating, editing, or publishing Python-based Gradio themes that control colours, typography, spacing, shadows, and dark mode. |
You are an expert at building Gradio themes. Themes control the entire visual identity of a Gradio app — colours, typography, spacing, shadows, and dark mode — through Python classes that compile to CSS custom properties.
Text contrast is non-negotiable. Every text element must be readable against its actual background — body text, label text on coloured label fills, button text on button fills, placeholder text, selected checkbox text, error text, link text. A beautiful theme that can't be read is useless. Audit every text/background pair before shipping.
Dark mode is independently designed. Never auto-invert light values. Every _dark variable should be intentionally chosen for contrast and readability on dark backgrounds. Specifically:
#000 — use #0a0a14-ish dark with a subtle hue castCommit to an aesthetic direction. Bold maximalism and refined minimalism both work — what fails is half-commitment. Pick one tone (editorial, brutal, glass, retro, organic, playful, industrial...) and execute every variable in service of it.
Use variable references (*name) for consistency. When one value should track another, reference it. This keeps themes maintainable and lets users customise constructor params (hues, sizes) and have changes cascade.
Test both modes. Always verify light AND dark for body, blocks, inputs, buttons, labels, checkboxes, tables, focus states, hover states, selected states.
Technical correctness isn't enough. A theme can have every variable set perfectly and still look generic. Apply these checks:
If you showed this theme to someone and said "AI made this," would they immediately believe you? If yes, that's the problem. A distinctive theme makes someone ask "how was this made?" not "which AI made this?"
#000) or pure white (#fff) — don't exist in nature; tint everything (even chroma 0.005-0.01 reads as natural)colors.gray, colors.zinc straight) — neutrals should hint at the brand hue for subconscious cohesion. Use colors.slate if your accent is cool, colors.stone if warm.rgba(...) everywhere) — usually means an incomplete palette. Define explicit overlay colours per context. Acceptable for focus rings and frosted glass; suspect everywhere else.0 2px 4px rgba(0,0,0,0.1)) — safe, forgettable. If you can clearly see the shadow, it's too strong. Either commit to bold shadows or none.Hierarchy is strongest when 2–3 of {size, weight, colour, position, space} change at once. A bigger label alone is weak; bigger + bolder + more space above is strong. Apply this to block_label_* vs block_title_* vs section_header_*.
Themes inherit from gradio.themes.Base, which defines 300+ CSS variables with defaults. Your theme overrides specific variables via super().set().
Flow: Python class → _get_theme_css() → CSS :root { --var: val; } → served at /theme.css → consumed by Svelte components via var(--name).
Theme CSS is injected into the <gradio-app> shadow DOM. The page's <body> is styled separately via body_background_fill (applied as body { background: var(--body-background-fill) } in the layout).
from __future__ import annotations
from collections.abc import Iterable
from gradio.themes.base import Base
from gradio.themes.utils import colors, fonts, sizes
class MyTheme(Base):
def __init__(
self,
*,
primary_hue: colors.Color | str = colors.blue,
secondary_hue: colors.Color | str = colors.violet,
neutral_hue: colors.Color | str = colors.slate,
spacing_size: sizes.Size | str = sizes.spacing_md,
radius_size: sizes.Size | str = sizes.radius_md,
text_size: sizes.Size | str = sizes.text_md,
font: fonts.Font | str | Iterable[fonts.Font | str] = (
fonts.GoogleFont("Instrument Sans", weights=(400, 500, 600, 700)),
"ui-sans-serif", "system-ui", "sans-serif",
),
font_mono: fonts.Font | str | Iterable[fonts.Font | str] = (
fonts.GoogleFont("JetBrains Mono"),
"ui-monospace", "Consolas", "monospace",
),
):
super().__init__(
primary_hue=primary_hue, secondary_hue=secondary_hue,
neutral_hue=neutral_hue, spacing_size=spacing_size,
radius_size=radius_size, text_size=text_size,
font=font, font_mono=font_mono,
)
self.name = "my_theme"
super().set(
# Override variables here
)
Always specify weights=(...) on GoogleFont if using anything outside the default (400, 600). Browsers will fake-bold missing weights, which looks awful.
Colour palettes — 22 named palettes, 11 shades each (c50 lightest → c950 darkest):
slate, gray, zinc, stone, neutral, red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose.
from gradio.themes.utils import colors
colors.blue.c500 # "#3b82f6"
f"{colors.violet.c800}60" # alpha hex (37.5% opacity)
Sizes — 7 scales each (xxs–xxl):
radius_none, radius_sm, radius_md, radius_lg, radius_xxl; spacing_sm/md/lg; text_sm/md/lg. Custom sizes via Size(xxs=..., xs=..., ...).
Fonts — GoogleFont(name, weights=(...)), LocalFont(name), plain strings for system fonts. Always include fallbacks in tuples.
Use *variable_name to reference other theme variables. References resolve recursively at CSS generation time.
input_shadow="*shadow_drop"
button_cancel_text_color="*button_secondary_text_color"
Dark mode references resolve automatically. Do not append _dark:
input_shadow_focus_dark="0 0 0 3px *primary_900" # CORRECT
input_shadow_focus_dark="0 0 0 3px *primary_900_dark" # WRONG — error
Variables accept any CSS value — colours, gradients, shadows, transforms, transitions, none, calc(), spacing tokens (*spacing_md).
Full variable list is in gradio/themes/base.py — search by name. Every variable accepts an optional _dark suffix.
block_label_* (media element titles like "Image", "Audio") vs block_title_* (form element titles like a Textbox label) — these are different and need to be styled together.body_background_fill paints the actual page <body> (full viewport), not just the gradio container. To make the container itself transparent, set background_fill_primary="transparent".button_transform_hover/_active — for translateY(-2px) "lift" effects. Pair with button_*_shadow_hover for proper depth.button_{size}_* (large/small) controls padding/radius/text size per size; button_{variant}_* (primary/secondary/cancel) controls colours/shadows per variant.checkbox_label_* is the surrounding pill button, separate from checkbox_* (the box itself).stat_background_fill accepts gradients — useful for confidence bars.Themes can bundle arbitrary CSS via self.custom_css (set in __init__). It's injected alongside the variables and ships with the theme when published to the Hub.
class MyTheme(Base):
def __init__(self, ...):
super().__init__(...)
self.name = "my_theme"
self.custom_css = """
...
"""
super().set(...)
Use custom_css for things variables can't express: backdrop-filter, tiling background images, custom slider thumbs, pseudo-element decorations, targeting specific Gradio DOM (.label-wrap, button.secondary, .reset-button, input[type="range"]).
Theme CSS is injected inside the <gradio-app> shadow DOM. Selectors targeting html or body will not work — those elements live in the light DOM (the actual page document).
To paint the page background, use the body_background_fill variable (Gradio applies it to the real <body> from +layout.svelte). Do not try to style body from custom_css.
# CORRECT — paints actual <body>, full viewport
body_background_fill="linear-gradient(...)"
# WRONG — selector doesn't resolve, gradient never paints
self.custom_css = "body { background: linear-gradient(...) }"
Must include both webkit and moz prefixes; need !important to beat Gradio's defaults:
input[type="range"]::-webkit-slider-thumb,
input[type="range"]::-moz-range-thumb {
appearance: none !important;
width: 30px !important;
height: 30px !important;
background: url("data:image/png;base64,...") no-repeat center / contain !important;
background-color: transparent !important;
border: none !important;
box-shadow: none !important;
}
Generic class names (no Svelte hashes on these): .gradio-container, .block, .panel, .form, .wrap, .label-wrap, button.primary, button.secondary, .reset-button, input[type="range"]. Dark mode: .dark .xxx. Inspect the live DOM to find anything else — class names with hashes can change between versions.
When matching a screenshot:
body_background_fill; cards → block_*; buttons → button_* + custom_css for complex gradients/glows; accent → custom Color() if no palette matches.overflow: hidden clips content (cap at ~20px); complex multi-stop button gradients need custom_css with !important; backdrop-filter doesn't work in Firefox by default.self.name set in __init__weights=(...)gr.themes.builder() for interactive previewtheme.push_to_hub(
repo_name="my-theme",
org_name="my-org",
version="0.0.1",
description="A bold theme for data dashboards.",
)
# Load
theme = gr.themes.Theme.from_hub("my-org/my-theme@1.2.0")
custom_css is bundled automatically. Save/load locally via theme.dump("my_theme.json") / Theme.load(...).
gradio/themes/my_theme.pyfrom gradio.themes.my_theme import MyTheme to gradio/themes/__init__.py and add "MyTheme" to __all__self.name = "my_theme" in __init__Read these directly for concrete patterns. Do not re-implement what exists:
| Theme | Style | Techniques to study |
|---|---|---|
gradio/themes/soft.py | Minimal, soft | Shadow-based depth, no block borders, rounded labels |
gradio/themes/cyberpunk.py | Bold, neon | Custom hex dark backgrounds, neon glow shadows, alpha colours |
gradio/themes/neon.py | Playful, raised | Bottom-edge shadows, transform hover/active, pill shapes |
gradio/themes/ember.py | Warm, polished | Comprehensive coverage, focus ring shadows |
gradio/themes/ocean.py | Gradient, fluid | CSS gradients in buttons + checkbox labels, scale transforms |
gradio/themes/glass.py | Editorial, subtle | Gradient fills on inputs/buttons, system fonts |
gradio/themes/monochrome.py | Sharp, no colour | All neutral hues, serif font, sharp radius, thick borders |
gradio/themes/default.py | Balanced, standard | Orange+blue dual hue, stat gradients, error colours |