# 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 = """
${value.output || 'No result yet'}
""" 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 = """

Calculator

+
${value.result !== undefined ? value.result : '—'}
${value.expression || ''}
""" 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