7.1 KiB
Gradio for Spaces
Patterns and quirks specific to running Gradio inside a Space. Assumes you're already comfortable with stock Gradio components and gr.Blocks / gr.Interface.
For deeper Gradio API guidance — components, layouts, event listeners, chatbots, the Gradio 5→6 migration — use the dedicated huggingface-gradio skill. Install it with hf skills add huggingface-gradio (add --claude --global to also install for Claude Code, user-level).
For ZeroGPU-specific decorator + worker semantics, see zerogpu.md.
Themes and layout
- Default theme preference:
gr.themes.Soft(). Alternatives: no theme, orgr.themes.Citrus(). Pick once and don't over-style. - For apps that don't need full-width, constrain with CSS so they're readable on 4K displays:
The dark-mode override fixes a recurring Gradio bug where dark text inherits unset colors.
CSS = """ #col-container { max-width: 1100px; margin: 0 auto; } .dark .gradio-container { color: var(--body-text-color); } """ with gr.Blocks(theme=gr.themes.Soft(), css=CSS) as demo: ... - Width-cap with
!importantif Gradio 6's own breakpoints fight you — targetmain,.gradio-container, and the inner fillable wrapper, otherwise the width caps but goes flush-left.
Minimal layout most demos converge to
with gr.Row():
prompt = gr.Textbox(show_label=False, placeholder="…", container=False, scale=4)
run = gr.Button("Run", variant="primary", scale=1)
output = gr.Image(...)
with gr.Accordion("Advanced settings", open=False):
...
container=False on the inline Textbox removes the default outer border for a tighter look.
gr.Markdown for the intro
Be succinct. Title, one-line description, a links section. Don't over-explain how it works — that's what the model card is for.
gr.Examples
The pattern that doesn't trip people up:
gr.Examples(
examples=[
["a cat sitting on a windowsill", 42],
["mountains at sunset, photorealistic", 7],
],
inputs=[prompt, seed],
outputs=output,
fn=generate,
cache_examples=True,
cache_mode="lazy",
)
Why these flags:
cache_examples=Truemakes example clicks instant (no re-inference per visitor).cache_mode="lazy"— caches on first user click for each example. Required on ZeroGPU (Gradio's default on ZeroGPU is already lazy viaGRADIO_CACHE_MODE=lazy). Eager would pre-run every example at app startup, but ZeroGPU has no GPU attached at startup — it'd fail and burn the creator's daily quota.cache_examples=Truesilently disablesrun_on_click/run_examples_on_click. If your app relies on click-only behavior, setcache_examples=False.
The cache key is the example row's file path, not a content hash. Regenerating an asset in place serves the stale cached output forever. If you replace example files, bump a cache_version marker or wipe .gradio/cached_examples/<id>/.
Hot-reload (rung 1) does not rebuild the cache. A cache_version bump needs a real commit + restart.
Streaming and generators
gr.Interface(fn=...) and .click(fn=...) both accept generator functions. Each yield pushes a new value:
@spaces.GPU(duration=120)
def generate(prompt):
yield gr.update(value=None, label="Starting…")
for k in range(num_steps):
yield gr.update(value=preview(k), label=f"Step {k+1}/{num_steps}")
yield gr.update(value=final, label="Done")
Use gr.update(label=...) for status narration — feels like a status line without a separate component.
Caveat: gr.Progress(track_tqdm=True) and yield partial outputs fight each other — pick one streaming mechanism.
Useful UX bits
progress=gr.Progress(track_tqdm=True)in the GPU function gives a free tqdm-driven progress bar.- Pair a "Randomize seed" checkbox with the seed input, and write the actually-used seed back so users can re-run deterministically:
randomize = gr.Checkbox(label="Randomize seed", value=True) seed = gr.Number(label="Seed", value=0, precision=0) @spaces.GPU def gen(..., seed, randomize_seed): if randomize_seed: seed = random.randint(0, 2**31 - 1) seed = int(seed) yield ..., gr.update(value=seed) # write back into the Seed input ... run.click(gen, inputs=[..., seed, randomize], outputs=[..., seed])
Custom HTML components
Stock Gradio components cover 95% of cases. When they don't, gr.HTML(...) lets you build contextual custom UI without leaving the Gradio Space (no need to switch to Docker).
Guide: https://www.gradio.app/guides/custom-HTML-components Example with a 3D camera-angle picker that makes sense in context: https://huggingface.co/spaces/multimodalart/qwen-image-multiple-angles-3d-camera
Don't reach for this if a stock component covers the need.
Custom frontends — gr.Server
For fully custom frontends with their own HTML/JS, while keeping Gradio's queue + GPU scheduling:
from gradio import Server
from fastapi.responses import HTMLResponse
app = Server(title="my-app")
@spaces.GPU(duration=60)
def _run_gpu(prompt): return inference(prompt)
@app.api(name="generate", concurrency_limit=1, time_limit=180)
def generate(prompt: str) -> str:
return _run_gpu(prompt) # @app.api wraps, doesn't stack with @spaces.GPU
@app.get("/", response_class=HTMLResponse)
async def homepage():
return open("index.html").read()
demo = app # HF runtime expects `demo`
if __name__ == "__main__":
demo.launch(ssr_mode=False)
Required for the custom / route to actually serve:
hf spaces variables add <ns>/<name> --env GRADIO_SSR_MODE=false
launch(ssr_mode=False) is ignored on HF — must be the env var.
Valid @app.api kwargs: name, description, concurrency_limit, concurrency_id, queue, batch, max_batch_size, api_visibility, time_limit, stream_every.
Don't stack @spaces.GPU and @app.api on the same function — silently breaks request flow. Keep them on separate functions.
Two-ceiling coordination: both @spaces.GPU(duration=N) and @app.api(time_limit=M) apply; the lower wins. Set time_limit to your max duration across modes — a too-low time_limit kills the request even if GPU duration would have allowed it.
Hot-reload (rung 1 in debugging.md) does not work with gr.Server — always hf upload or commit.
Reference Space: https://huggingface.co/spaces/huggingface-projects/rf-detr-realtime-webcam
Slow-startup Gradio (big-model Spaces)
For Spaces that take 10–20 min to load weights:
- Set
startup_duration_timeout: 1hin README frontmatter (default 30 min). - Disable SSR:
hf spaces variables add <id> --env GRADIO_SSR_MODE=false. Otherwise the SSR health check times out before the app finishes loading.
Don't
gr.Button(text="X")—text=was removed; usegr.Button("X").gr.Button(type="button")— drop the kwarg..click(..., _js=share_js)— renamed tojs=..style(height=...)— removed in gradio 4+.gr.ImageMask(brush_color=...)— kwarg removed.demo.launch(mcp_server=True)on gradio 4.x — only valid on 5+.