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.
Gradio Theme Building
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.
Core Principles
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
_darkvariable should be intentionally chosen for contrast and readability on dark backgrounds. Specifically:- Reduce font weight slightly on dark (350 instead of 400) — light text on dark reads heavier
- Desaturate accents — high chroma at high lightness looks garish
- Use lighter surfaces for elevation, not heavier shadows
- Never use pure black
#000— use#0a0a14-ish dark with a subtle hue cast
Commit 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.
Aesthetic Quality
Technical correctness isn't enough. A theme can have every variable set perfectly and still look generic. Apply these checks:
The "AI Slop" Test
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?"
Palette traps to avoid
- Cyan accents on near-black backgrounds — the default "AI cyberpunk" look
- Purple-to-blue gradients — overused and dated
- Neon glow on dark mode — looks "cool" without requiring real design decisions
- Gradient text on headings/metrics — decorative, not meaningful
- Glassmorphism everywhere — backdrop-blur as decoration rather than purpose
- Pure black (
#000) or pure white (#fff) — don't exist in nature; tint everything (even chroma 0.005-0.01 reads as natural) - Untinted neutrals (
colors.gray,colors.zincstraight) — neutrals should hint at the brand hue for subconscious cohesion. Usecolors.slateif your accent is cool,colors.stoneif warm. - Heavy alpha use (
rgba(...)everywhere) — usually means an incomplete palette. Define explicit overlay colours per context. Acceptable for focus rings and frosted glass; suspect everywhere else. - Gray text on coloured backgrounds — washes out. Use a darker shade of the background colour instead.
Typography traps to avoid
- Inter, Roboto, Open Sans, Lato, Montserrat — these are the invisible defaults that signal "AI-generated." Fine for utility themes; lethal for distinctive ones.
- Better Google Font alternatives: Instrument Sans, Plus Jakarta Sans, Outfit, Onest, Figtree, DM Sans, Source Sans 3 (sans); Fraunces, Newsreader, Lora (serif/editorial); Chakra Petch, Space Grotesk, JetBrains Mono (technical)
- Monospace as lazy "technical" shorthand — only use mono when it actually communicates something
- Too many font sizes too close together (12, 13, 14, 15, 16) — creates muddy hierarchy. Use fewer sizes with more contrast (1.25–1.5× ratio).
Visual detail traps to avoid
- Generic drop shadows (
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. - Identical card grids — every block looking the same shape and weight creates visual monotony.
- Uniform spacing — vary tight groupings with generous separations to create rhythm.
Hierarchy through multiple dimensions
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_*.
Architecture
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).
Skeleton
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.
Building Blocks
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.
Variable Reference System
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.
Non-obvious variables worth knowing
block_label_*(media element titles like "Image", "Audio") vsblock_title_*(form element titles like a Textbox label) — these are different and need to be styled together.body_background_fillpaints the actual page<body>(full viewport), not just the gradio container. To make the container itself transparent, setbackground_fill_primary="transparent".button_transform_hover/_active— fortranslateY(-2px)"lift" effects. Pair withbutton_*_shadow_hoverfor 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 fromcheckbox_*(the box itself).stat_background_fillaccepts gradients — useful for confidence bars.
Custom CSS
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"]).
Critical gotcha: Shadow DOM scope
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(...) }"
Custom slider thumbs
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;
}
Gradio DOM selectors
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.
Building from a Reference Image
When matching a screenshot:
- Extract: background (solid/gradient/texture, exact colours), card style (border, radius, shadow), text weight/colour, accent hue, font feel, distinctive elements.
- Map: bg →
body_background_fill; cards →block_*; buttons →button_*+custom_cssfor complex gradients/glows; accent → customColor()if no palette matches. - Build order: background → blocks → buttons → inputs/labels → details (slider thumbs, focus rings).
- Pitfalls: large border-radius + Gradio's
overflow: hiddenclips content (cap at ~20px); complex multi-stop button gradients needcustom_csswith!important; backdrop-filter doesn't work in Firefox by default.
Pre-Shipping Checklist
self.nameset in__init__- Text contrast audit (do this first):
- Body text on body/block backgrounds
- Label text on coloured label fills (contrast against the fill, not the page)
- Button text on button fills (all 3 variants: primary, secondary, cancel)
- Placeholder text — visible but distinct from entered text (≥ #999 on white)
- Selected checkbox/radio text on selected fill
- Error text on error background
- Link text on body background
- Light mode: body, blocks, inputs, buttons, labels, checkboxes, tables
- Dark mode: same elements, independently designed (not auto-inverted)
- Focus, hover, active, selected states all 3 button variants
- Aesthetic Quality pass: AI slop test, no palette/typography traps, hierarchy works at squint distance
- Font weights all explicitly loaded via
weights=(...) - Test with
gr.themes.builder()for interactive preview
Publishing
theme.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(...).
Registering a Built-in Theme
- Create
gradio/themes/my_theme.py - Add
from gradio.themes.my_theme import MyThemetogradio/themes/__init__.pyand add"MyTheme"to__all__ - Set
self.name = "my_theme"in__init__
Reference: Exemplar Theme Files
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 |