component

Input Upload

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.

Feedback

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>

Size

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>

Slots

Override any slot — icon, title, description, or trigger — to customize the dropzone content. Pass strings via the matching props (title, description, trigger) for simple text overrides, or use named slots / snippets for rich content.

Custom icon

Rich title and description

Custom trigger label

---
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"
      trigger="Pick a PDF"
      accept="application/pdf"
    />
  </div>
</div>

Custom Styling

The passThrough prop provides per-slot escape hatches: style accepts a Panda CSS SystemStyleObject and props forwards arbitrary HTML attributes onto the slot element.

Pill-shaped surface

Solid border + accented icon

Accessibility props

---
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"
      trigger="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>

Upload Button

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>

Validation

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.

Max file size

Max file count

Image dimensions

Custom validator

---
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>
props · 31 shown · 31 total
InputUpload
accept svelte
string

Native `accept` filter forwarded to the file input.

background
"surface.peak""surface.hill""surface.base""surface.valley""surface.ground""transparent"
default "transparent"

Surface background variant.

capture svelte
boolean"user""environment"

Native `capture` hint for camera/microphone sources.

children slot svelte
Snippet

Extra dropzone content rendered after the trigger row.

description slot svelte
Snippetstring

Secondary helper line under the title (e.g. accepted formats).

element binding svelte
HTMLInputElementnull

Bindable reference to the native `<input type="file">` element.

feedback
"primary""neutral""success""warning""danger""inherit"
default "neutral"

Semantic feedback color scheme indicating validation state or context (for example, a red dropzone after rejecting an oversized file).

files svelte
File[]

Bindable mirror of the accepted files (engine → out only; use the input itself to set files).

fullWidth
boolean
default true

Stretch the dropzone to fill its container width.

icon slot svelte
Snippet

Centered dropzone icon. Defaults to a built-in upload glyph.

imageDimensions svelte
ImageDimensionsConstraint

Width/height constraints applied to image files.

maxFiles svelte
number

Maximum number of accepted files.

maxSize svelte
number

Maximum size per file, in bytes. Larger files are rejected.

multiple svelte
boolean

Allow selecting multiple files.

onFilesChange svelte
(files: File[]) => void

Fires with the accepted files whenever the selection changes.

onReject svelte
(rejections: UploadRejection[]) => void

Fires with the rejected files (and their reasons) whenever validation runs.

passThrough
InputUploadPassThrough< RootElementAttributes, InputElementAttributes, IconElementAttributes, TitleElementAttributes, DescriptionElementAttributes, TriggerElementAttributes >

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.

radius
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""full""2xs""inner""inherit"

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`).

radiusBottom
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""full""2xs""inner""inherit"

Border radius for the bottom-left and bottom-right corners.

radiusLeft
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""full""2xs""inner""inherit"

Border radius for the top-left and bottom-left corners.

radiusRight
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""full""2xs""inner""inherit"

Border radius for the top-right and bottom-right corners.

radiusTop
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""full""2xs""inner""inherit"

Border radius for the top-left and top-right corners. Overrides `radius` on those corners only (flush-edge chains).

size
"sm""md""lg""xs"
default "md"

Size variant. Scales the dropzone's min-height, padding, icon, and typography together.

title slot svelte
Snippetstring
default "Choose a file or drag & drop it here"

Dropzone headline — a plain string or a snippet for rich content.

trigger slot svelte
Snippetstring
default "Browse file"

Trigger button label — a plain string renders the built-in button, a snippet takes over the trigger entirely.

validate svelte
UploadValidator

Custom per-file validator — return an error string to reject, `null` to accept. May be async.

Plus all standard <input> HTML attributes.

UploadButton
emphasis
"primary""secondary""ghost""adaptive"
default "secondary"

Visual emphasis forwarded to the inner Pindoba `<Button>`.

feedback
"primary""neutral""success""warning""danger""inherit"

Semantic feedback color forwarded to the inner Pindoba `<Button>`.

fullWidth
boolean

Stretch the upload button to fill its container width.

passThrough
UploadButtonPassThrough< RootElementAttributes, InputElementAttributes, IconElementAttributes >

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.

size
"sm""md""lg""xs"
default "md"

Size variant for the upload button.

Type

  • Components
  • Blocks