---
tags: [gradio-custom-component, ui, form, settings, dataclass]
title: gradio_propertysheet
short_description: Property Sheet Component for Gradio
colorFrom: blue
colorTo: green
sdk: gradio
pinned: true
app_file: space.py
---
# `gradio_propertysheet`
## Key Features
- **Automatic UI Generation**: Instantly converts `dataclass` fields into a structured UI.
- **Rich Component Support**: Automatically maps Python types to UI controls:
- `str` -> Text Input
- `int`, `float` -> Number Input
- `bool` -> Styled Checkbox
- `typing.Literal` -> Dropdown
- **Metadata-Driven Components**: Force a specific component using metadata:
- `metadata={"component": "slider"}`
- `metadata={"component": "colorpicker"}`
- **Nested Groups**: Nested `dataclasses` are rendered as collapsible groups for organization.
- **Conditional Visibility**: Show or hide fields based on the value of others using `interactive_if` metadata.
- **Built-in Helpers**:
- **Tooltips**: Add `help` text to any property's metadata for an info icon.
- **Reset Button**: Each property gets a button to reset its value to default.
- **Accordion Layout**: The entire component can act as a main collapsible accordion panel using the `open` parameter.
- **Theme-Aware**: Designed to look and feel native in all Gradio themes.
- **Dynamic Updates**: Supports advanced patterns where changing one field (e.g., a model selector) can dynamically update the options of another field (e.g., a sampler dropdown).
## Installation
```bash
pip install gradio_propertysheet
```
## Usage
```python
import os
import json
import gradio as gr
from dataclasses import dataclass, field, asdict
from typing import Literal
from gradio_propertysheet import PropertySheet
from gradio_htmlinjector import HTMLInjector
# --- 1. Dataclass Definitions (unchanged) ---
@dataclass
class ModelSettings:
model_type: Literal["SD 1.5", "SDXL", "Pony", "Custom"] = field(default="SDXL", metadata={"component": "dropdown", "label": "Base Model"})
custom_model_path: str = field(default="/path/to/default.safetensors", metadata={"label": "Custom Model Path", "interactive_if": {"field": "model_type", "value": "Custom"}})
vae_path: str = field(default="", metadata={"label": "VAE Path (optional)"})
@dataclass
class SamplingSettings:
sampler_name: Literal["Euler", "Euler a", "DPM++ 2M Karras", "UniPC"] = field(default="DPM++ 2M Karras", metadata={"component": "dropdown", "label": "Sampler"})
steps: int = field(default=25, metadata={"component": "slider", "minimum": 1, "maximum": 150, "step": 1})
cfg_scale: float = field(default=7.0, metadata={"component": "slider", "minimum": 1.0, "maximum": 30.0, "step": 0.5})
@dataclass
class RenderConfig:
seed: int = field(default=-1, metadata={"component": "number_integer", "label": "Seed (-1 for random)"})
model: ModelSettings = field(default_factory=ModelSettings)
sampling: SamplingSettings = field(default_factory=SamplingSettings)
@dataclass
class Lighting:
sun_intensity: float = field(default=1.0, metadata={"component": "slider", "minimum": 0, "maximum": 5, "step": 0.1})
color: str = field(default="#FFDDBB", metadata={"component": "colorpicker", "label": "Sun Color"})
@dataclass
class EnvironmentConfig:
background: Literal["Sky", "Color", "Image"] = field(default="Sky", metadata={"component": "dropdown"})
lighting: Lighting = field(default_factory=Lighting)
@dataclass
class EulerSettings:
s_churn: float = field(default=0.0, metadata={"component": "slider", "minimum": 0.0, "maximum": 1.0, "step": 0.01})
@dataclass
class DPM_Settings:
karras_style: bool = field(default=True, metadata={"label": "Use Karras Sigma Schedule"})
# --- 2. Data Mappings and Initial Instances (unchanged) ---
initial_render_config = RenderConfig()
initial_env_config = EnvironmentConfig()
sampler_settings_map_py = {"Euler": EulerSettings(), "DPM++ 2M Karras": DPM_Settings(), "UniPC": None}
model_settings_map_py = {"SDXL 1.0": DPM_Settings(), "Stable Diffusion 1.5": EulerSettings(), "Pony": None}
# --- 3. CSS & JS Injection function (unchanged) ---
def inject_assets():
"""
This function prepares the payload of CSS, JS, and Body HTML for injection.
"""
popup_html = """"""
css_code = ""
js_code = ""
try:
with open("custom.css", "r", encoding="utf-8") as f:
css_code += f.read() + "\n"
with open("custom.js", "r", encoding="utf-8") as f:
js_code += f.read() + "\n"
except FileNotFoundError as e:
print(f"Warning: Could not read asset file: {e}")
return {"js": js_code, "css": css_code, "body_html": popup_html}
# --- 4. Gradio App Build ---
with gr.Blocks(title="PropertySheet Demos") as demo:
html_injector = HTMLInjector()
gr.Markdown("# PropertySheet Component Demos")
with gr.Row():
# --- Flyout popup ---
with gr.Column(elem_id="flyout_panel_source", elem_classes=["flyout-source-hidden"]) as flyout_panel_source:
close_btn = gr.Button("×", elem_classes=["flyout-close-btn"])
flyout_sheet = PropertySheet(visible=True, container=False, label="Settings", show_group_name_only_one=False, disable_accordion=True)
with gr.Tabs():
with gr.TabItem("Original Sidebar Demo"):
gr.Markdown("An example of using the `PropertySheet` component as a traditional sidebar for settings.")
render_state = gr.State(value=initial_render_config)
env_state = gr.State(value=initial_env_config)
sidebar_visible = gr.State(False)
with gr.Row():
with gr.Column(scale=3):
generate = gr.Button("Show Settings", variant="primary")
with gr.Row():
output_render_json = gr.JSON(label="Live Render State")
output_env_json = gr.JSON(label="Live Environment State")
with gr.Column(scale=1):
render_sheet = PropertySheet(value=initial_render_config, label="Render Settings", width=400, height=550, visible=False, root_label="Generator")
environment_sheet = PropertySheet(value=initial_env_config, label="Environment Settings", width=400, open=False, visible=False, root_label="General")
def change_visibility(is_visible, render_cfg, env_cfg):
new_visibility = not is_visible
button_text = "Hide Settings" if new_visibility else "Show Settings"
return (new_visibility, gr.update(visible=new_visibility, value=render_cfg), gr.update(visible=new_visibility, value=env_cfg), gr.update(value=button_text))
def handle_render_change(updated_config: RenderConfig, current_state: RenderConfig):
if updated_config is None: return current_state, asdict(current_state), current_state
if updated_config.model.model_type != "Custom": updated_config.model.custom_model_path = "/path/to/default.safetensors"
return updated_config, asdict(updated_config), updated_config
def handle_env_change(updated_config: EnvironmentConfig, current_state: EnvironmentConfig):
if updated_config is None: return current_state, asdict(current_state), current_state
return updated_config, asdict(updated_config), current_state
generate.click(fn=change_visibility, inputs=[sidebar_visible, render_state, env_state], outputs=[sidebar_visible, render_sheet, environment_sheet, generate])
render_sheet.change(fn=handle_render_change, inputs=[render_sheet, render_state], outputs=[render_sheet, output_render_json, render_state])
environment_sheet.change(fn=handle_env_change, inputs=[environment_sheet, env_state], outputs=[environment_sheet, output_env_json, env_state])
demo.load(fn=lambda r_cfg, e_cfg: (asdict(r_cfg), asdict(e_cfg)), inputs=[render_state, env_state], outputs=[output_render_json, output_env_json])
with gr.TabItem("Flyout Popup Demo"):
gr.Markdown("An example of attaching a `PropertySheet` as a flyout panel to other components.")
# --- State Management ---
flyout_visible = gr.State(False)
active_anchor_id = gr.State(None)
js_data_bridge = gr.Textbox(visible=False, elem_id="js_data_bridge")
with gr.Column(elem_classes=["flyout-context-area"]):
with gr.Row(elem_classes=["fake-input-container", "no-border-dropdown"]):
sampler_dd = gr.Dropdown(choices=list(sampler_settings_map_py.keys()), label="Sampler", value="Euler", elem_id="sampler_dd", scale=10)
sampler_ear_btn = gr.Button("⚙️", elem_id="sampler_ear_btn", scale=1, elem_classes=["integrated-ear-btn"])
with gr.Row(elem_classes=["fake-input-container", "no-border-dropdown"]):
model_dd = gr.Dropdown(choices=list(model_settings_map_py.keys()), label="Model", value="SDXL 1.0", elem_id="model_dd", scale=10)
model_ear_btn = gr.Button("⚙️", elem_id="model_ear_btn", scale=1, elem_classes=["integrated-ear-btn"])
# --- Event Logic ---
def handle_flyout_toggle(is_vis, current_anchor, clicked_dropdown_id, settings_obj):
if is_vis and current_anchor == clicked_dropdown_id:
js_data = json.dumps({"isVisible": False, "anchorId": None})
return False, None, gr.update(), gr.update(value=js_data)
else:
js_data = json.dumps({"isVisible": True, "anchorId": clicked_dropdown_id})
return True, clicked_dropdown_id, gr.update(value=settings_obj), gr.update(value=js_data)
def update_ear_visibility(selection, settings_map):
has_settings = settings_map.get(selection) is not None
return gr.update(visible=has_settings)
def on_flyout_change(updated_settings, active_id, sampler_val, model_val):
if updated_settings is None or active_id is None:
return
if active_id == sampler_dd.elem_id:
sampler_settings_map_py[sampler_val] = updated_settings
elif active_id == model_dd.elem_id:
model_settings_map_py[model_val] = updated_settings
def close_the_flyout():
js_data = json.dumps({"isVisible": False, "anchorId": None})
return False, None, gr.update(value=js_data)
js_update_flyout = "(jsonData) => { update_flyout_from_state(jsonData); }"
sampler_dd.change(
fn=lambda sel: update_ear_visibility(sel, sampler_settings_map_py),
inputs=[sampler_dd],
outputs=[sampler_ear_btn]
).then(fn=close_the_flyout, outputs=[flyout_visible, active_anchor_id, js_data_bridge]
).then(fn=None, inputs=[js_data_bridge], js=js_update_flyout)
sampler_ear_btn.click(
fn=lambda is_vis, anchor, sel: handle_flyout_toggle(is_vis, anchor, sampler_dd.elem_id, sampler_settings_map_py.get(sel)),
inputs=[flyout_visible, active_anchor_id, sampler_dd],
outputs=[flyout_visible, active_anchor_id, flyout_sheet, js_data_bridge]
).then(fn=None, inputs=[js_data_bridge], js=js_update_flyout)
model_dd.change(
fn=lambda sel: update_ear_visibility(sel, model_settings_map_py),
inputs=[model_dd],
outputs=[model_ear_btn]
).then(fn=close_the_flyout, outputs=[flyout_visible, active_anchor_id, js_data_bridge]
).then(fn=None, inputs=[js_data_bridge], js=js_update_flyout)
model_ear_btn.click(
fn=lambda is_vis, anchor, sel: handle_flyout_toggle(is_vis, anchor, model_dd.elem_id, model_settings_map_py.get(sel)),
inputs=[flyout_visible, active_anchor_id, model_dd],
outputs=[flyout_visible, active_anchor_id, flyout_sheet, js_data_bridge]
).then(fn=None, inputs=[js_data_bridge], js=js_update_flyout)
flyout_sheet.change(
fn=on_flyout_change,
inputs=[flyout_sheet, active_anchor_id, sampler_dd, model_dd],
outputs=None
)
close_btn.click(
fn=close_the_flyout,
inputs=None,
outputs=[flyout_visible, active_anchor_id, js_data_bridge]
).then(fn=None, inputs=[js_data_bridge], js=js_update_flyout)
def initial_flyout_setup(sampler_val, model_val):
return {
sampler_ear_btn: update_ear_visibility(sampler_val, sampler_settings_map_py),
model_ear_btn: update_ear_visibility(model_val, model_settings_map_py)
}
# --- App Load ---
demo.load(
fn=inject_assets, inputs=None, outputs=[html_injector]
).then(
fn=initial_flyout_setup, inputs=[sampler_dd, model_dd], outputs=[sampler_ear_btn, model_ear_btn]
).then(
fn=None, inputs=None, outputs=None,
js="() => { setTimeout(reparent_flyout, 200); }"
)
if __name__ == "__main__":
demo.launch()
```
## `PropertySheet`
### Initialization
| name | type | default | description |
|---|---|---|---|
value |
```python typing.Optional[typing.Any][Any, None] ``` | None |
The initial dataclass instance to render. |
label |
```python str | None ``` | None |
The main label for the component, displayed in the accordion header. |
root_label |
```python str ``` | "General" |
The label for the root group of properties. |
show_group_name_only_one |
```python bool ``` | True |
If True, only the group name is shown when there is a single group. |
disable_accordion |
```python bool ``` | False |
If True, disables the accordion functionality. |
visible |
```python bool ``` | True |
If False, the component will be hidden. |
open |
```python bool ``` | True |
If False, the accordion will be collapsed by default. |
elem_id |
```python str | None ``` | None |
An optional string that is assigned as the id of this component in the DOM. |
scale |
```python int | None ``` | None |
The relative size of the component in its container. |
width |
```python int | str | None ``` | None |
The width of the component in pixels. |
height |
```python int | str | None ``` | None |
The maximum height of the component's content area in pixels before scrolling. |
min_width |
```python int | None ``` | None |
The minimum width of the component in pixels. |
container |
```python bool ``` | True |
If True, wraps the component in a container with a background. |
elem_classes |
```python list[str] | str | None ``` | None |
An optional list of strings that are assigned as the classes of this component in the DOM. |