Skip to main content
Don’t panic. Prefab is under extremely active development. You probably shouldn’t use it yet.
Prefab is a frontend framework with a Python DSL that compiles to JSON. Describe a UI — layouts, forms, charts, data tables, full interactivity — and a bundled React renderer turns that JSON into a self-contained application. Composing frontends in Python is blasphemous surprisingly natural. And because it’s a JSON protocol, any source can produce a Prefab UI. Write one in Python, serve one as an MCP App, or let an agent generate one dynamically — no templates or predefined views required.
Every component above was generated from the following Python code.
In fact, every example in the Prefab docs is rendered by Prefab itself. The Python code and corresponding Prefab protocol JSON are always shown below the example for reference.
from prefab_ui.components import *
from prefab_ui.components.charts import *
from prefab_ui.components.control_flow import Else, If
from prefab_ui.actions import *

with Grid(columns={"default": 1, "md": 2, "lg": 4}, gap=4):

    # ── Col 1 ─────────────────────────────────────────────────────────
    with Column(gap=4):
        with Card():
            with CardHeader():
                CardTitle("Register Towel")
                CardDescription("The most important item in the galaxy")
            with CardContent():
                with Column(gap=3):
                    owner_input = Input(
                        placeholder="Owner name...", name="owner",
                    )
                    with Combobox(
                        placeholder="Type...",
                        search_placeholder="Search types...",
                    ):
                        ComboboxOption("Bath", value="bath")
                        ComboboxOption("Beach", value="beach")
                        ComboboxOption("Interstellar", value="interstellar")
                        ComboboxOption("Microfiber", value="micro")
                    DatePicker(placeholder="Registration date")
            with CardFooter():
                with Row(gap=2):
                    with Dialog(
                        title="Towel Registered!",
                        description=(
                            "Your towel has been added to the "
                            "galactic registry."
                        ),
                    ):
                        Button("Register")
                        with If("{{ owner }}"):
                            Text(
                                f"Thanks, {owner_input.rx}. "
                                "Don't forget to bring it."
                            )
                        with Else():
                            Text(
                                "Anonymous, I see? "
                                "Don't forget to bring it."
                            )
                    Button("Cancel", variant="outline")
        with Card():
            with CardHeader():
                CardTitle("Ship Status")
            with CardContent():
                with Column(gap=3):
                    with Row(
                        align="center", css_class="justify-between",
                    ):
                        Text("heart-of-gold")
                        with HoverCard(open_delay=0, close_delay=200):
                            Badge("In Orbit", variant="default")
                            with Column(gap=2):
                                Text("heart-of-gold")
                                Muted("Deployed 2h ago")
                                Progress(
                                    value=100, max=100, variant='success'
                                )
                    Progress(
                        value=100, max=100,
                        indicator_class="bg-yellow-400",
                    )
                    with Row(
                        align="center", css_class="justify-between",
                    ):
                        Text("vogon-poetry")
                        with Tooltip("64% — ETA 12 min", delay=0):
                            with Badge(variant="secondary"):
                                Loader(size="sm")
                                Text("Deploying")
                    Progress(value=64, max=100)
                    with Row(
                        align="center", css_class="justify-between",
                    ):
                        Text("deep-thought")
                        with Tooltip(
                            "Computing... 7.5 million years remaining",
                            delay=0,
                        ):
                            with Badge( variant="outline"):
                                Loader(size="sm", variant="ios")
                                Text("Soon...")
                    Progress(value=12, max=100)
        with Card():
            with CardHeader():
                CardTitle("Planet Ratings")
            with CardContent():
                RadarChart(
                    data=[
                        {"axis": "Views", "earth": 30, "mag": 95},
                        {"axis": "Fjords", "earth": 65, "mag": 100},
                        {"axis": "Pubs", "earth": 90, "mag": 10},
                        {"axis": "Mice", "earth": 40, "mag": 85},
                        {"axis": "Tea", "earth": 95, "mag": 15},
                        {"axis": "Safety", "earth": 45, "mag": 70},
                    ],
                    series=[
                        ChartSeries(
                            dataKey="earth", label="Earth",
                        ),
                        ChartSeries(
                            dataKey="mag", label="Magrathea",
                        ),
                    ],
                    axis_key="axis", height=200,
                    show_legend=True, show_tooltip=True,
                )
    # ── Col 2 ─────────────────────────────────────────────────────────
    with Column(gap=4):
        with Card():
            with CardHeader():
                CardTitle("Survival Odds")
            with CardContent(css_class="w-fit mx-auto"):
                Ring(
                    value=42, label="42%",
                    variant="info", size="lg",
                    thickness=12,
                    indicator_class="group-hover:drop-shadow-[0_0_24px_rgba(59,130,246,0.9)]",
                )
        with Card():
            with CardHeader():
                with Row(gap=2, align="center"):
                    CardTitle("Improbability Drive")
                    Loader(
                        variant="pulse", size="sm",
                        css_class="text-blue-500",
                    )
            with CardContent():
                with Column(gap=2):
                    Slider(
                        min=0, max=100, value=42, name="improbability",
                    )
                    with Row(
                        align="center", css_class="justify-between",
                    ):
                        Muted("Probable")
                        Muted("Infinite")
        with Alert(variant="success", icon="circle-check"):
            AlertTitle("Don't Panic")
            AlertDescription(
                "Normality achieved.",
            )
        with Card():
            with CardHeader():
                CardTitle("Prefect Horizon Config")
            with CardContent():
                with Column(gap=3):
                    Switch(
                        label="Auto-scale agents", checked=True,
                        name="autoscale",
                    )
                    Separator()
                    Switch(
                        label="Code Mode", checked=True,
                        name="code_mode",
                    )
                    Separator()
                    Switch(
                        label="Tool call caching", checked=False,
                        name="cache",
                    )
            with CardFooter():
                Button(
                    "Save Preferences",
                    on_click=ShowToast("Preferences saved!"),
                )
        with Card():
            with CardHeader():
                CardTitle("Travel Class")
            with CardContent():
                with RadioGroup(name="travel_class"):
                    Radio(value="economy", label="Economy")
                    Radio(
                        value="business", label="Business Class",
                    )
                    Radio(
                        value="improbability",
                        label="Infinite Improbability",
                        checked=True,
                    )

    # ── Cols 3–4: summary row, chart, then 2-col grid below ─────────
    with GridItem(col_span=2):
        with Column(gap=4):
            with Grid(columns=2, gap=4):
                with Card():
                    with CardHeader():
                        with Row(gap=2, align="center"):
                            CardTitle("Context Window")
                            Loader(variant="bars", size="sm")
                    with CardContent():
                        with Column(gap=2):
                            with Row(
                                align="center",
                                css_class="justify-between",
                            ):
                                Text("45% used")
                                Muted("90k / 200k tokens")
                            with Tooltip(
                                "Auto-compact buffer: 12%",
                                delay=0,
                            ):
                                Progress(value=45, max=100)
                with Card():
                    with CardContent():
                        Metric(
                            label="Fjords designed",
                            value="1,847",
                            delta="+3 coastlines",
                        )
            with Card():
                with CardHeader():
                    CardTitle("Towel Incidents")
                with CardContent():
                    BarChart(
                        data=[
                            {"month": "Jan", "lost": 8, "found": 5},
                            {"month": "Feb", "lost": 24, "found": 15},
                            {"month": "Mar", "lost": 12, "found": 28},
                            {"month": "Apr", "lost": 35, "found": 19},
                            {"month": "May", "lost": 18, "found": 38},
                            {"month": "Jun", "lost": 42, "found": 30},
                        ],
                        series=[
                            ChartSeries(
                                dataKey="lost", label="Lost",
                            ),
                            ChartSeries(
                                dataKey="found", label="Found",
                            ),
                        ],
                        x_axis="month", height=200, bar_radius=4,
                        show_legend=True, show_tooltip=True,
                        show_grid=True,
                    )

            with Grid(columns=2, gap=4):
                with Column(gap=4):
                    with Card():
                        with CardContent():
                            with Column(gap=2):
                                Checkbox(
                                    label="Towel packed",
                                    checked=True,
                                )
                                Checkbox(
                                    label="Guide charged",
                                    checked=True,
                                )
                                Checkbox(
                                    label="Babel fish inserted",
                                    checked=False,
                                )
                    with Card():
                        with CardHeader():
                            CardTitle("Marvin's Mood")
                        with CardContent():
                            with Column(gap=3):
                                P(
                                    "How's life?"
                                )
                                with Row(gap=2, align="center"):
                                    Loader(variant="dots", size="sm")
                                    Muted("Marvin is thinking...")
                                with Column(gap=2):
                                    Button(
                                        "Meh",
                                        on_click=ShowToast(
                                            "Noted. Enthusiasm "
                                            "levels nominal."
                                        ),
                                    )
                                    Button(
                                        "Depressed",
                                        variant="secondary",
                                        on_click=ShowToast(
                                            "I think you ought to "
                                            "know I'm feeling very "
                                            "depressed."
                                        ),
                                    )
                                    Button(
                                        "Don't talk to me about life",
                                        variant="outline",
                                        on_click=ShowToast(
                                            "Brain the size of a "
                                            "planet and they ask me "
                                            "to pick up a piece of "
                                            "paper."
                                        ),
                                    )
                    with Alert(variant="destructive", icon="triangle-alert"):
                        AlertTitle("Beware of the Leopard")
                        

                with Column(gap=4):
                    with Column(gap=3):
                        with If("{{ !pressed }}"):
                            Button(
                                "This is probably the best "
                                "button to press.",
                                variant="info",
                                on_click=SetState(
                                    "pressed", True,
                                ),
                            )
                        with Else():
                            Button(
                                "Please do not press this "
                                "button again.",
                                variant="destructive",
                                on_click=SetState(
                                    "pressed", False,
                                ),
                            )
                    with Card():
                        with CardContent():
                            DataTable(
                                columns=[
                                    DataTableColumn(
                                        key="crew", header="Crew",
                                        sortable=True,
                                    ),
                                    DataTableColumn(
                                        key="species",
                                        header="Species",
                                        sortable=True,
                                    ),
                                    DataTableColumn(
                                        key="towel", header="Towel?",
                                        sortable=True,
                                    ),
                                    DataTableColumn(
                                        key="status",
                                        header="Status",
                                        sortable=True,
                                    ),
                                ],
                                rows=[
                                    {"crew": "Arthur Dent", "species": "Human",
                                     "towel": "Yes", "status": "Confused"},
                                    {"crew": "Ford Prefect", "species": "Betelgeusian",
                                     "towel": "Always", "status": "Drinking"},
                                    {"crew": "Zaphod", "species": "Betelgeusian",
                                     "towel": "Lost it", "status": "Presidential"},
                                    {"crew": "Trillian", "species": "Human",
                                     "towel": "Yes", "status": "Navigating"},
                                    {"crew": "Marvin", "species": "Android",
                                     "towel": "No point", "status": "Depressed"},
                                    {"crew": "Slartibartfast", "species": "Magrathean",
                                     "towel": "Somewhere", "status": "Designing"},
                                ],
                                searchable=True, paginated=False,
                            )
                    with Card():
                        with CardHeader():
                            CardTitle("MCP Servers")
                            CardDescription(
                                "Deployed on Horizon",
                            )
                        with CardContent():
                            with Column(gap=2):
                                with Row(
                                    align="center",
                                    css_class="justify-between",
                                ):
                                    Text("weather-api")
                                    with Tooltip(
                                        "Uptime: 99.97%",
                                        delay=0,
                                    ):
                                        Badge(
                                            "Healthy",
                                            variant="default",
                                        )
                                Separator()
                                with Row(
                                    align="center",
                                    css_class="justify-between",
                                ):
                                    Text("doc-search")
                                    with Tooltip(
                                        "Uptime: 99.94%",
                                        delay=0,
                                    ):
                                        Badge(
                                            "Healthy",
                                            variant="default",
                                        )
                                Separator()
                                with Row(
                                    align="center",
                                    css_class="justify-between",
                                ):
                                    Text("marvin-brain")
                                    with HoverCard(
                                        open_delay=0,
                                        close_delay=200,
                                    ):
                                        Badge(
                                            "Depressed",
                                            variant="secondary",
                                        )
                                        with Column(gap=2):
                                            Text("marvin-brain")
                                            Muted(
                                                "Uptime: 100% — "
                                                "not that it "
                                                "matters"
                                            )
                                            Progress(
                                                value=100,
                                                max=100,
                                            )

Why Prefab

Prefab began as a DSL for building MCP Apps as part of FastMCP. MCP Apps are an extension of the Model Context Protocol that lets MCP servers render fully interactive UIs directly inside the chat — dashboards, forms, data tables, visualizations — not as static text, but as live interfaces the user can interact with while talking to an agent. Building an MCP App means shipping a self-contained UI bundle. For TypeScript SDK users, that’s natural — they’re already in the JavaScript ecosystem. But the vast majority of MCP servers are written in Python, and for those developers, building an MCP App means spinning up an entirely new project in an entirely new language, tightly coupled to their server with no shared types or enforcement. Prefab eliminates that gap entirely: describe your UI in Python, right next to your server logic, and Prefab compiles it to a production React bundle.

The design philosophy

The goal isn’t to port React to Python. Developers who want to build rich, bespoke frontends will — and should — reach for the best tools in the JavaScript ecosystem. Prefab is built on a different insight: the vast majority of interactive experiences appropriate for MCP are not full-fledged web applications. They are forms of information delivery, visualization, and collection. When an agent pulls a large dataset from a database, the user today must choose between asking the agent to summarize it (losing detail) or dumping it into the chat (losing structure). An interactive data table with sorting and filtering is a better answer. When an agent needs structured input, a form beats a multi-turn conversation. When results need context, summary cards and charts communicate more than paragraphs of text. These use cases don’t need the infinite complexity of a full frontend stack. They need a relatively small number of composable components — layout, typography, forms, data display, charts — with enough interactivity for data delivery and collection to cover the vast majority of scenarios. Prefab is a JSON protocol by design, not just a Python library. This means agents can dynamically construct new UIs on the fly — rendering interfaces that no developer predefined. An agent can decide, mid-conversation, to show a chart, build a form, or compose a dashboard from components, all by producing JSON that the renderer already knows how to paint.

The landscape

There is a rich ecosystem of projects bringing frontend development to Python — Streamlit, Gradio, NiceGUI, Reflex, and many others. Most assume a running Python server that manages state and serves the UI. FastUI pioneered a different approach: a declarative component protocol rendered by a separate frontend. Prefab builds directly on that insight, from a different starting point — MCP and agent-native use cases where the UI may not have a server at all. The renderer compiles JSON to real React — production components with state management, forms, and declarative interactivity — and ships as a self-contained static bundle. The Python side has no transport dependencies: it works with MCP servers via FastMCP, with REST APIs via FastAPI, or with anything else that can produce JSON. Prefab is made with 💙 by Prefect.

How It Works

The core idea is a pipeline: JSON → React. For Python authors, it’s Python → JSON → React.
  1. A component tree is described — in Python via the DSL, or as raw JSON from any source
  2. The tree serializes to Prefab’s JSON wire format
  3. A React renderer (shipped as @prefecthq/prefab-ui on npm) compiles the JSON into a live interface
State flows through templates. When you write {{ query }}, the renderer interpolates the current value of query from client-side state. Actions like CallTool and SetState update that state, keeping the UI reactive. Here’s a simpler example — type in the input and watch the heading update in real time:

Installation

pip install prefab-ui
Prefab requires Python 3.10+. See Installation for version pinning guidance.

What’s in the Box