component
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>"transparent"Surface background variant.
"neutral"Semantic feedback color scheme indicating validation state or context (for example, a red dropzone after rejecting an oversized file).
trueStretch the dropzone to fill its container width.
Per-slot escape hatch bag — each slot accepts a Panda CSS `style` object and a `props` map of HTML attributes spread onto that slot's element.
Corner radius override. Without it the dropzone keeps its default `lg` corner. `inner` renders a concentric corner computed from the nearest parent panel (parent radius − parent padding, floored at `xs`).
"md"Size variant. Scales the dropzone's min-height, padding, icon, and typography together.
"secondary"Visual emphasis forwarded to the inner Pindoba `<Button>`.
Semantic feedback color forwarded to the inner Pindoba `<Button>`.
Stretch the upload button to fill its container width.
Per-slot escape hatch bag — each slot accepts a Panda CSS `style` object and a `props` map of HTML attributes spread onto that slot's element.
"md"Size variant for the upload button.