> ## Documentation Index
> Fetch the complete documentation index at: https://prefab.prefect.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# File Upload

> Upload files and inspect their metadata — all client-side.

export const ComponentPreview = ({json, height, resizable, bare, hideJson, playground, children}) => {
  const hostRef = React.useRef(null);
  const handleRef = React.useRef(null);
  const cardRef = React.useRef(null);
  const containerRef = React.useRef(null);
  const [previewWidth, setPreviewWidth] = React.useState(null);
  const [isDragging, setIsDragging] = React.useState(false);
  const [playgroundHref, setPlaygroundHref] = React.useState(null);
  React.useEffect(function () {
    if (!playground) return;
    var isLocal = location.hostname === "localhost" || location.hostname === "127.0.0.1";
    var path = isLocal ? "/playground" : "/docs/playground";
    var url = new URL(path, window.location.href);
    url.hash = "code=" + playground;
    setPlaygroundHref(url.href);
  }, [playground]);
  var jsonStr = typeof json === "string" ? json : JSON.stringify(json, null, 2);
  React.useEffect(function () {
    if (cardRef.current) {
      var card = cardRef.current.closest(".group");
      if (card) card.classList.remove("group");
    }
    var host = hostRef.current;
    if (!host || !json) return;
    function mount() {
      if (!window.__prefab || !host) return;
      var dark = document.documentElement.classList.contains("dark");
      handleRef.current = window.__prefab.mountPreview(host, jsonStr, {
        dark: dark
      });
      if (bare && host.shadowRoot) {
        var m = host.shadowRoot.querySelector("[data-prefab-mount]");
        if (m) m.style.background = "transparent";
      }
    }
    if (window.__prefab) {
      mount();
    } else {
      if (!window.__prefabLoading) {
        if (!window.__prefabReady) {
          var s = document.createElement("script");
          s.src = "/renderer.js";
          document.head.appendChild(s);
        }
        window.__prefabLoading = new Promise(function (resolve) {
          function check() {
            if (window.__prefabReady) {
              window.__prefabReady.then(resolve);
            } else {
              setTimeout(check, 10);
            }
          }
          check();
        });
      }
      window.__prefabLoading.then(mount);
    }
    var observer = new MutationObserver(function () {
      var dark = document.documentElement.classList.contains("dark");
      if (handleRef.current) handleRef.current.setDark(dark);
    });
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["class"]
    });
    return function () {
      observer.disconnect();
      if (handleRef.current) {
        handleRef.current.unmount();
        handleRef.current = null;
      }
    };
  }, [json]);
  function startResize(e) {
    e.preventDefault();
    var startX = e.clientX;
    var startW = hostRef.current ? hostRef.current.offsetWidth : 0;
    var maxW = containerRef.current ? containerRef.current.offsetWidth - 10 : startW;
    setIsDragging(true);
    function onMove(ev) {
      var w = Math.max(260, Math.min(startW + (ev.clientX - startX), maxW));
      setPreviewWidth(w);
    }
    function onUp() {
      document.removeEventListener("mousemove", onMove);
      document.removeEventListener("mouseup", onUp);
      setIsDragging(false);
    }
    document.addEventListener("mousemove", onMove);
    document.addEventListener("mouseup", onUp);
  }
  if (bare) {
    return <div ref={hostRef} />;
  }
  if (resizable) {
    return <Card>
        <div ref={cardRef} />
        <div ref={containerRef} style={{
      display: "flex",
      position: "relative",
      overflow: "hidden"
    }}>
          <div ref={hostRef} style={{
      flex: previewWidth ? "none" : 1,
      width: previewWidth ? previewWidth + "px" : undefined,
      minWidth: 0
    }} />
          <div onMouseDown={startResize} style={{
      width: "14px",
      flexShrink: 0,
      cursor: "col-resize",
      background: isDragging ? "var(--border, #e5e7eb)" : "color-mix(in srgb, var(--border, #e5e7eb) 40%, transparent)",
      display: "flex",
      alignItems: "center",
      justifyContent: "center",
      transition: "background 0.15s",
      userSelect: "none"
    }}>
            <svg width="6" height="24" style={{
      opacity: 0.5
    }}>
              <circle cx="2" cy="4" r="1.2" fill="currentColor" />
              <circle cx="2" cy="9" r="1.2" fill="currentColor" />
              <circle cx="2" cy="14" r="1.2" fill="currentColor" />
              <circle cx="2" cy="19" r="1.2" fill="currentColor" />
              <circle cx="5" cy="4" r="1.2" fill="currentColor" />
              <circle cx="5" cy="9" r="1.2" fill="currentColor" />
              <circle cx="5" cy="14" r="1.2" fill="currentColor" />
              <circle cx="5" cy="19" r="1.2" fill="currentColor" />
            </svg>
          </div>
          <div style={{
      flex: previewWidth ? 1 : "0 0 24px",
      minWidth: "24px",
      background: "repeating-linear-gradient(-45deg, transparent, transparent 3px, var(--border, #e5e7eb) 3px, var(--border, #e5e7eb) 4px)",
      opacity: 0.4
    }} />
          {previewWidth && <div style={{
      position: "absolute",
      bottom: "8px",
      right: "8px",
      background: "var(--background, white)",
      border: "1px solid var(--border, #e5e7eb)",
      borderRadius: "4px",
      padding: "1px 6px",
      fontSize: "11px",
      fontFamily: "monospace",
      color: "var(--muted-foreground, #6b7280)",
      pointerEvents: "none"
    }}>
              {previewWidth}px
            </div>}
        </div>
        {playgroundHref && <div style={{
      display: "flex",
      justifyContent: "flex-end",
      padding: "4px 0 8px"
    }}>
            <a href={playgroundHref} target="_blank" rel="noopener noreferrer" style={{
      fontSize: "11px",
      opacity: 0.4,
      textDecoration: "none",
      textDecorationLine: "none",
      borderBottom: "none",
      boxShadow: "none",
      display: "inline-flex",
      alignItems: "center",
      gap: "3px",
      transition: "opacity 0.15s",
      fontWeight: 400
    }} onMouseOver={function (e) {
      e.currentTarget.style.opacity = "0.6";
    }} onMouseOut={function (e) {
      e.currentTarget.style.opacity = "0.4";
    }}>
              Edit in Playground
              <svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M15 3h6v6" /><path d="M10 14 21 3" /><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></svg>
            </a>
          </div>}
        <div style={{
      marginBottom: "-2rem"
    }}>{children}</div>
      </Card>;
  }
  return <Card>
      <div ref={cardRef} />
      <div ref={hostRef} />
      {playgroundHref && <div style={{
    display: "flex",
    justifyContent: "flex-end",
    padding: "4px 0 8px"
  }}>
          <a href={playgroundHref} target="_blank" rel="noopener noreferrer" style={{
    fontSize: "11px",
    opacity: 0.4,
    textDecoration: "none",
    textDecorationLine: "none",
    borderBottom: "none",
    boxShadow: "none",
    display: "inline-flex",
    alignItems: "center",
    gap: "3px",
    transition: "opacity 0.15s",
    fontWeight: 400
  }} onMouseOver={function (e) {
    e.currentTarget.style.opacity = "0.6";
  }} onMouseOut={function (e) {
    e.currentTarget.style.opacity = "0.4";
  }}>
            Edit in Playground
            <svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M15 3h6v6" /><path d="M10 14 21 3" /><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></svg>
          </a>
        </div>}
      <div style={{
    marginBottom: "-2rem"
  }}>{children}</div>
    </Card>;
};

MCP servers typically receive files only through the LLM conversation — every byte burns tokens and inflates latency. File upload components let users send files directly to your backend tools via `CallTool`, keeping binary data out of the context window entirely. This example stays fully client-side to demonstrate the UI mechanics, but in practice you'd wire the DropZone's `on_change` (or a button's `on_click`) to a `CallTool` that sends the base64 payload to your server for processing.

<ComponentPreview json={{"view":{"cssClass":"w-full max-w-2xl","type":"Card","children":[{"type":"CardHeader","children":[{"cssClass":"gap-2 items-center","type":"Row","children":[{"content":"File Inbox","type":"H3"},{"type":"Condition","cases":[{"when":"{{ files | length }}","children":[{"type":"Badge","label":"{{ files | length }}","variant":"secondary"}]}]}]}]},{"type":"CardContent","children":[{"cssClass":"gap-4","type":"Column","children":[{"name":"files","type":"DropZone","icon":"inbox","label":"Drop files here","description":"Don't worry, files aren't actually uploaded","multiple":true,"maxSize":10000000,"disabled":false},{"type":"Condition","cases":[{"when":"{{ files | length }}","children":[{"cssClass":"gap-2","type":"Column","children":[{"let":{"_loop_10":"{{ $item }}","_loop_10_idx":"{{ $index }}"},"type":"ForEach","key":"files","children":[{"cssClass":"gap-3 items-center","type":"Row","children":[{"cssClass":"gap-0","type":"Column","children":[{"content":"{{ _loop_10.name }}","type":"Small"},{"content":"{{ _loop_10.type }} \u00b7 {{ _loop_10.size }} bytes","type":"Muted"}]},{"cssClass":"ml-auto h-6 w-6 p-0 text-muted-foreground","type":"Button","label":"\u00d7","variant":"ghost","size":"sm","disabled":false,"onClick":{"action":"popState","key":"files","index":"{{ _loop_10_idx }}"}}]}]}]}]}]}]}]},{"type":"CardFooter","children":[{"cssClass":"items-center w-full","type":"Row","children":[{"type":"Condition","cases":[{"when":"{{ files | length }}","children":[{"content":"{{ files | length }} {{ files | length | pluralize:file }}","type":"Muted"}]}],"else":[{"content":"No files uploaded","type":"Muted"}]},{"type":"Condition","cases":[{"when":"{{ files | length }}","children":[{"cssClass":"ml-auto","type":"Button","label":"Clear all","variant":"outline","size":"sm","disabled":false,"onClick":{"action":"setState","key":"files","value":[]}}]}]}]}]}]}}} playground="ZnJvbSBwcmVmYWJfdWkuY29tcG9uZW50cyBpbXBvcnQgKAogICAgU1RBVEUgYXMgc3RhdGUsIEJhZGdlLCBCdXR0b24sIENhcmQsIENhcmRDb250ZW50LCBDYXJkRm9vdGVyLAogICAgQ2FyZEhlYWRlciwgQ29sdW1uLCBEcm9wWm9uZSwKICAgIEgzLCBNdXRlZCwgUm93LCBTbWFsbCwKKQpmcm9tIHByZWZhYl91aS5jb21wb25lbnRzLmNvbnRyb2xfZmxvdyBpbXBvcnQgRWxzZSwgRm9yRWFjaCwgSWYKZnJvbSBwcmVmYWJfdWkuYWN0aW9ucyBpbXBvcnQgUG9wU3RhdGUsIFNldFN0YXRlCgoKd2l0aCBDYXJkKGNzc19jbGFzcz0idy1mdWxsIG1heC13LTJ4bCIpIGFzIHZpZXc6CiAgICB3aXRoIENhcmRIZWFkZXIoKToKICAgICAgICB3aXRoIFJvdyhnYXA9MiwgYWxpZ249ImNlbnRlciIpOgogICAgICAgICAgICBIMygiRmlsZSBJbmJveCIpCiAgICAgICAgICAgIHdpdGggSWYoc3RhdGUuZmlsZXMubGVuZ3RoKCkpOgogICAgICAgICAgICAgICAgQmFkZ2Uoc3RhdGUuZmlsZXMubGVuZ3RoKCksIHZhcmlhbnQ9InNlY29uZGFyeSIpCgogICAgd2l0aCBDYXJkQ29udGVudCgpOgogICAgICAgIHdpdGggQ29sdW1uKGdhcD00KToKICAgICAgICAgICAgRHJvcFpvbmUoCiAgICAgICAgICAgICAgICBpY29uPSJpbmJveCIsCiAgICAgICAgICAgICAgICBsYWJlbD0iRHJvcCBmaWxlcyBoZXJlIiwKICAgICAgICAgICAgICAgIGRlc2NyaXB0aW9uPSJEb24ndCB3b3JyeSwgZmlsZXMgYXJlbid0IGFjdHVhbGx5IHVwbG9hZGVkIiwKICAgICAgICAgICAgICAgIG11bHRpcGxlPVRydWUsCiAgICAgICAgICAgICAgICBtYXhfc2l6ZT0xMF8wMDBfMDAwLAogICAgICAgICAgICAgICAgbmFtZT0iZmlsZXMiLAogICAgICAgICAgICApCgogICAgICAgICAgICB3aXRoIElmKHN0YXRlLmZpbGVzLmxlbmd0aCgpKToKICAgICAgICAgICAgICAgIHdpdGggQ29sdW1uKGdhcD0yKToKICAgICAgICAgICAgICAgICAgICB3aXRoIEZvckVhY2goImZpbGVzIikgYXMgKGksIGl0ZW0pOgogICAgICAgICAgICAgICAgICAgICAgICB3aXRoIFJvdyhnYXA9MywgYWxpZ249ImNlbnRlciIpOgogICAgICAgICAgICAgICAgICAgICAgICAgICAgd2l0aCBDb2x1bW4oZ2FwPTApOgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIFNtYWxsKGl0ZW0ubmFtZSkKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBNdXRlZChmIntpdGVtLnR5cGV9IMK3IHtpdGVtLnNpemV9IGJ5dGVzIikKICAgICAgICAgICAgICAgICAgICAgICAgICAgIEJ1dHRvbigKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiw5ciLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHZhcmlhbnQ9Imdob3N0IiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzaXplPSJzbSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY3NzX2NsYXNzPSgKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIm1sLWF1dG8gaC02IHctNiBwLTAiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICIgdGV4dC1tdXRlZC1mb3JlZ3JvdW5kIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgb25fY2xpY2s9UG9wU3RhdGUoImZpbGVzIiwgaSksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICApCgogICAgd2l0aCBDYXJkRm9vdGVyKCk6CiAgICAgICAgd2l0aCBSb3coYWxpZ249ImNlbnRlciIsIGNzc19jbGFzcz0idy1mdWxsIik6CiAgICAgICAgICAgIHdpdGggSWYoc3RhdGUuZmlsZXMubGVuZ3RoKCkpOgogICAgICAgICAgICAgICAgTXV0ZWQoCiAgICAgICAgICAgICAgICAgICAgZiJ7c3RhdGUuZmlsZXMubGVuZ3RoKCl9IgogICAgICAgICAgICAgICAgICAgIGYiIHtzdGF0ZS5maWxlcy5sZW5ndGgoKS5wbHVyYWxpemUoJ2ZpbGUnKX0iCiAgICAgICAgICAgICAgICApCiAgICAgICAgICAgIHdpdGggRWxzZSgpOgogICAgICAgICAgICAgICAgTXV0ZWQoIk5vIGZpbGVzIHVwbG9hZGVkIikKICAgICAgICAgICAgd2l0aCBJZihzdGF0ZS5maWxlcy5sZW5ndGgoKSk6CiAgICAgICAgICAgICAgICBCdXR0b24oCiAgICAgICAgICAgICAgICAgICAgIkNsZWFyIGFsbCIsCiAgICAgICAgICAgICAgICAgICAgdmFyaWFudD0ib3V0bGluZSIsCiAgICAgICAgICAgICAgICAgICAgc2l6ZT0ic20iLAogICAgICAgICAgICAgICAgICAgIGNzc19jbGFzcz0ibWwtYXV0byIsCiAgICAgICAgICAgICAgICAgICAgb25fY2xpY2s9U2V0U3RhdGUoImZpbGVzIiwgW10pLAogICAgICAgICAgICAgICAgKQo">
  <CodeGroup>
    ```python Python expandable icon="python" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    from prefab_ui.components import (
        STATE as state, Badge, Button, Card, CardContent, CardFooter,
        CardHeader, Column, DropZone,
        H3, Muted, Row, Small,
    )
    from prefab_ui.components.control_flow import Else, ForEach, If
    from prefab_ui.actions import PopState, SetState


    with Card(css_class="w-full max-w-2xl") as view:
        with CardHeader():
            with Row(gap=2, align="center"):
                H3("File Inbox")
                with If(state.files.length()):
                    Badge(state.files.length(), variant="secondary")

        with CardContent():
            with Column(gap=4):
                DropZone(
                    icon="inbox",
                    label="Drop files here",
                    description="Don't worry, files aren't actually uploaded",
                    multiple=True,
                    max_size=10_000_000,
                    name="files",
                )

                with If(state.files.length()):
                    with Column(gap=2):
                        with ForEach("files") as (i, item):
                            with Row(gap=3, align="center"):
                                with Column(gap=0):
                                    Small(item.name)
                                    Muted(f"{item.type} · {item.size} bytes")
                                Button(
                                    "×",
                                    variant="ghost",
                                    size="sm",
                                    css_class=(
                                        "ml-auto h-6 w-6 p-0"
                                        " text-muted-foreground"
                                    ),
                                    on_click=PopState("files", i),
                                )

        with CardFooter():
            with Row(align="center", css_class="w-full"):
                with If(state.files.length()):
                    Muted(
                        f"{state.files.length()}"
                        f" {state.files.length().pluralize('file')}"
                    )
                with Else():
                    Muted("No files uploaded")
                with If(state.files.length()):
                    Button(
                        "Clear all",
                        variant="outline",
                        size="sm",
                        css_class="ml-auto",
                        on_click=SetState("files", []),
                    )
    ```

    ```json Protocol icon="brackets-curly" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    {
      "view": {
        "cssClass": "w-full max-w-2xl",
        "type": "Card",
        "children": [
          {
            "type": "CardHeader",
            "children": [
              {
                "cssClass": "gap-2 items-center",
                "type": "Row",
                "children": [
                  {"content": "File Inbox", "type": "H3"},
                  {
                    "type": "Condition",
                    "cases": [
                      {
                        "when": "{{ files | length }}",
                        "children": [{"type": "Badge", "label": "{{ files | length }}", "variant": "secondary"}]
                      }
                    ]
                  }
                ]
              }
            ]
          },
          {
            "type": "CardContent",
            "children": [
              {
                "cssClass": "gap-4",
                "type": "Column",
                "children": [
                  {
                    "name": "files",
                    "type": "DropZone",
                    "icon": "inbox",
                    "label": "Drop files here",
                    "description": "Don't worry, files aren't actually uploaded",
                    "multiple": true,
                    "maxSize": 10000000,
                    "disabled": false
                  },
                  {
                    "type": "Condition",
                    "cases": [
                      {
                        "when": "{{ files | length }}",
                        "children": [
                          {
                            "cssClass": "gap-2",
                            "type": "Column",
                            "children": [
                              {
                                "let": {"_loop_10": "{{ $item }}", "_loop_10_idx": "{{ $index }}"},
                                "type": "ForEach",
                                "key": "files",
                                "children": [
                                  {
                                    "cssClass": "gap-3 items-center",
                                    "type": "Row",
                                    "children": [
                                      {
                                        "cssClass": "gap-0",
                                        "type": "Column",
                                        "children": [
                                          {"content": "{{ _loop_10.name }}", "type": "Small"},
                                          {
                                            "content": "{{ _loop_10.type }} \u00b7 {{ _loop_10.size }} bytes",
                                            "type": "Muted"
                                          }
                                        ]
                                      },
                                      {
                                        "cssClass": "ml-auto h-6 w-6 p-0 text-muted-foreground",
                                        "type": "Button",
                                        "label": "\u00d7",
                                        "variant": "ghost",
                                        "size": "sm",
                                        "disabled": false,
                                        "onClick": {"action": "popState", "key": "files", "index": "{{ _loop_10_idx }}"}
                                      }
                                    ]
                                  }
                                ]
                              }
                            ]
                          }
                        ]
                      }
                    ]
                  }
                ]
              }
            ]
          },
          {
            "type": "CardFooter",
            "children": [
              {
                "cssClass": "items-center w-full",
                "type": "Row",
                "children": [
                  {
                    "type": "Condition",
                    "cases": [
                      {
                        "when": "{{ files | length }}",
                        "children": [
                          {
                            "content": "{{ files | length }} {{ files | length | pluralize:file }}",
                            "type": "Muted"
                          }
                        ]
                      }
                    ],
                    "else": [{"content": "No files uploaded", "type": "Muted"}]
                  },
                  {
                    "type": "Condition",
                    "cases": [
                      {
                        "when": "{{ files | length }}",
                        "children": [
                          {
                            "cssClass": "ml-auto",
                            "type": "Button",
                            "label": "Clear all",
                            "variant": "outline",
                            "size": "sm",
                            "disabled": false,
                            "onClick": {"action": "setState", "key": "files", "value": []}
                          }
                        ]
                      }
                    ]
                  }
                ]
              }
            ]
          }
        ]
      }
    }
    ```
  </CodeGroup>
</ComponentPreview>

## How It Works

The [DropZone](/docs/components/drop-zone) is bound to state by its `name="files"` prop. When a user drops files, the renderer reads each one and writes a list of file descriptors into the `files` [state](/docs/concepts/state) key — each entry is an object with `name`, `type`, and `size` fields, plus the base64-encoded contents. Everything else on the page is a reactive function of that list. `STATE as state` imports the state accessor so the Python reads naturally as `state.files`, and `state.files.length()` compiles to a [reactive expression](/docs/concepts/expressions) the renderer re-evaluates whenever the list changes.

That length expression drives the page's conditional structure. The header [Badge](/docs/components/badge), the file rows, and the footer message are each wrapped in `If(state.files.length())`, so they appear only once at least one file is present; the `Else()` branch in the footer shows "No files uploaded" otherwise. The file rows themselves come from [`ForEach`](/docs/components/foreach): `with ForEach("files") as (i, item):` walks the list, exposing the loop index `i` and the current descriptor `item`, so `item.name`, `item.type`, and `item.size` render each file's metadata. The per-row `×` button calls `PopState("files", i)` to drop that one file by index, and `Clear all` calls `SetState("files", [])` to empty the list — both client-side [actions](/docs/concepts/actions), so add/remove updates the count, the badge, and the rows with no server involved.

This example stays entirely in the browser to show the UI mechanics, which is why the DropZone is labeled "files aren't actually uploaded." The reason file uploads matter for MCP is the [client/server boundary](/docs/concepts/actions). When an LLM passes a file through the conversation, every byte is tokenized and counted against the context window. A DropZone sidesteps that: the file lands in client state as base64, and you forward it straight to your backend with a [CallTool](/docs/actions/call-tool) action — wiring the DropZone's `on_change` or a submit button's `on_click` to `CallTool(...)` with the payload from `state.files`. The tool runs server-side, processes the bytes, and returns a result; you read `$result` in the action's `on_success` and write it back into state to update the UI. The binary data travels client → server directly and never enters the model's context.
