---
title: DropZone
description: Drag-and-drop file upload area.
icon: cloud-arrow-up
---

import { ComponentPreview } from "/snippets/component-preview.mdx";

A styled drag-and-drop area for file uploads. Users can drop files onto it or click to browse — either way, the selected files are read client-side, converted to base64, and passed through the action system as `$event`. No server round-trip until you want one.

<Info>
  All file reading happens in the browser. Files are never sent to a server unless you explicitly wire up an action (like `CallTool`) to do so.
</Info>

If you don't need a dedicated upload area — say you just want an "Upload" button — see the [OpenFilePicker](/actions/open-file-picker) action instead. It turns any clickable element into a file input.

## Basic Usage

With no configuration, DropZone renders a full-width dashed upload area that accepts a single file of any type.

<ComponentPreview json={{"view":{"name":"dropzone_1","type":"DropZone","multiple":false,"disabled":false},"state":{"dropzone_1":[]}}} playground="ZnJvbSBwcmVmYWJfdWkuY29tcG9uZW50cyBpbXBvcnQgRHJvcFpvbmUKCkRyb3Bab25lKCkK">
<CodeGroup>
```python Python icon="python"
from prefab_ui.components import DropZone

DropZone()
```
```json Protocol icon="brackets-curly"
{
  "view": {"name": "dropzone_1", "type": "DropZone", "multiple": false, "disabled": false},
  "state": {"dropzone_1": []}
}
```
</CodeGroup>
</ComponentPreview>

## Customizing the Prompt

The `label` sets the primary prompt text and `description` adds secondary helper text below it — useful for hinting at accepted formats or size limits. You can also swap the default upload icon for any [Lucide icon](https://lucide.dev/icons) via the `icon` prop.

<ComponentPreview json={{"view":{"name":"dropzone_2","type":"DropZone","icon":"image","label":"Drop images here","description":"PNG, JPG, or GIF up to 10MB","multiple":false,"disabled":false},"state":{"dropzone_2":[]}}} playground="ZnJvbSBwcmVmYWJfdWkuY29tcG9uZW50cyBpbXBvcnQgRHJvcFpvbmUKCkRyb3Bab25lKAogICAgaWNvbj0iaW1hZ2UiLAogICAgbGFiZWw9IkRyb3AgaW1hZ2VzIGhlcmUiLAogICAgZGVzY3JpcHRpb249IlBORywgSlBHLCBvciBHSUYgdXAgdG8gMTBNQiIsCikK">
<CodeGroup>
```python Python icon="python"
from prefab_ui.components import DropZone

DropZone(
    icon="image",
    label="Drop images here",
    description="PNG, JPG, or GIF up to 10MB",
)
```
```json Protocol icon="brackets-curly"
{
  "view": {
    "name": "dropzone_2",
    "type": "DropZone",
    "icon": "image",
    "label": "Drop images here",
    "description": "PNG, JPG, or GIF up to 10MB",
    "multiple": false,
    "disabled": false
  },
  "state": {"dropzone_2": []}
}
```
</CodeGroup>
</ComponentPreview>

## Accepted File Types

The `accept` parameter restricts which files the browser will allow. It works just like the HTML `accept` attribute — MIME types (`"image/*"`), extensions (`".csv"`), or a comma-separated mix of both.

<ComponentPreview json={{"view":{"name":"dropzone_3","type":"DropZone","label":"Upload documents","description":"PDF, DOCX, or TXT","accept":".pdf,.docx,.txt","multiple":false,"disabled":false},"state":{"dropzone_3":[]}}} playground="ZnJvbSBwcmVmYWJfdWkuY29tcG9uZW50cyBpbXBvcnQgRHJvcFpvbmUKCkRyb3Bab25lKAogICAgbGFiZWw9IlVwbG9hZCBkb2N1bWVudHMiLAogICAgZGVzY3JpcHRpb249IlBERiwgRE9DWCwgb3IgVFhUIiwKICAgIGFjY2VwdD0iLnBkZiwuZG9jeCwudHh0IiwKKQo=">
<CodeGroup>
```python Python icon="python"
from prefab_ui.components import DropZone

DropZone(
    label="Upload documents",
    description="PDF, DOCX, or TXT",
    accept=".pdf,.docx,.txt",
)
```
```json Protocol icon="brackets-curly"
{
  "view": {
    "name": "dropzone_3",
    "type": "DropZone",
    "label": "Upload documents",
    "description": "PDF, DOCX, or TXT",
    "accept": ".pdf,.docx,.txt",
    "multiple": false,
    "disabled": false
  },
  "state": {"dropzone_3": []}
}
```
</CodeGroup>
</ComponentPreview>

## Multiple Files

By default, DropZone accepts one file. Set `multiple=True` to let users select several at once — each new drop accumulates into the state array rather than replacing it.

<ComponentPreview json={{"view":{"name":"dropzone_4","type":"DropZone","label":"Upload files","description":"Select as many as you like","multiple":true,"disabled":false},"state":{"dropzone_4":[]}}} playground="ZnJvbSBwcmVmYWJfdWkuY29tcG9uZW50cyBpbXBvcnQgRHJvcFpvbmUKCkRyb3Bab25lKAogICAgbGFiZWw9IlVwbG9hZCBmaWxlcyIsCiAgICBkZXNjcmlwdGlvbj0iU2VsZWN0IGFzIG1hbnkgYXMgeW91IGxpa2UiLAogICAgbXVsdGlwbGU9VHJ1ZSwKKQo=">
<CodeGroup>
```python Python icon="python"
from prefab_ui.components import DropZone

DropZone(
    label="Upload files",
    description="Select as many as you like",
    multiple=True,
)
```
```json Protocol icon="brackets-curly"
{
  "view": {
    "name": "dropzone_4",
    "type": "DropZone",
    "label": "Upload files",
    "description": "Select as many as you like",
    "multiple": true,
    "disabled": false
  },
  "state": {"dropzone_4": []}
}
```
</CodeGroup>
</ComponentPreview>

## Handling Uploads

Files stay in the browser until you do something with them. The `on_change` parameter fires actions when files are selected, with `$event` containing the file data — so the typical pattern is to pair it with a `CallTool` that sends the base64 payload to your MCP server for processing.

```python DropZone with CallTool
from prefab_ui.components import DropZone
from prefab_ui.actions.mcp import CallTool

DropZone(
    label="Upload CSV",
    accept=".csv",
    on_change=CallTool(
        "process_csv",
        arguments={"file": "{{ $event }}"},
    ),
)
```

Since `on_change` accepts a list, you can chain actions for UI feedback — show a loading state, call the tool, then clear the loading state:

```python DropZone with chained actions
from prefab_ui.components import DropZone
from prefab_ui.actions import SetState
from prefab_ui.actions.mcp import CallTool

DropZone(
    label="Upload data file",
    on_change=[
        SetState("uploading", True),
        CallTool("upload_file", arguments={"file": "{{ $event }}"}),
        SetState("uploading", False),
    ],
)
```

## The `FileUpload` Type

Both DropZone and [OpenFilePicker](/actions/open-file-picker) always produce `$event` as `list[FileUpload]` — an array of objects with `name`, `size` (bytes), `type` (MIME), and `data` (raw base64 — no `data:` URL prefix). Even single-file uploads produce a one-element array.

Prefab ships a `FileUpload` type so you can annotate your MCP tool parameters with it:

```python Typing a tool parameter
from prefab_ui.actions import FileUpload

@server.tool()
def process_csv(files: list[FileUpload]):
    import base64
    for f in files:
        contents = base64.b64decode(f.data)
        # f.name, f.size, f.type also available
        ...
```

## Disabled State

<ComponentPreview json={{"view":{"name":"dropzone_5","type":"DropZone","label":"Uploads disabled","multiple":false,"disabled":true},"state":{"dropzone_5":[]}}} playground="ZnJvbSBwcmVmYWJfdWkuY29tcG9uZW50cyBpbXBvcnQgRHJvcFpvbmUKCkRyb3Bab25lKGxhYmVsPSJVcGxvYWRzIGRpc2FibGVkIiwgZGlzYWJsZWQ9VHJ1ZSkK">
<CodeGroup>
```python Python icon="python"
from prefab_ui.components import DropZone

DropZone(label="Uploads disabled", disabled=True)
```
```json Protocol icon="brackets-curly"
{
  "view": {
    "name": "dropzone_5",
    "type": "DropZone",
    "label": "Uploads disabled",
    "multiple": false,
    "disabled": true
  },
  "state": {"dropzone_5": []}
}
```
</CodeGroup>
</ComponentPreview>

## Reading the Value

DropZone stores selected file data in client state as `list[FileUpload]` — always an array, defaulting to `[]`. Each entry has `name`, `size` (bytes), `type` (MIME), and `data` (base64) properties. With `multiple=True`, files accumulate across drops; with `multiple=False`, each upload replaces the previous one.

Use `.rx` to reference file data in other components. The `.length()` pipe counts entries, and `ForEach` iterates over the array to display each file's details. Drop some files below to see it work:

<ComponentPreview json={{"view":{"cssClass":"gap-3","type":"Column","children":[{"name":"dropzone_6","type":"DropZone","label":"Drop files to try it","multiple":true,"disabled":false},{"type":"Condition","cases":[{"when":"{{ dropzone_6 | length }}","children":[{"cssClass":"gap-2","type":"Column","children":[{"cssClass":"text-sm font-medium","content":"{{ dropzone_6 | length }} file(s) selected","type":"Text"},{"let":{"_loop_2":"{{ $item }}","_loop_2_idx":"{{ $index }}"},"type":"ForEach","key":"dropzone_6","children":[{"cssClass":"gap-2 items-center","type":"Row","children":[{"type":"Badge","label":"{{ $item.type }}","variant":"secondary"},{"content":"{{ $item.name }}","type":"Small"},{"content":"{{ $item.size }} bytes","type":"Muted"}]}]}]}]}],"else":[{"content":"No files selected yet","type":"Muted"}]}]},"state":{"dropzone_6":[]}}} playground=" ">
<CodeGroup>
```python Python icon="python"
from prefab_ui.components import (
    Badge, Column, DropZone, ForEach,
    If, Else, Muted, Row, Small, Text,
)

with Column(gap=3):
    zone = DropZone(
        label="Drop files to try it",
        multiple=True,
    )
    with If(f"{zone.rx.length()}"):
        with Column(gap=2):
            Text(
                f"{zone.rx.length()} file(s) selected",
                css_class="text-sm font-medium",
            )
            with ForEach(zone.rx):
                with Row(gap=2, align="center"):
                    Badge("{{ $item.type }}", variant="secondary")
                    Small("{{ $item.name }}")
                    Muted("{{ $item.size }} bytes")
    with Else():
        Muted("No files selected yet")
```
```json Protocol icon="brackets-curly"
{
  "view": {
    "cssClass": "gap-3",
    "type": "Column",
    "children": [
      {
        "name": "dropzone_6",
        "type": "DropZone",
        "label": "Drop files to try it",
        "multiple": true,
        "disabled": false
      },
      {
        "type": "Condition",
        "cases": [
          {
            "when": "{{ dropzone_6 | length }}",
            "children": [
              {
                "cssClass": "gap-2",
                "type": "Column",
                "children": [
                  {
                    "cssClass": "text-sm font-medium",
                    "content": "{{ dropzone_6 | length }} file(s) selected",
                    "type": "Text"
                  },
                  {
                    "let": {"_loop_2": "{{ $item }}", "_loop_2_idx": "{{ $index }}"},
                    "type": "ForEach",
                    "key": "dropzone_6",
                    "children": [
                      {
                        "cssClass": "gap-2 items-center",
                        "type": "Row",
                        "children": [
                          {"type": "Badge", "label": "{{ $item.type }}", "variant": "secondary"},
                          {"content": "{{ $item.name }}", "type": "Small"},
                          {"content": "{{ $item.size }} bytes", "type": "Muted"}
                        ]
                      }
                    ]
                  }
                ]
              }
            ]
          }
        ],
        "else": [{"content": "No files selected yet", "type": "Muted"}]
      }
    ]
  },
  "state": {"dropzone_6": []}
}
```
</CodeGroup>
</ComponentPreview>

Inside `ForEach`, the template variables `$item.name`, `$item.type`, and `$item.size` resolve to each file's properties.

## API Reference

<Card icon="code" title="DropZone Parameters">

<ParamField body="icon" type="str | None" default="None">
  Lucide icon name in kebab-case (e.g. `"cloud-upload"`, `"image"`). Defaults to an upload icon.
</ParamField>

<ParamField body="label" type="str | None" default="None">
  Primary prompt text shown in the drop zone.
</ParamField>

<ParamField body="description" type="str | None" default="None">
  Secondary helper text below the label (e.g. file type hints).
</ParamField>

<ParamField body="accept" type="str | None" default="None">
  File type filter. Accepts MIME types (`"image/*"`), extensions (`".csv"`), or a comma-separated mix (`".pdf,.docx,.txt"`).
</ParamField>

<ParamField body="multiple" type="bool" default="False">
  Allow selecting multiple files. When `True`, files accumulate across drops. When `False`, each upload overwrites the previous one.
</ParamField>

<ParamField body="max_size" type="int | None" default="None">
  Maximum file size in bytes per file. Files exceeding this are rejected with a toast notification.
</ParamField>

<ParamField body="disabled" type="bool" default="False">
  Whether the drop zone is non-interactive.
</ParamField>

<ParamField body="name" type="str | None" default="None">
  State key for auto-state binding. When set without `on_change`, file data is stored in state under this key.
</ParamField>

<ParamField body="on_change" type="Action | list[Action] | None" default="None">
  Action(s) to execute when files are selected. `$event` is always `list[FileUpload]`.
</ParamField>

<ParamField body="css_class" type="str | None" default="None">
  Additional Tailwind CSS classes appended to the component's built-in styles.
</ParamField>
</Card>

## Protocol Reference

```json DropZone
{
  "type": "DropZone",
  "icon?": "string",
  "label?": "string",
  "description?": "string",
  "accept?": "string",
  "multiple?": false,
  "maxSize?": "number",
  "disabled?": false,
  "name?": "string",
  "onChange?": "Action | Action[]",
  "cssClass?": "string"
}
```

For the complete protocol schema, see [DropZone](/protocol/drop-zone).
