The gr.HTML component lets you create custom UI elements with HTML, CSS, and JavaScript. You can build everything from simple styled displays to interactive custom components.
Basic HTML
At its simplest, you can display custom HTML:
import gradio as gr
with gr.Blocks() as demo:
gr.HTML(value="<h1>Hello World!</h1>")
demo.launch()
HTML templates
For dynamic content, use the html_template parameter. Templates support two syntaxes:
${}: Custom JavaScript expressions
{{}}: Handlebars templating for loops and conditionals
import gradio as gr
with gr.Blocks() as demo:
gr.HTML(
value="John",
html_template="<h1>Hello, {{value}}!</h1><p>${value.length} letters</p>"
)
demo.launch()
This renders as:
<h1>Hello, John!</h1><p>4 letters</p>
Templating with lists
Use Handlebars loops to iterate over arrays:
import gradio as gr
with gr.Blocks() as demo:
gr.HTML(
value=["apple", "banana", "cherry"],
html_template="""
<h1>${value.length} fruits:</h1>
<ul>
{{#each value}}
<li>{{this}}</li>
{{/each}}
</ul>
"""
)
demo.launch()
Adding CSS styles
Use the css_template parameter to style your HTML:
import gradio as gr
with gr.Blocks() as demo:
gr.HTML(
value=3,
html_template="""
<h2>Star Rating:</h2>
<div class='stars'>
{{#each (range 1 6)}}
<img class='{{#if (lte this ../value)}}filled{{else}}empty{{/if}}'
src='https://upload.wikimedia.org/wikipedia/commons/d/df/Award-star-gold-3d.svg'>
{{/each}}
</div>
""",
css_template="""
img { height: 50px; display: inline-block; }
.empty { filter: grayscale(100%); opacity: 0.3; }
"""
)
demo.launch()
By default, gr.HTML applies some CSS to match the Gradio theme. Disable this with apply_default_css=False if you want full control over styling.
Dynamic updates with templates
Templates automatically update when you change the value:
import gradio as gr
with gr.Blocks() as demo:
rating = gr.HTML(
value=3,
html_template="""
<h2>Star Rating:</h2>
<div class='stars'>
{{#each (range 1 6)}}
<img class='{{#if (lte this ../value)}}filled{{else}}empty{{/if}}'
src='https://upload.wikimedia.org/wikipedia/commons/d/df/Award-star-gold-3d.svg'>
{{/each}}
</div>
""",
css_template="""
img { height: 50px; display: inline-block; }
.empty { filter: grayscale(100%); opacity: 0.3; }
"""
)
slider = gr.Slider(1, 5, value=3, step=1, label="Select Rating")
slider.change(lambda x: x, slider, rating)
demo.launch()
Additional props
You can pass extra properties beyond value to your templates:
import gradio as gr
with gr.Blocks() as demo:
rating = gr.HTML(
value=3,
max_stars=7,
size="60px",
html_template="""
<h2>Star Rating: {{value}}/{{max_stars}}</h2>
<div class='stars'>
{{#each (range 1 (add max_stars 1))}}
<img class='{{#if (lte this ../value)}}filled{{else}}empty{{/if}}'
src='https://upload.wikimedia.org/wikipedia/commons/d/df/Award-star-gold-3d.svg'>
{{/each}}
</div>
""",
css_template="""
img { height: {{size}}; display: inline-block; }
.empty { filter: grayscale(100%); opacity: 0.3; }
"""
)
gr.Slider(1, 7, value=3, step=1, label="Rating").change(
lambda x: x, inputs=rating, outputs=rating
)
demo.launch()
All props are available in both html_template and css_template, and any prop can be updated via event listeners.
To create interactive custom components, use the js_on_load parameter. This JavaScript code runs when the component loads and has access to:
props: All component properties including value
trigger(event_name, data): Function to trigger events that Gradio can listen to
import gradio as gr
with gr.Blocks() as demo:
star_rating = gr.HTML(
value=0,
html_template="""
<div class='star-container'>
<h2>Rate this: <span id='rating-value'>{{value}}</span>/5</h2>
<div class='stars'>
{{#each (range 1 6)}}
<img class='star {{#if (lte this ../value)}}filled{{else}}empty{{/if}}'
data-rating='{{this}}'
src='https://upload.wikimedia.org/wikipedia/commons/d/df/Award-star-gold-3d.svg'>
{{/each}}
</div>
<button id='submit-rating'>Submit Rating</button>
</div>
""",
css_template="""
.star { height: 50px; cursor: pointer; display: inline-block; }
.empty { filter: grayscale(100%); opacity: 0.3; }
.star:hover { transform: scale(1.2); }
#submit-rating { margin-top: 10px; padding: 8px 16px; }
""",
js_on_load="""
// Access the container element
const container = element.querySelector('.star-container');
// Add click listeners to stars
container.querySelectorAll('.star').forEach(star => {
star.addEventListener('click', () => {
const rating = parseInt(star.dataset.rating);
props.value = rating; // Update the value
});
});
// Add submit button listener
container.querySelector('#submit-rating').addEventListener('click', () => {
trigger('submit'); // Trigger a 'submit' event
});
"""
)
output = gr.Textbox(label="Your Rating")
# Listen to the custom 'submit' event
star_rating.submit(lambda x: f"You rated: {x}/5", star_rating, output)
demo.launch()
Important: Event listeners attached in js_on_load are only attached once when the component first renders. If you create new elements dynamically that need listeners, use event delegation:element.addEventListener('click', (e) => {
if (e.target && e.target.matches('.child-element')) {
props.value = e.target.dataset.value;
}
});
Uploading files
The js_on_load scope includes an upload() function that uploads JavaScript File objects to the Gradio server:
import gradio as gr
with gr.Blocks() as demo:
file_uploader = gr.HTML(
value=None,
html_template="""
<div>
<h3>Custom File Uploader</h3>
<input type='file' id='file-input' />
<button id='upload-btn'>Upload</button>
<div id='status'></div>
</div>
""",
js_on_load="""
const fileInput = element.querySelector('#file-input');
const uploadBtn = element.querySelector('#upload-btn');
const status = element.querySelector('#status');
uploadBtn.addEventListener('click', async () => {
const file = fileInput.files[0];
if (!file) {
status.textContent = 'Please select a file';
return;
}
status.textContent = 'Uploading...';
// Upload the file
const { path, url } = await upload(file);
props.value = { path, url, name: file.name };
status.textContent = 'Upload complete!';
trigger('upload');
});
"""
)
output = gr.JSON(label="Uploaded File Info")
file_uploader.upload(lambda x: x, file_uploader, output)
demo.launch()
The upload() function returns a dictionary with:
path: Server-side file path
url: Public URL to access the file
Server functions
You can call Python functions directly from your js_on_load code using server_functions:
import gradio as gr
import random
def get_random_quote():
quotes = [
"The only way to do great work is to love what you do.",
"Innovation distinguishes between a leader and a follower.",
"Stay hungry, stay foolish."
]
return random.choice(quotes)
def save_quote(quote):
# Save to database or file
return f"Saved: {quote}"
with gr.Blocks() as demo:
quote_component = gr.HTML(
value="",
html_template="""
<div>
<blockquote id='quote'>{{value}}</blockquote>
<button id='new-quote'>New Quote</button>
<button id='save-quote'>Save Quote</button>
<div id='status'></div>
</div>
""",
server_functions=[get_random_quote, save_quote],
js_on_load="""
const status = element.querySelector('#status');
element.querySelector('#new-quote').addEventListener('click', async () => {
// Call Python function from JavaScript
const quote = await server.get_random_quote();
props.value = quote;
});
element.querySelector('#save-quote').addEventListener('click', async () => {
const result = await server.save_quote(props.value);
status.textContent = result;
});
// Load initial quote
server.get_random_quote().then(quote => props.value = quote);
"""
)
demo.launch()
Server functions are available as async methods on the server object inside js_on_load. They can accept any JSON-serializable arguments and return JSON-serializable values.
Creating reusable component classes
For components you’ll use multiple times, create a class by subclassing gr.HTML:
import gradio as gr
from gradio.data_classes import GradioModel
class StarRating(gr.HTML):
def __init__(self, value=0, max_stars=5, **kwargs):
super().__init__(
value=value,
max_stars=max_stars,
html_template="""
<div class='star-rating'>
{{#each (range 1 (add max_stars 1))}}
<img class='star {{#if (lte this ../value)}}filled{{else}}empty{{/if}}'
data-rating='{{this}}'
src='https://upload.wikimedia.org/wikipedia/commons/d/df/Award-star-gold-3d.svg'>
{{/each}}
</div>
""",
css_template="""
.star { height: 40px; cursor: pointer; display: inline-block; }
.empty { filter: grayscale(100%); opacity: 0.3; }
.star:hover { transform: scale(1.2); }
""",
js_on_load="""
element.querySelectorAll('.star').forEach(star => {
star.addEventListener('click', () => {
props.value = parseInt(star.dataset.rating);
trigger('change');
});
});
""",
**kwargs # Important: pass through kwargs to parent
)
# Now you can reuse it easily
with gr.Blocks() as demo:
rating1 = StarRating(value=3, max_stars=5)
rating2 = StarRating(value=4, max_stars=10)
output = gr.Textbox()
rating1.change(lambda x: f"Rating 1: {x}", rating1, output)
demo.launch()
Important: Gradio requires all components to accept certain arguments (like render, key, elem_id, etc.). Always add **kwargs to your __init__ method and pass it to super().__init__() to ensure your component works correctly.
Embedding components in HTML
You can embed other Gradio components inside your HTML using the @children placeholder:
import gradio as gr
with gr.Blocks() as demo:
with gr.HTML(
html_template="""
<div class='fancy-container'>
<h2>User Information</h2>
<div class='form-fields'>@children</div>
<p class='help-text'>Please fill out all fields</p>
</div>
""",
css_template="""
.fancy-container {
border: 2px solid #4CAF50;
border-radius: 10px;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.form-fields { margin: 20px 0; }
.help-text { color: #ccc; font-style: italic; }
"""
):
name = gr.Textbox(label="Name")
email = gr.Textbox(label="Email")
demo.launch()
The @children placeholder must be at the top level of the html_template. Target the parent element directly with CSS or JavaScript to style or interact with the container.
API and MCP support
To make your custom HTML component work with Gradio’s API and MCP (Model Context Protocol), define how its data should be serialized.
Option 1: Define api_info method
class StarRating(gr.HTML):
def api_info(self):
return {
"type": "integer",
"minimum": 0,
"maximum": self.max_stars,
"description": "Star rating value"
}
Option 2: Define a Pydantic data model
For complex data structures, use a Pydantic model:
from gradio.data_classes import GradioModel, GradioRootModel
from typing import List
class RatingData(GradioModel):
value: int
max_stars: int
comments: List[str]
class DetailedStarRating(gr.HTML):
data_model = RatingData
Use GradioModel for dictionary-like data with named fields, or GradioRootModel for simple types (strings, lists, etc.) that don’t need wrapping.
Security considerations
Important security notes:
-
XSS vulnerabilities: Using
gr.HTML injects raw HTML and JavaScript into your app. Never use untrusted user input directly in html_template or js_on_load.
-
Input validation: Python event listeners that accept your
gr.HTML component as input can receive arbitrary values, not just the values your frontend sets. Always sanitize and validate input in production apps.
-
Trust your code: Only use
gr.HTML with code you trust and control.
Next steps
Explore example custom components: