By default, the components and event listeners you define in a Blocks app are fixed once the app launches. The @gr.render decorator changes this, allowing you to dynamically create, modify, or remove components based on user interactions.
Dynamic number of components
Let’s start with a simple example that creates a variable number of textboxes:
import gradio as gr
with gr.Blocks() as demo:
input_text = gr.Textbox(label="input")
@gr.render(inputs=input_text)
def show_split(text):
if len(text) == 0:
gr.Markdown("## No Input Provided")
else:
for letter in text:
gr.Textbox(letter)
demo.launch()
As you type in the input textbox, a new textbox is created for each letter. The @gr.render decorator enables this with three simple steps:
Create a function and add the decorator
Wrap your function with @gr.render to make it re-render dynamically.
Specify inputs
Add input components to the inputs= parameter. The function will automatically re-run when any input changes.
Create components inside the function
Any components created inside the function will be rendered based on the inputs.
Whenever the inputs change, the function re-runs and replaces the previous components with the new ones.
Customizing triggers
By default, @gr.render re-runs on:
- The
.load event of the app
- The
.change event of any input component
You can override this with custom triggers:
import gradio as gr
with gr.Blocks() as demo:
input_text = gr.Textbox(label="input")
@gr.render(inputs=input_text, triggers=[input_text.submit])
def show_split(text):
if len(text) == 0:
gr.Markdown("## No Input Provided")
else:
for letter in text:
gr.Textbox(letter)
demo.launch()
Now the function only re-runs when you press Enter in the textbox.
If you set custom triggers and want an automatic render at app start, add demo.load to your list of triggers.
Dynamic event listeners
When you create components inside a render function, you can also attach event listeners to them:
import gradio as gr
with gr.Blocks() as demo:
text_count = gr.State(1)
add_btn = gr.Button("Add Textbox")
merge_btn = gr.Button("Merge")
output = gr.Textbox(label="Merged Output")
add_btn.click(lambda x: x + 1, text_count, text_count)
@gr.render(inputs=text_count)
def render_textboxes(count):
textboxes = []
for i in range(count):
textbox = gr.Textbox(label=f"Textbox {i+1}", key=f"textbox_{i}")
textboxes.append(textbox)
# Event listener must be inside the render function
merge_btn.click(
lambda *texts: " ".join(texts),
inputs=textboxes,
outputs=output
)
demo.launch()
Critical rule: All event listeners that use components created inside a render function must also be defined inside that render function.However, these event listeners can still reference components defined outside the render function (like merge_btn and output in the example above).
When the function re-renders:
- Event listeners from the previous render are cleared
- New event listeners from the latest run are attached
- The number of textboxes matches the current
text_count value
Understanding the key parameter
The key= parameter tells Gradio that the same component is being recreated across re-renders. This provides two benefits:
Browser performance
The same DOM element is reused instead of being destroyed and rebuilt, which is faster and preserves browser attributes.
Property preservation
Properties that may change (like value) are preserved across re-renders instead of being reset.
By default, only the value property is preserved. You can specify additional properties with preserved_by_key=:
import gradio as gr
with gr.Blocks() as demo:
number_of_boxes = gr.Slider(
1, 5, value=2, step=1, label="Number of Textboxes"
)
@gr.render(inputs=number_of_boxes)
def render_boxes(count):
for i in range(count):
textbox = gr.Textbox(
label=f"Box {i+1}",
info=f"This is textbox number {i+1}",
key=f"box_{i}",
preserved_by_key=["value", "label"]
)
gr.Button("Change Label").click(
lambda: gr.Textbox(label="Updated!", info="Info resets"),
outputs=textbox
)
demo.launch()
In this example:
- When you change the slider, textboxes re-render
- Any entered
value is preserved (because it’s preserved by default)
- Any changed
label is preserved (because we added it to preserved_by_key)
- The
info property resets (because it’s not in preserved_by_key)
Parent layouts must also be keyed: If your component is nested within layout items like gr.Row or gr.Column, make sure to key those parent elements as well. The keys of all parent elements must match for the component to be properly preserved.
Keying event listeners
You can also key event listeners to improve performance and prevent errors:
button.click(
some_function,
inputs=input_box,
outputs=output_box,
key="my_event"
)
This is useful when:
- The same listener is recreated with the same inputs and outputs across renders
- An event from a previous render might finish after a re-render occurs
Keying ensures Gradio knows where to send the data properly.
Building a todo list app
Let’s put everything together with a complete example:
import gradio as gr
with gr.Blocks() as demo:
tasks = gr.State([]) # Store tasks as ["task text", completed (bool)]
new_task = gr.Textbox(label="New Task", placeholder="Enter a task...")
add_btn = gr.Button("Add Task")
def add_task(task_text, current_tasks):
if task_text.strip():
return current_tasks + [[task_text, False]]
return current_tasks
add_btn.click(add_task, [new_task, tasks], [tasks])
new_task.submit(add_task, [new_task, tasks], [tasks])
@gr.render(inputs=tasks)
def render_todos(task_list):
if not task_list:
gr.Markdown("*No tasks yet. Add one above!*")
return
for i, (task_text, completed) in enumerate(task_list):
with gr.Row(key=f"task_{i}"):
checkbox = gr.Checkbox(
value=completed,
label=task_text,
container=False,
key=f"checkbox_{i}"
)
delete_btn = gr.Button(
"🗑️",
scale=0,
min_width=50,
key=f"delete_{i}"
)
def mark_done(checked, task=task_list[i]):
task[1] = checked
return task_list
def delete(task=task_list[i]):
task_list.remove(task)
return task_list
checkbox.change(
mark_done,
inputs=checkbox,
outputs=tasks,
key=f"mark_{i}"
)
delete_btn.click(
delete,
outputs=tasks,
key=f"delete_click_{i}"
)
demo.launch()
Important pattern: When a variable from a loop is used inside an event listener function, “freeze” it by setting it as a default argument:def mark_done(checked, task=task_list[i]): # task=task_list[i] freezes the value
...
This ensures each listener uses the correct loop-time value, not the final value after the loop completes.
Working with nested state
When your render function reacts to a list or dict in gr.State:
Set state as output
Any event listener that modifies the state must include the state variable as an output, even if the function modifies it in-place. This tells Gradio to check if the state has changed.
Freeze loop variables
Use default arguments to freeze variables that are used inside event listeners within loops.
Building an audio mixer
Here’s a more advanced example that demonstrates all the concepts:
import gradio as gr
import numpy as np
def mix_audio(audio_dict):
"""Mix multiple audio tracks with volume controls."""
if not audio_dict:
return None
# Find the longest audio track
max_length = max(a[0].shape[0] if a else 0
for a in audio_dict.values() if isinstance(a, tuple))
if max_length == 0:
return None
# Get sample rate from first audio
sample_rate = next(
a[1] for a in audio_dict.values()
if isinstance(a, tuple) and a is not None
)
# Initialize mixed audio array
mixed = np.zeros(max_length)
for component, value in audio_dict.items():
if isinstance(value, tuple) and value is not None:
audio_data, sr = value
volume = audio_dict.get(f"{component}_volume", 1.0)
# Pad or trim to match max_length
if audio_data.shape[0] < max_length:
audio_data = np.pad(
audio_data,
(0, max_length - audio_data.shape[0])
)
else:
audio_data = audio_data[:max_length]
# Apply volume and add to mix
mixed += audio_data * volume
# Normalize to prevent clipping
if np.max(np.abs(mixed)) > 0:
mixed = mixed / np.max(np.abs(mixed))
return (sample_rate, mixed)
with gr.Blocks() as demo:
track_count = gr.State(1)
output_audio = gr.Audio(label="Mixed Output")
with gr.Row():
add_track_btn = gr.Button("Add Track")
mix_btn = gr.Button("Mix Tracks", variant="primary")
add_track_btn.click(
lambda x: x + 1,
inputs=track_count,
outputs=track_count
)
@gr.render(inputs=track_count)
def render_tracks(count):
audio_inputs = set()
for i in range(count):
with gr.Row(key=f"track_row_{i}"):
audio = gr.Audio(
label=f"Track {i+1}",
key=f"audio_{i}"
)
volume = gr.Slider(
0, 2, value=1,
label="Volume",
key=f"volume_{i}"
)
audio_inputs.add(audio)
audio_inputs.add(volume)
# Event listener uses all dynamically created components
mix_btn.click(
mix_audio,
inputs=audio_inputs,
outputs=output_audio,
key="mix_event"
)
demo.launch()
Using sets for many inputs: When you have many components of different types as inputs, use set notation (inputs={comp1, comp2, ...}) instead of list notation. In your function, you’ll receive a dictionary where keys are the components and values are their current values.
Best practices
Always use keys
Key all components and layouts inside render functions to preserve state and improve performance.
Freeze loop variables
When using loop variables inside event listeners, freeze them with default arguments: func(task=task).
Set state as output
When modifying state variables (especially nested structures), always include them in the event listener’s outputs.
Define listeners inside render
Event listeners that use dynamically created components must be defined inside the render function.
Key event listeners
Key your event listeners when the same listener is recreated across renders to prevent errors and improve performance.
Next steps
The @gr.render decorator dramatically expands what you can build with Gradio. Now you can: