Spaces:
Running
A newer version of the Gradio SDK is available:
6.0.2
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 componentprops- Object containingvalueand other propertiestrigger(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:
- Sends
props.valuetohandler() - Takes the return value
- Updates
props.valuewith it - 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 ofdocument.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=Falseongr.HTMLfor 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()(useelement.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