How I Built 7 Custom Gradio Components in Just 12 Days!

Community Article Published August 12, 2025

My workflow with LLM's, Python and Gradio that allowed me to build rich UIs in record time.

Introduction: The Spark of Necessity

Yes, you read that title correctly. Seven custom Gradio components, from conception to a functional first version, in just twelve days. It all started with a practical challenge: I was developing a Gradio interface for a complex image restoration pipeline using the diffusers library. While the core functionality was easy to implement, creating the premium user experience I envisioned required more specialized tools.

Gradio's standard components are an incredible starting point, but for my specific needs, they weren't enough..

That's when the question arose: "What if, instead of working around the limitations, I just built the exact components I need?"

This question led me on an intense 12-day development journey. In this article, I won't just list the components I created; I'm going to pull back the curtain on my process. I will show you the exact, step-by-step dialogue I had with Google Gemini to create an eighth component, ImageMeta, revealing the prompts I used and the insights that allowed me to refine the result until it was exactly what I needed.

image/png

ImageMeta Component


1. My Component Suite: Solving Real-World Problems

To understand the "why" behind this journey, here's a summary of the 7 components I created and the problems they solve in my image restoration application:

  • TokenizerTextBox: Models like SDXL have a limit of 77 tokens per prompt. This component displays the text, counts the tokens and characters in real time, and shows the user exactly how the model "sees" the prompt. This gives the user the power to optimize their prompts while respecting the model's limits.
  • TagGroupHelper: Simplifies prompt engineering. It displays groups of tags (e.g., "Styles," "Artists," "Quality") and, with one click, the user concatenates these tags to their main prompt.
  • PropertySheet: Inspired by IDE property sheets, this component groups multiple inputs (sliders, checkboxes, color pickers) into a single, collapsible panel, organizing complex interfaces cleanly and efficiently.
  • LiveLog: It displays real-time Python logs in the interface, with an integrated tqdm-style progress bar. Perfect for tracking long processes like model training or inference.
  • HTMLInjector: Inspired by ASP.NET's ScriptContainer, it allows you to inject CSS, JS, or custom HTML. The big advantage is that the injected code becomes part of the component, bypassing security restrictions that sometimes block Gradio's native injection.
  • VideoSlider: Allows you to compare videos side by side with a draggable slider.
  • BottomBar: Creates a collapsible toolbar at the bottom of the screen.

2. The Essential Foundation: What You Should Already Know

This is not a "Hello, World!" guide. I won't teach you how to install Node.js or set up a Python environment. The goal here is to demonstrate an advanced workflow. Therefore, to execute this workflow, I assume that you, the reader:

  • Are familiar with Python: You are comfortable with the language and its object-oriented programming concepts.
  • Have already used Gradio: You know how to build a basic interface with the framework's native components. Understanding event logic (.click(), .change()) is fundamental.
  • Have a grasp of web development: You don't need to be a frontend expert, but you know what HTML, CSS, and JavaScript are. The most important thing is having a clear vision of the visual and interactive result you want to achieve.
  • Have your environment ready: You have already followed the official Gradio documentation and have the custom component creation environment (gradio cc) up and running.

If you meet these prerequisites, you're ready to supercharge your development.


3. The Dialogue: Building ImageMeta with LLM (Google Gemini)

This is where the practical part begins. Instead of a code tutorial, I'm going to share my thought process and the dialogue I had with Gemini, starting with the most important step: preparing the AI for the task.

Round 1: Setting the Rules of the Game (The Setup Prompt)

Before even mentioning my idea, my first step was to "calibrate" Gemini. I needed to ensure it fully understood the technical environment and my expectations throughout the development journey.

My Prompt: What do you think about building a component for the Gradio framework? I'm using Gradio version 5.38.2, where the main commands are: gradio cc create, gradio cc dev, gradio cc build... You should know that it has a new architecture, different from version 3. I ask that you respect this new architecture in your suggestions. I'd also like you to always provide the code in English and documented in English, if possible always adding logs for us to debug in the browser console and the Python backend. Let's first talk about the component idea. Please don't provide any complete code yet, just a rough outline of the component's file structure based on the idea I'll present.

What this prompt does:

  • Defines the Technical Context: Specifying the Gradio version and gradio cc commands is crucial.
  • Establishes Authority and Control: The phrase "I ask that you respect this new architecture" is an instruction, not a request.
  • Sets Quality Standards: Requesting code in English and debug logs acts as a "System Prompt" for the entire conversation.
  • Manages the Pace: The instruction "Please don't provide any complete code yet" forces the AI to work incrementally.

Round 2: The Initial Idea and Gemini's Response

With the ground rules set, it was time to present the component's vision.

My Prompt: Okay. I'm thinking about creating an image loading component called "ImageMeta," similar to the native image component, but I want to add some additional features to display the file's metadata. What I thought of is an 'info' button next to the component's x button that, when clicked, will open a floating panel displaying the metadata, with each entry on a separate line in a "title: value" format (e.g., "Title: My Image"). This panel should have a scrollbar, as it will have the same height as the image component. We can also add a new button to the floating panel: "Load metadata." This button will transfer the metadata values to the components it receives in the event. Upon receiving the event, a collection of interface components will be updated by matching the component's label with a title obtained from the metadata. To avoid user responsibility for the loading logic, we can encapsulate this logic in a helper class. For example, we will have a extract_metadata function, which can be stored in a file in the backend folder called helpers.py. An important point to note is that I have a custom component called PropertySheet, which is updated based on a provided dataclass. This type of component will be responsible for several properties that may correspond to a metadata title. The load function will need to be prepared to handle both native components and this type of component. Later, as we progress in development, I'll go into more detail about this component

What this prompt does:

  • Defines the Complete Vision: It describes the UI, behavior, event logic and backend architecture.
  • Anticipates Complexity: It mentions the need to handle other custom components like PropertySheet.
  • Enforces Strict Constraints: The final instructions (Don't use _..., don't show any code yet, and Do not change the name...) act as a master of ceremonies, forcing the AI to adhere to Gradio's standards and the conversational pace.

Gemini's Response (Summarized): Excellent idea... Here is a sketch of the file structure...

imagemeta/
├── backend/
│   └── imagemeta/
│       ├── imagemeta.py
│       └── helpers.py
├── demo/
│   └── app.py
├── frontend/
│   ├── Index.svelte
│   └── ...
└── pyproject.toml

This structure looks solid...

Round 3: The Strategic Shortcut - Guiding with a Template

With this clear structure in mind, I had a crucial insight: instead of asking it to create code from scratch, my next move was to give it the code of an existing, functional component.

Setup Note: Before running the command, it's good practice to have separate virtual environments. I often use one for general development (python -m venv venv) and another for installing and testing custom components (python -m venv venv2).

In the terminal, I ran:

gradio cc create --template Image ImageMeta

This command creates the entire file structure, pre-filled with the functional source code of the gr.Image component. With these files in hand, my next prompt to Gemini was much more powerful.

Benefits of this approach:

  • Avoids Reinventing the Wheel: Giving the AI functional code is infinitely more efficient.
  • Reduces the Margin for Error: The AI will receive high-quality code that already follows Gradio's conventions.
  • High-Quality Context: The LLM will have a complete, working context to build upon.
  • Professional Workflow: Using gradio cc templates is how component development is done in practice.

Round 4: Simplifying the Foundation - The Refactoring Dialogue

I now had a working codebase, but it was complex and had some features I didn't need (like webcam support). My next step was to inform the AI of the generated structure, provide the code for the generated files, and finally prune what I didn't need.

My Prompt: Okay, I've scaffolded the new component using the gradio cc create --template Image ImageMeta command. I noticed that this process generated several additional files, including some in a shared directory within the frontend folder. I will provide the complete code for each file so you can analyze the initial structure and we can use it as a foundation for our component. First, the backend code: imagemeta.py {code}, then in the frontend we have: index.svelte {code}, Image.svelte {code}, ImagePreview.svelte {code}, ImageUploader.svelte {code}, index.ts {code}, stream_utils.ts {code}, types.ts {code}, utils.ts {code}, Webcam.svelte {code}, WebcamPermissions.svelte {code}. We also have the demo application app.py {code}. Since our component's objective is to simply load the image, we don't need to load it via webcam, so we can simplify things. Please show me the updated structure, explaining which files we should keep and which we can delete.

{code} - replace this by the file code.

Gemini's Response (Summarized): "Of course. This is an excellent and necessary step... Your New, Simplified File Structure

imagemeta/
├── backend/
│   └── imagemeta/
│       └── imagemeta.py      # <-- To be simplified
├── frontend/
│   ├── Index.svelte        # <-- Main component, to be simplified
│   └── shared/
│       ├── Image.svelte        # (Kept)
│       ├── ImagePreview.svelte # (Kept)
│       └── ImageUploader.svelte# (Kept) For interactive uploads, to be simplified

This structure is now much more manageable..."

Concrete Action: I followed the plan and deleted the unnecessary files. Now, we have a clean foundation to build upon.

Round 5: The Iteration Dance - Where the Developer Takes the Lead

This is where things get interesting, and your knowledge as a developer becomes crucial.

My methodology for this phase is as follows:

  • Run the current code first. Run the component with gradio cc dev to understand its behavior before adding anything new.
  • Add one new feature at a time. Don't try to ask for everything at once. Request a feature, test it, and if it's not what you expect, iterate or intervene manually until you get the desired result.
  • Provide existing code snippets. If you already have functions ready, give them to the AI to speed up the process.
  • Be ready for the AI's "bugs." Gemini can be stubborn about comments, forget to provide a file, or "hallucinate" functions that don't exist. Your role is to detect, correct, and guide.

The Construction of ImageMeta, Step-by-Step

Here’s how I applied this methodology in practice, building the component incrementally.

Step 1: Cleaning the Base

  • Problem: The code generated from the gr.Image template was functional but bloated with features I didn't need, like webcam support. It wasn't a clean slate.
  • Action (My Prompt): I instructed Gemini to perform a cleanup.

    "Now that I've deleted the files that needed to be deleted, show me the full code for the files, but start by removing all functionality associated with the webcam."

  • Result: Gemini provided a streamlined version of the codebase, giving me a clean and focused foundation to build upon.

Step 2: Implementing the Core Frontend Feature

  • Problem: The component needed its primary feature: the ability to read image metadata directly in the browser and display it.
  • Action (My Prompt): I asked Gemini to modify the Svelte handle_upload function to extract metadata and show it in a popup.

    "Is it possible in this front-end function load the image metadata, whether it's a PNG or JPEG image, and display it in a floating popup in the component?:

    async function handle_upload({
          detail
          }: CustomEvent<FileData>): Promise<void> {
          if (detail.path?.toLowerCase().endsWith(".svg") && detail.url) {
          const response = await fetch(detail.url);
          const svgContent = await response.text();
          value = {
          ...detail,
          url: `data:image/svg+xml,${encodeURIComponent(svgContent)}`
          };
          } else {
          value = detail;
          }      
          await tick();
          dispatch("upload");
    }
    
  • Result: The AI successfully added the logic. The metadata was now being read on the frontend, marking the first major milestone.

Step 3: Refining the Input Source

  • Problem: The component still allowed pasting images from the clipboard, which was irrelevant for a metadata-focused tool and added unnecessary complexity.
  • Action (My Prompt): A simple instruction to simplify the component.

    "You can remove the source 'clipboard' from the component as it is not interesting for metadata."

  • Result: The component's functionality was tightened, focusing solely on file uploads.

Step 4: Building the Bridge to the Backend

  • Problem: The metadata was trapped in the frontend. I needed a way to send it to the Python backend for processing.
  • Action (My Prompt): I asked for a new, custom event to be created specifically for this purpose.

    "Now that we have the metadata on the front end, can we create a button in the component that triggers a new event so I can capture this metadata on the backend and then somehow add the metadata to the payload that is sent to the Python application? Also, generate app.py; I need the function to read this event and, first, print the metadata in Python. Example for a new event:"

      load_metadata = EventListener(
      "load_metadata",
      doc="This listener is triggered when the user clicks the 'Load metadata' button. The event data will be the metadata dictionary extracted from the image.",
      )
    EVENTS = [
      Events.clear,
      Events.change,
      Events.select,
      Events.upload,
      Events.input,
      load_metadata,
    ] 
    
  • Result: Gemini generated the code for the EventListener in Python and the corresponding frontend logic, successfully creating the communication channel.

Step 5: Improving the User Interaction

  • Problem: The event was likely triggered too easily. I wanted the user to view the metadata first, then explicitly click a button to load it.
  • Action (My Prompt): I requested a change in the UI flow.

    "Can you put a load metadata button inside the popup loaded after I click on info and fire the load metadata event only when I click that button?"

  • Result: The user experience became more intuitive. The "load" action was now a deliberate choice made from within the metadata popup.

Step 6: An Architectural Shift

  • Problem: My initial idea of using the preprocess function in the backend failed. I realized it was impossible to distinguish if the function was triggered by a simple .click() or my new .load_metadata() event.
  • Action (My Prompt): I took the lead and proposed a more robust architecture: a separate helpers.py file to handle the logic, which could be called directly from the main app.

    "You may need to change the component's architecture. We can create a helpers.py file in the component's backend folder as a utility, and it can be imported into our app.py using the import gradio_imagemeta.helpers function, which can be our _get_metadata function. Next, we start receiving the component's original payload, which is the original image, in the event function's parameters. In our handle_load_metadata application function, we call this helper function to extract the image received in the parameter. What do you think? We've solved two things at once and simplified the component's operations."

  • Result: This was a turning point. The backend architecture became cleaner, more modular, and solved the event ambiguity problem. This is a prime example of the developer guiding the AI, not just following it.

Step 7: Achieving Complete Metadata Extraction

  • Problem: The frontend library was only extracting a subset of the metadata. I needed to get the rest of the standard EXIF data, which could be done more reliably on the backend.
  • Action (My Prompt): I provided the list of metadata found by the frontend and asked the backend to find the rest.

    "In the frontend, exifData extracts these metadata: "ImageWidth: 2048 ImageHeight: 3072 BitDepth: 8 ColorType: RGB Compression: Deflate/Inflate Filter: Adaptive Interlace: Noninterlaced Model: abc FNumber: 123 ISOSpeedRatings: 123. However, for png even the custom ones I added are only Model, FNumber and ISOSpedRatings, can we extract the others in the backend as well:"

  • Result: The AI implemented the backend logic using a Python library (like Pillow), ensuring a complete set of metadata was available for processing.

Step 8: Adding a Custom Control Property

  • Problem: A long list of metadata can be noisy. I wanted to give the user the option to see only the custom, most important fields.
  • Action (My Prompt): I requested a new boolean property for the component.

    "We can have a property in the component like only_custom_metadata of the default boolean type True, if it is True it will only display the properties that I created, those that we added more and that the exifr loads more in the frontend must be hidden if it is False they can be displayed in both."

  • Result: The component became more flexible, allowing the user to toggle the verbosity of the displayed metadata.

Step 9 & 10: Polishing the UI

  • Problem: The default popup was functional but visually unappealing and couldn't handle long lists of metadata.
  • Action (My Prompts): I sent a series of prompts focused purely on aesthetics and layout.

    "Let's improve the appearance of the metadata popup on the frontend. I want it to have a horizontal and vertical scroll bar in case the content doesn't fit. We can also receive two props, popup_metadata_width and popup_metadata_height to define the height and width of the popup. For the content, the title can be bold, then below you can display each property with its label highlighted and without wrapping the value on the line. It needs to be on the line, one prop per line without breaking. I don't know if rendered in a two-column table layout where the label column is a soft gray, the popup's background will already be white, or if you can use the color classes of the applied theme."

    "Is it possible for the popup to be limited to the component width -20px?"

  • Result: Through iterative requests, the popup was transformed into a polished, well-formatted, and responsive UI element.

Step 11: Creating a Full-Circle Test Case

  • Problem: To properly test the "load metadata" feature, I first needed a reliable way to save metadata into an image.
  • Action (My Prompt): I asked Gemini to build a feature into the demo app itself to add metadata to an image and save it.

    "Add a feature to the demo to add metadata to an image I upload, then save the image to a location for reuploading. This metadata needs to be the same as the fields we intend to upload. For PNG, use from PIL import Image, PngImagePlugin; meta = PngImagePlugin.PngInfo()"

  • Result: This created a complete development and testing loop. I could now create a test image, upload it, and verify that my component read the data correctly.

Step 12: Manual Intervention (Developer's Insight)

  • Insight: I noticed that despite our architectural changes, the component's default preprocess method was still causing conflicts. The solution was to disable it completely for my custom event.
  • Action (My Code): I manually implemented the disable_preprocess = True attribute in the backend where my event was defined.
  • Result: This small, manual change, born from developer experience, was the final piece needed to make the backend event handling work perfectly.

Step 13 - 16: The PropertySheet Challenge (A Mini-Arc)

This was the most complex part of the journey: making ImageMeta interact with another one of my custom components, PropertySheet.

  • The Challenge: How could I map arbitrary metadata keys (e.g., "Model") to specific fields in a nested Python dataclass used by PropertySheet?
  • Initial Prompt: I implemented the dataclasses in app.py and then explained the problem to the AI.

    "For the PropertySheet component I tried to assign the metadata but initial_property_from_meta_config only returns the name of the attribute that was defined in the dataclass. If there is no way to get the label of the dataclass through initial_property_from_meta_config, is it possible to get it by the value of the component by browsing the group name and properties to match the label and then retrieve the name of the property that needs to be changed in initial_property_from_meta_config and then return it filled in?"

  • Refactoring Logic: The initial solution worked but cluttered my main app.py. I asked the AI to refactor.

    "You can extract the core logic in a function like transfer_metadata to the helpers.py file. There it will receive the list of fields, the metadata and will return the output list of the filled fields for the main handle function to receive this and return in the app."

  • Improving the Algorithm: The refactored function was still too specific. It needed to be generic enough to handle any nested dataclass structure.

    "But we have to think of a better solution, think that the objective is to go through the entire root class and if it finds a subclass like the image_settings field for example, it enters it and does the same, it must go down in order looking for the fields to set the values, the function must not know the name of these subattributes in the case image_settings must be agnostic."

  • Final Code Organization (My Insight): After the AI produced the recursive algorithm, I decided the helper functions were so tied to PropertySheet's logic that they belonged in its own helper file, not ImageMeta's. I moved the files myself.
  • Result: This sequence shows a true collaboration. I defined the problem, the AI provided solutions, I identified weaknesses, and we iterated towards a robust, recursive, and well-organized final algorithm.

Step 17: Ensuring UI Consistency

  • Problem: The "info" button and metadata popup worked when the component was interactive, but disappeared in interactive=False mode, which uses a different Svelte component (ImagePreview.svelte) for display.
  • Action (My Prompt): I provided the code for the other Svelte component and asked to replicate the functionality.

    "This is my ImageUploader.svelte {code}. I need you to add the info button functionality to display the metadata in ImagePreview as well, as it is used when the image is loaded with interactive=False, you can set the styles, parameters, and everything."

  • Result: The AI successfully transferred the feature, ensuring a consistent user experience regardless of the component's mode.

Step 18: Final Backend Tweak

  • Problem: A final bug appeared in the postprocess function. It wasn't correctly handling the image format when the input was a file path (str) versus a PIL Image object.
  • Action (My Prompt): I provided the function and specified the exact logic needed.

    "We need to check if self.interactive = False then we need to pass the format of value.format if it is of type Image.Image, if the value is of type str then get the file extension and pass it:"

     def postprocess(
            self, value: np.ndarray | Image.Image | str | Path | None
        ) -> ImageMetaData | Base64ImageData | None:
            print("DEBUG: postprocess called with value:", value)
            processed_value = image_utils.postprocess_image(
                value,
                cache_dir=self.GRADIO_CACHE,
                format=self.format,
            )
            print("DEBUG: postprocess returning:", processed_value)
            return processed_value
    
  • Result: With this last fix, the ImageMeta component was robust, feature-complete, and polished.

Round 6: Polishing and Final Thoughts

After the component is functional, the journey isn't over.

1. Documentation and Cleanup: Once everything works, it's time to clean house. Use the AI for this.

My Final Prompt: "Document this code, remove unnecessary comments, remove debug and log lines, be objective when documenting classes and functions with their parameters, and be careful with the content of Svelte comments in the HTML section, as they can break the syntax. All in English. {code}"

2. Help from Other LLMs: If you get stuck with Gemini for too long, don't hesitate to get a second opinion. Consult other models like Grok, Kimi, or Deepseek. In some situations, other models might be more accurate for a specific problem.


Conclusion: You're the Architect, and Iteration is the Secret

The journey to create 7 components in 12 days taught me that the key to AI productivity isn't just using it, but knowing how to dialogue with it. You are the architect; tools like Gemini are your tireless development team. Your experience is what allows you to ask the right questions, spot flaws, and guide the project in the right direction.

To wrap up, the entire recipe I've shared can be used for anything you want to develop, not just Gradio applications. The secret is to decompose the problem into smaller parts and iterate on it, one feature at a time.

I hope you can follow this blueprint, in your own way, for whatever scenario you desire, and achieve great results!

Don't forget to comment on the blog and let me know what you think!


Components Repositories:

Useful Resources:

Community

Sign up or log in to comment