# 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
```python
import gradio as gr
def my_python_function(data):
"""Your Python logic here"""
result = data.get("input") * 2
return {"output": result}
HTML = """
"""
CSS = """
"""
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
```javascript
// 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`:
```html
${value.result}
${value.result || 'Default text'}
${value.result !== undefined ? value.result : '—'}
```
### 4. Python Handler
The Python function receives `props.value` as its input:
```python
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:
```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:
```css
/* 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:
```css
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
```python
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 = """
"""
CSS = """
"""
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:**
```javascript
function updateStatus(msg) {
element.querySelector('#status').textContent = msg;
}
$('#btn').addEventListener('click', function() {
updateStatus('Processing...');
trigger('submit');
});
```
**Multiple Event Types:**
```python
component.submit(handle_submit, inputs=component, outputs=component)
component.click(handle_click, inputs=component, outputs=component)
```
**Error Handling:**
```python
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