gradio-html / guide-gradio-with-html-and-js.md
victor's picture
victor HF Staff
Use height: 100% instead of fixed min-height
cc97280

A newer version of the Gradio SDK is available: 6.0.2

Upgrade

Building Custom UI Gradio Apps with HTML/CSS/JS

This guide shows how to create Gradio apps with fully custom HTML/CSS/JS interfaces that call Python functions. Perfect for deploying on Hugging Face Spaces while maintaining complete control over your UI.

Overview

Gradio 6.0+ allows you to:

  • Replace the native Gradio UI with custom HTML/CSS/JS
  • Call Python functions from JavaScript using trigger()
  • Use template syntax to display Python results dynamically

Basic Structure

import gradio as gr

def my_python_function(data):
    """Your Python logic here"""
    result = data.get("input") * 2
    return {"output": result}

HTML = """
<div id="app">
  <input type="text" id="my-input">
  <button id="submit-btn">Submit</button>
  <div id="result">${value.output || 'No result yet'}</div>
</div>
"""

CSS = """
<style>
  #app { padding: 20px; }
  /* your styles */
</style>
"""

JS = """
(function() {
  function $(sel) { return element.querySelector(sel); }

  $('#submit-btn').addEventListener('click', function() {
    // Set the data to send to Python
    props.value = { input: $('#my-input').value };
    // Call the Python function
    trigger('submit');
  });
})();
"""

with gr.Blocks() as demo:
    component = gr.HTML(
        value={},
        html_template=CSS + HTML,
        js_on_load=JS,
        container=False
    )
    component.submit(my_python_function, inputs=component, outputs=component)

demo.launch()

Key Concepts

1. The gr.HTML Component

In Gradio 6.0+, gr.HTML has special parameters:

Parameter Description
value Initial data (use {} for empty object)
html_template Your HTML with template syntax ${value.xxx}
js_on_load JavaScript that runs when component loads
container Set to False to remove Gradio's wrapper

2. JavaScript API

Inside js_on_load, you have access to:

  • element - The DOM element of your component
  • props - Object containing value and other properties
  • trigger(event) - Function to trigger Gradio events
// Access DOM elements within your component
element.querySelector('#my-button')

// Update the value (this is what Python receives)
props.value = { key: "value" }

// Call Python function
trigger('submit')  // triggers .submit() handler
trigger('click')   // triggers .click() handler

3. Template Syntax

Use JavaScript template literals in html_template:

<!-- Simple value -->
<div>${value.result}</div>

<!-- With fallback -->
<div>${value.result || 'Default text'}</div>

<!-- Conditional -->
<div>${value.result !== undefined ? value.result : 'β€”'}</div>

4. Python Handler

The Python function receives props.value as its input:

def handler(data):
    # data is whatever you set in props.value
    num1 = data.get("num1", 0)
    num2 = data.get("num2", 0)

    # Return a dict - this becomes the new props.value
    return {"result": num1 + num2}

5. Wiring It Up

Connect the JavaScript trigger to Python:

component.submit(handler, inputs=component, outputs=component)

When JS calls trigger('submit'), Gradio:

  1. Sends props.value to handler()
  2. Takes the return value
  3. Updates props.value with it
  4. Re-renders the HTML template

Hiding Gradio's Native UI

To make your app look completely custom, add CSS to hide Gradio's chrome:

/* Hide footer */
footer { display: none !important; }

/* Reset containers */
.gradio-container, [class*="gradio-container"] {
  max-width: 100% !important;
  width: 100% !important;
  padding: 0 !important;
  margin: 0 !important;
}

/* Remove padding from main */
main.fillable, main[class*="fillable"], .app {
  padding: 0 !important;
  margin: 0 !important;
}

/* Clean up blocks */
.block, .block.padded {
  padding: 0 !important;
  border: none !important;
  background: transparent !important;
}

/* Reset svelte components */
[class*="svelte"] {
  padding: 0 !important;
  margin: 0 !important;
}

Hugging Face Spaces: Avoiding Iframe Issues

HF Spaces use an iframe resizer. Using min-height: 100vh causes infinite scroll. Instead:

html, body {
  height: 100%;
  overflow: hidden;  /* NOT auto */
}

#app {
  height: 100%;
  /* Use 100% not 100vh - viewport units cause iframe resizer loops */
}

/* Prevent iframe resizer issues */
html, body, #root, .gradio-container, .contain, gradio-app {
  height: 100% !important;
  overflow: hidden !important;
  min-height: 0 !important;  /* Important! Removes default min-height */
}

.contain, .main, gradio-app {
  min-height: 0 !important;
  max-height: 100% !important;
}

Key rule: Never use vh units (100vh, min-height: 100vh). Always use 100% which is relative to the parent, not the viewport.

Complete Example: Math Calculator

import gradio as gr

def add_numbers(data):
    num1 = float(data.get("num1", 0))
    num2 = float(data.get("num2", 0))
    result = num1 + num2
    return {"result": result, "expression": f"{num1} + {num2} = {result}"}

HTML = """
<div id="app">
  <h1>Calculator</h1>
  <input type="number" id="num1" value="0">
  <span>+</span>
  <input type="number" id="num2" value="0">
  <button id="calc-btn">Calculate</button>
  <div id="result">${value.result !== undefined ? value.result : 'β€”'}</div>
  <div id="expr">${value.expression || ''}</div>
</div>
"""

CSS = """
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; overflow: hidden; }
#app {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100%;
  background: #1a1a2e;
  color: white;
  gap: 1rem;
}
input { padding: 0.5rem; font-size: 1.2rem; width: 100px; }
button { padding: 0.5rem 1rem; cursor: pointer; }
#result { font-size: 2rem; color: #00d2ff; }

/* Hide Gradio UI */
footer { display: none !important; }
.gradio-container { max-width: 100% !important; padding: 0 !important; }
</style>
"""

JS = """
(function() {
  function $(sel) { return element.querySelector(sel); }

  $('#calc-btn').addEventListener('click', function() {
    props.value = {
      num1: parseFloat($('#num1').value) || 0,
      num2: parseFloat($('#num2').value) || 0
    };
    trigger('submit');
  });

  // Optional: Enter key support
  ['#num1', '#num2'].forEach(function(sel) {
    $(sel).addEventListener('keypress', function(e) {
      if (e.key === 'Enter') $('#calc-btn').click();
    });
  });
})();
"""

with gr.Blocks(fill_height=True, title="Calculator") as demo:
    calc = gr.HTML(
        value={},
        html_template=CSS + HTML,
        js_on_load=JS,
        container=False
    )
    calc.submit(add_numbers, inputs=calc, outputs=calc)

if __name__ == "__main__":
    demo.launch()

Tips & Gotchas

DO:

  • Use element.querySelector() instead of document.querySelector() (scoped to component)
  • Use IIFE (function() { ... })(); to avoid global scope pollution
  • Return a dict from Python - it becomes props.value
  • Use ${value.xxx} template syntax for dynamic content
  • Set container=False on gr.HTML for cleaner output

DON'T:

  • Use inline onclick="fn()" handlers (they won't work due to scoping)
  • Use min-height: 100vh (breaks HF iframe resizer)
  • Forget to connect .submit() handler
  • Use document.getElementById() (use element.querySelector('#id'))

Common Patterns

Status Updates:

function updateStatus(msg) {
  element.querySelector('#status').textContent = msg;
}
$('#btn').addEventListener('click', function() {
  updateStatus('Processing...');
  trigger('submit');
});

Multiple Event Types:

component.submit(handle_submit, inputs=component, outputs=component)
component.click(handle_click, inputs=component, outputs=component)

Error Handling:

def handler(data):
    try:
        result = do_something(data)
        return {"success": True, "result": result}
    except Exception as e:
        return {"success": False, "error": str(e)}

File Structure for HF Spaces

your-space/
β”œβ”€β”€ app.py           # Main Gradio app
β”œβ”€β”€ requirements.txt # Just: gradio>=6.0.0
└── README.md        # HF Space metadata

README.md for HF Spaces: ```yaml

title: Your App Name emoji: πŸš€ colorFrom: blue colorTo: purple sdk: gradio sdk_version: 6.0.1 app_file: app.py pinned: false


## Resources

- [Gradio HTML Component Docs](https://www.gradio.app/docs/gradio/html)
- [Custom HTML Components Guide](https://www.gradio.app/guides/custom_HTML_components/)
- [Hugging Face Spaces](https://huggingface.co/spaces)

---

Built with Gradio 6.0+ | Works on Hugging Face Spaces with ZeroGPU