Spaces:
Running
Running
| # 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 = """ | |
| <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 | |
| ```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 | |
| <!-- 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: | |
| ```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 = """ | |
| <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:** | |
| ```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 | |