How I Implemented Live Preview in GenieAI (With Code & Deep Dive)

Software Development

When I first set out to implement live preview functionality in GenieAI, my initial thought was to leverage an existing solution. I started with Sandpack from CodeSandbox, a well-known tool designed for live coding environments. Sandpack seemed like a solid choice at first because it offers a fast, interactive development experience with a built-in bundler, allowing you to preview and execute React components instantly.

But here’s where I ran into problems.

Why Sandpack Didn’t Work for Me

While Sandpack is great for many use cases, I quickly realized it wasn’t flexible enough for what I needed:

  1. Limited Customization: Sandpack operates within its own ecosystem, making it difficult to tweak beyond its predefined configuration. I needed a solution where I had full control over how the preview was rendered and updated.
  2. Performance Issues: I noticed performance bottlenecks when handling complex UI updates. Sandpack’s internal state management introduced unnecessary re-renders, which affected the responsiveness of my UI.
  3. Dependency Restrictions: GenieAI relies on a variety of UI libraries, including Radix UI and TailwindCSS. Sandpack’s dependency management didn’t always play nicely with these, leading to inconsistencies in the rendered output.
  4. Inability to Capture Preview Screenshots: One of the features I wanted to include was exporting the preview as an image (PNG, JPEG, SVG). Sandpack’s sandboxed environment limited direct DOM access, making it difficult to implement this feature cleanly.

After wrestling with these limitations for a while, I realized it was time to build my own sandbox from scratch.

The Custom Sandbox Solution

Creating a custom sandbox gave me the flexibility I needed. I structured the implementation as follows:

1. Setting Up the Tab System

I built a tab-based UI using Radix UI to allow users to switch between:

  • Preview Mode: Displays the live rendering of the component.
  • Code Mode: Shows the JSX/TSX code for easy modification and copying.

This was implemented using samples-tabs.tsx:

<Tabs defaultValue="preview" onValueChange={handleTabChange} value={activeTab}>
  <TabsList>
    <TabsTrigger value="preview">Preview</TabsTrigger>
    <TabsTrigger value="code">Code</TabsTrigger>
  </TabsList>
  <TabsContent value="preview">
    <SamplesPreview component={component} />
  </TabsContent>
  <TabsContent value="code">
    <pre>
      <code>{component.code || "No code available"}</code>
    </pre>
  </TabsContent>
</Tabs>

This allows seamless switching between the preview and the code editor.

2. Implementing the Live Preview

The core of the live preview functionality is inside samples-preview.tsx. This file:

  • Embeds an iframe to isolate the preview environment.
  • Uses postMessage to send the user’s code into the iframe for execution.
  • Applies TailwindCSS dynamically to ensure styles are properly loaded.

Key implementation snippet:

const sendCodeToPreview = useCallback(() => {
  if (!component.code || !iframeRef.current?.contentWindow) return;

  const tailwindConfig = {
    content: [{ raw: component.code, extension: "tsx" }],
    theme: { extend: {} },
    plugins: [],
  };

  iframeRef.current?.contentWindow?.postMessage(
    { type: "LOAD_CODE", code: component.code, tailwindConfig },
    "*"
  );
}, [component.code]);

This ensures that any updates to the component code are instantly reflected in the preview.

3. Enabling Component Selection

One of my favorite features is component selection mode, which allows users to click on UI elements inside the preview to modify them directly. This required:

  • Listening for click events inside the iframe.
  • Sending the selected component’s tag name, text, and dimensions to the parent app.
  • Pre-filling a modification prompt so users can describe what changes they want.

Implemented using the handleMessage listener in samples-tabs.tsx:

useEffect(() => {
  const handleMessage = (event: MessageEvent) => {
    if (event.data.type === "COMPONENT_SELECTED") {
      setSelectedComponent({
        tagName: event.data.tagName,
        textContent: event.data.textContent,
        rect: event.data.rect,
      });
    }
  };
  window.addEventListener("message", handleMessage);
  return () => window.removeEventListener("message", handleMessage);
}, []);

This provides a smooth interactive experience where users can select and edit components on the fly.

4. Exporting Preview as an Image

To support preview exports, I implemented a message-based capture system. When the user clicks "Export," the iframe listens for a CAPTURE_PREVIEW request, takes a screenshot using html2canvas, and sends the image back to the main app for download.

Code snippet for handling the export:

const exportPreview = async (format: "png" | "jpeg" | "svg") => {
  setIsExporting(true);
  try {
    const iframe = document.querySelector("iframe");
    if (!iframe) throw new Error("Preview not found");

    iframe.contentWindow?.postMessage({ type: "CAPTURE_PREVIEW", format }, "*");
  } catch (error) {
    console.error("Export failed:", error);
  } finally {
    setIsExporting(false);
  }
};

This solution allows users to download their design snapshots in PNG, JPEG, or SVG formats, a feature that was impossible with Sandpack.

Final Thoughts

At first, I thought I could rely on Sandpack to get live preview up and running quickly, but I soon realized that the flexibility and control of a custom-built sandbox were worth the effort.

Now, GenieAI’s live preview is: ✅ Fully customizableMore performantSupports component selectionAllows exporting previews as images

By building my own sandbox, I eliminated third-party constraints and ensured the experience was tailored exactly to GenieAI’s needs.

If you're thinking about adding a live preview to your own project, my advice is: don’t be afraid to build your own solution if existing tools don’t meet your needs. It might take extra work, but the results are worth it!