A file upload input rendered as a labelled dropzone surface with a centered icon, title, helper description, and a styled trigger affordance. The whole surface is clickable to open the native file picker and accepts drag-and-drop. Drag state and selected files are owned by the shared core package, so Astro and Svelte adapters consume the same headless logic.
Apply semantic feedback colors to indicate validation state or context (for example, a red dropzone after rejecting an oversized file).
---
import InputUpload from "../InputUpload.astro";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "md", direction: "column" })}>
<InputUpload feedback="neutral" description="JPEG, PNG, PDF up to 50 MB" />
<InputUpload feedback="primary" description="JPEG, PNG, PDF up to 50 MB" />
<InputUpload feedback="success" description="JPEG, PNG, PDF up to 50 MB" />
<InputUpload feedback="danger" description="JPEG, PNG, PDF up to 50 MB" />
<InputUpload feedback="warning" description="JPEG, PNG, PDF up to 50 MB" />
</div>Four sizes scale the dropzone’s min-height, padding, icon, and typography together.
---
import InputUpload from "../InputUpload.astro";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "md", direction: "column" })}>
<InputUpload size="xs" description="Extra small" />
<InputUpload size="sm" description="Small" />
<InputUpload size="md" description="Medium" />
<InputUpload size="lg" description="Large" />
</div>Override any slot — icon, title, description, or trigger — to customize the dropzone content. Pass strings via the matching props (title, description, triggerLabel) for simple text overrides, or use named slots / snippets for rich content.
---
import InputUpload from "../InputUpload.astro";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "xl", direction: "column" })}>
<div>
<h3>Custom icon</h3>
<InputUpload accept="image/*" description="JPEG or PNG, up to 10 MB">
<svg
slot="icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.75"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<rect width="18" height="18" x="3" y="3" rx="2" ry="2"></rect>
<circle cx="9" cy="9" r="2"></circle>
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"></path>
</svg>
</InputUpload>
</div>
<div>
<h3>Rich title and description</h3>
<InputUpload accept=".pdf,.doc,.docx" multiple>
<Fragment slot="title">Drop <em>contracts</em> here</Fragment>
<Fragment slot="description">
Accepts <code>.pdf</code>, <code>.doc</code>, <code>.docx</code>
</Fragment>
</InputUpload>
</div>
<div>
<h3>Custom trigger label</h3>
<InputUpload
title="Upload your résumé"
description="One PDF, 5 MB max"
triggerLabel="Pick a PDF"
accept="application/pdf"
/>
</div>
</div>The passThrough prop provides per-slot escape hatches: style accepts a Panda CSS SystemStyleObject and props forwards arbitrary HTML attributes onto the slot element.
---
import InputUpload from "../InputUpload.astro";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "xl", direction: "column" })}>
<div>
<h3>Pill-shaped surface</h3>
<InputUpload
title="Drop here"
description="JPEG or PNG up to 5 MB"
passThrough={{
root: { style: { borderRadius: "full" } },
}}
/>
</div>
<div>
<h3>Solid border + accented icon</h3>
<InputUpload
title="Upload your resume"
description="PDF only, 10 MB max"
triggerLabel="Pick a file"
passThrough={{
root: { style: { borderStyle: "solid" } },
icon: { style: { color: "primary.text" } },
}}
/>
</div>
<div>
<h3>Accessibility props</h3>
<InputUpload
title="Drop your avatar"
passThrough={{
input: {
props: {
"aria-label": "Upload avatar",
"data-testid": "avatar-upload",
},
},
}}
/>
</div>
</div>For compact entry points where a full dropzone is overkill, use the sibling <UploadButton> component. It renders a real Pindoba <Button> that opens the file picker on click and still accepts drag-and-drop on its surface. Forward emphasis and feedback to pick the button look — they’re passed straight through to the underlying button.
---
import UploadButtonComponent from "../UploadButton.astro";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "md", direction: "column", align: "flex-start" })}>
<UploadButtonComponent emphasis="primary" label="Upload" />
<UploadButtonComponent emphasis="secondary" label="Upload" />
<UploadButtonComponent emphasis="ghost" label="Upload" />
<UploadButtonComponent emphasis="primary" feedback="danger" label="Upload" />
<UploadButtonComponent
emphasis="secondary"
label="Upload (max 1 KB)"
maxSize={1024}
/>
</div>Built-in validation rejects files that exceed maxSize, surpass maxFiles, fail imageDimensions checks, or are flagged by a custom validate function. Rejected files are reported via onReject (Svelte) or a pindoba:reject CustomEvent (Astro). Combine with feedback="danger" to indicate the error state.
In Astro, custom function-based validators are registered on window.pindobaValidators and referenced by name through the validatorName prop, since functions can’t be passed through frontmatter to inline scripts.
---
import InputUpload from "../InputUpload.astro";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "xl", direction: "column" })}>
<div data-demo="size">
<h3>Max file size</h3>
<InputUpload
accept="image/*,application/pdf"
description="Up to 2 MB"
maxSize={2 * 1024 * 1024}
/>
<p
data-demo-error="size"
style="color: var(--colors-danger-text); margin-top: 8px;"
>
</p>
</div>
<div data-demo="count">
<h3>Max file count</h3>
<InputUpload multiple description="Up to 2 files" maxFiles={2} />
<p
data-demo-error="count"
style="color: var(--colors-danger-text); margin-top: 8px;"
>
</p>
</div>
<div data-demo="image">
<h3>Image dimensions</h3>
<InputUpload
accept="image/*"
description="Square images, 200×200 to 2000×2000"
imageDimensions={{
minWidth: 200,
maxWidth: 2000,
minHeight: 200,
maxHeight: 2000,
aspectRatio: 1,
aspectRatioTolerance: 0.05,
}}
/>
<p
data-demo-error="image"
style="color: var(--colors-danger-text); margin-top: 8px;"
>
</p>
</div>
<div data-demo="custom">
<h3>Custom validator</h3>
<InputUpload
description="Filename cannot contain spaces"
validatorName="noSpaces"
/>
<p
data-demo-error="custom"
style="color: var(--colors-danger-text); margin-top: 8px;"
>
</p>
</div>
</div>
<script>
window.pindobaValidators = window.pindobaValidators ?? {};
window.pindobaValidators.noSpaces = (file: File) =>
/\s/.test(file.name) ? "Filename has spaces" : null;
type RejectDetail = {
rejections: { file: File; message: string }[];
};
document.querySelectorAll<HTMLElement>("[data-demo]").forEach((wrapper) => {
const id = wrapper.dataset.demo;
const root = wrapper.querySelector<HTMLElement>("[data-input-upload-root]");
const errorEl = wrapper.querySelector<HTMLElement>(
`[data-demo-error="${id}"]`,
);
if (!root || !errorEl) return;
root.addEventListener("pindoba:reject", (e) => {
const detail = (e as CustomEvent<RejectDetail>).detail;
errorEl.textContent = detail.rejections
.map((r) => `${r.file.name}: ${r.message}`)
.join(", ");
root.setAttribute("data-feedback-state", "danger");
});
root.addEventListener("pindoba:files", () => {
errorEl.textContent = "";
});
});
</script>| prop | type | default | req | description |
|---|---|---|---|---|
| feedback | "neutral""primary""success""danger""warning" | "neutral" | Semantic feedback color scheme. neutral: Default. primary: Primary color. success: Green. danger: Red. warning: Orange. | |
| size | "xs""sm""md""lg" | "md" | Size variant. Scales min-height, padding, icon, and typography. xs: Extra small. sm: Small. md: Default. lg: Large. | |
| background | "surface.soft""surface.step.1""surface.step.2""surface.step.3""surface.deep""transparent" | "transparent" | Surface background variant. | |
| fullWidth | boolean | true | Stretch the dropzone to fill its container width. | |
| title | stringSnippet | "Choose a file or drag & drop it here" | Primary line shown inside the dropzone. Pass a string, or use the matching slot (Astro) / snippet (Svelte) for rich content. | |
| description | stringSnippet | undefined | Helper text shown below the title (e.g., accepted formats and size limits). Empty by default. | |
| triggerLabel | string | "Browse file" | Label for the styled trigger pill (Astro). Svelte exposes a `trigger` prop that accepts string or snippet. | |
| accept | string | undefined | Native file input accept pattern (e.g., "image/*" or ".pdf,.doc"). | |
| multiple | boolean | false | Allow selecting more than one file. | |
| capture | boolean"user""environment" | undefined | Hint for capture devices on mobile (camera/microphone). | |
| maxSize | number | undefined | Reject files larger than this many bytes. | |
| maxFiles | number | undefined | Reject files past this count when multiple is true. | |
| imageDimensions | { minWidth?, maxWidth?, minHeight?, maxHeight?, aspectRatio?, aspectRatioTolerance? } | undefined | Image-only constraints. Skipped for non-image files. Supports min/maxWidth, min/maxHeight, aspectRatio (with optional aspectRatioTolerance). | |
| validate | (file: File) => string | null | Promise<stringnull> | undefined | Svelte: function returning a string error or null. Runs after built-in checks pass. | |
| validatorName | string | undefined | Astro only. Name of a validator registered on window.pindobaValidators. Functions can't be serialized through frontmatter, so Astro looks the validator up by name. | |
| onReject | (rejections: UploadRejection[]) => void | undefined | Svelte: callback receiving an array of { file, reason, message } rejections. Astro: subscribe to the pindoba:reject CustomEvent on the root element. | |
| passThrough | { root?: { style?: SystemStyleObject; props?: Record<string, unknown> }; input?: ...; icon?: ...; title?: ...; description?: ...; trigger?: ... } | undefined | Custom styling and props for each slot: root (label), input (hidden file input), icon, title, description, trigger. | |
| ...rest | HTMLAttributes<HTMLInputElement> | - | Standard HTML input attributes forwarded to the inner file input element. |