Skip to main content
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:
1

Create a function and add the decorator

Wrap your function with @gr.render to make it re-render dynamically.
2

Specify inputs

Add input components to the inputs= parameter. The function will automatically re-run when any input changes.
3

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:
  1. Event listeners from the previous render are cleared
  2. New event listeners from the latest run are attached
  3. 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:
1

Browser performance

The same DOM element is reused instead of being destroyed and rebuilt, which is faster and preserves browser attributes.
2

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:
1

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.
2

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

1

Always use keys

Key all components and layouts inside render functions to preserve state and improve performance.
2

Freeze loop variables

When using loop variables inside event listeners, freeze them with default arguments: func(task=task).
3

Set state as output

When modifying state variables (especially nested structures), always include them in the event listener’s outputs.
4

Define listeners inside render

Event listeners that use dynamically created components must be defined inside the render function.
5

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: