File upload

Drag-and-drop uploader with validation for type, size, count, and image dimensions.

Installation
$Terminal
npx shadcn@latest add https://sdk-components.thesqd.com/r/file-upload-beta.json
Usage
The CLI install pulls every file the demo needs (component, registry deps, FilePond plugins, the FilePond CSS overrides, and the S3 upload route). After it runs you wire the `globals.css` import and set the env vars below — the component itself works without any further setup.
$Terminal
npm install filepond@^4 react-filepond@^7 \
  filepond-plugin-file-validate-type \
  filepond-plugin-file-validate-size \
  filepond-plugin-image-validate-size \
  filepond-plugin-image-preview
TSImport
import { FileUpload } from "@/components/blocks/file-upload-beta";
TSExample
"use client";

import { FileUpload } from "@/components/blocks/file-upload-beta";

// Single component, every "variant" driven by props:
//   label / caption / error / restrictionText / showUploadedUrls
//   acceptedFileTypes / maxFileSize / maxFiles / maxTotalFileSize / allowMultiple
//   imageValidateSizeMin/MaxWidth/Height
export function Example() {
  return (
    <FileUpload
      label="Files"
      allowMultiple
      maxFiles={2}
      maxFileSize="10MB"
      acceptedFileTypes={["image/*"]}
      restrictionText="Images only"
      showUploadedUrls
    />
  );
}
Post-install wiring
Things the registry can't do for you (`globals.css` import line + env vars). Skip these and you'll see an unstyled FilePond panel and / or a 500 from `/api/s3-presign` on the first upload.
What `shadcn add file-upload-beta.json` drops in for you:
- src/components/blocks/file-upload-beta.tsx          (the component)
- src/components/ui/{alert,label,field-hint}.tsx      (registry deps)
- src/app/filepond.css                                (light + dark FilePond overrides)
- src/app/api/s3-presign/route.ts                     (proxy to api.thesqd.com /v1/s3/get-presigned-upload-url)
- .env.example                                        (the env vars the route reads)
- npm: filepond, react-filepond,
       filepond-plugin-{file-validate-type,file-validate-size,image-validate-size,image-preview},
       lucide-react

Auto-injected into globals.css by the install:
- `@import "./filepond.css"` (so the FilePond style overrides kick in
  immediately — no manual CSS editing required).

What you still have to wire up by hand:
1. Copy `.env.example` → `.env.local` and set `SQUAD_API_KEY`. Without
   it the presign route 500s on every upload (browser PUTs land at the
   Squad gateway, not directly on Wasabi).

2. If this is a brand-new project and you haven't installed the theme
   yet, run that first — it provides the tokens, `cn` helper, and the
   shared dependencies every Squad component needs:
     npx shadcn@latest add https://sdk-components.thesqd.com/r/theme.json
Styling
FilePond ships its own stylesheet. Drop these overrides into a sibling CSS file and import it from `globals.css` so theme changes live in one place instead of being scattered across components.
CSSsrc/app/filepond.css
/* FilePond — light + dark overrides (https://pqina.nl/filepond/docs/api/style/) */

/* Strip filepond's per-slice panel bg so the root can carry the unified surface */
.filepond--panel > .filepond--panel-root {
  background-color: transparent !important;
}

/* Light mode — match Tabs track (bg-[#fbfafc] / border-neutral-200).
   Use the literal hex, not var(--color-neutral-200): Tailwind v4 only emits
   that variable when something in the project actually uses the neutral-200
   utility, so referencing it by name silently breaks in lean apps. */
.filepond--root {
  background-color: #fbfafc !important;
  border: 1px solid #e5e5e5 !important;
  border-radius: 0.5rem !important;
  overflow: hidden !important;
}
.filepond--drop-label,
.filepond--drop-label label {
  font-size: 0.875rem !important;
  font-weight: 500 !important;
}
.filepond--drop-label .filepond--label-sub,
.filepond--drop-label label .filepond--label-sub {
  display: inline-block;
  margin-top: -2px;
  font-size: 0.75rem !important;
  font-weight: 400 !important;
  line-height: 1 !important;
  color: oklch(0.55 0 0) !important;
}
.dark .filepond--label-sub {
  color: oklch(0.7 0 0);
}

/* Dark mode — match dark Tabs track (white/5 surface, white/12 border) */
.dark .filepond--root {
  background-color: oklch(1 0 0 / 3.6%) !important;
  border: 1px solid oklch(1 0 0 / 12%) !important;
}
.dark .filepond--drop-label,
.dark .filepond--drop-label label {
  color: #e5e5e5 !important;
}
.dark .filepond--label-action {
  text-decoration-color: #71717a !important;
}
.dark .filepond--item-panel {
  background-color: #242428 !important;
}
.dark .filepond--item[data-filepond-item-state*="processing-complete"] .filepond--item-panel {
  background-color: #166534 !important;
}
.dark .filepond--item[data-filepond-item-state*="error"] .filepond--item-panel,
.dark .filepond--item[data-filepond-item-state*="invalid"] .filepond--item-panel {
  background-color: #7f1d1d !important;
}
.dark .filepond--file {
  color: #e5e5e5 !important;
}
.dark .filepond--credits {
  color: #71717a !important;
}
CSSsrc/app/globals.css
/* All @import rules MUST sit above any @plugin / @theme / non-import rule.
   Standard CSS forbids @import after other declarations, and Tailwind v4 +
   Turbopack will silently drop the file when the order is wrong — leaving
   FilePond rendering with its default light-gray panel. */
@import "tailwindcss";
@import "tw-animate-css";        /* if you use it */
@import "./filepond.css";        /* must come BEFORE any @plugin directive */
@plugin "@tailwindcss/typography";
Pitfalls
Things that have already burned us. Read them before debugging styling issues.
  • @import order matters. Standard CSS rejects @import after any non-@import rule, and Tailwind v4 + Turbopack will silently drop the file when the order is wrong — leaving FilePond rendering with its default light-gray panel and no border. Put @import "./filepond.css"; above every @plugin, @theme, or rule block.
  • Don't reference Tailwind theme vars by name from filepond.css. var(--color-neutral-200) only exists in the generated CSS if a class in the project actually uses the neutral-200utility. Lean apps won't trigger emission, so the border collapses to transparent. Use the literal hex (#e5e5e5) — that's what the spec ships.
  • Override .filepond--root, not .filepond--panel-root. FilePond renders four elements with the panel-rootclass (outer + top/center/bottom slices). Styling all four gives you stacked borders and shows the slices' transforms. The spec's pattern — transparent on .filepond--panel > .filepond--panel-root, surface + border + radius on .filepond--root — gives a single clean container.
  • !important is required. filepond.min.css is imported inside the client component, so it loads after globals.css and wins the cascade unless overrides are marked important.
Composition
Anatomy of the FilePond uploader.
FilePond                          (the upload widget — registers plugins via registerPlugin)
├── server.process               (your own XHR/fetch — call load(serverId) on success)
├── acceptedFileTypes            (filepond-plugin-file-validate-type)
├── maxFileSize / maxTotalFileSize (filepond-plugin-file-validate-size)
├── maxFiles                     (built into core)
└── imageValidateSize{Min,Max}{Width,Height}  (filepond-plugin-image-validate-size)

Pair with a server route handler (e.g. src/app/api/s3-presign/route.ts)
that proxies to the Squad presign API. The browser then PUTs each file
body straight to Wasabi using the returned upload_url + required_headers,
and FilePond stores public_url as each file's serverId.
Default uploaderLive demo
Uploads to Wasabi S3 via a custom `server.process` handler and prints the public URL once each file lands. Capped at 2 files, 10MB each.
Limit file types
Pass an array of MIME types to `acceptedFileTypes`. Requires the `filepond-plugin-file-validate-type` plugin.
Limit per-file size
Set `maxFileSize` (and optionally `minFileSize`) as natural strings like `5MB`. Requires the `filepond-plugin-file-validate-size` plugin.
Limit file count
Use `maxFiles` to cap how many files can be added. Built into core — no plugin required.
Limit total upload size
`maxTotalFileSize` caps the combined size of every selected file. Requires the `filepond-plugin-file-validate-size` plugin.
Image dimension limits
The `filepond-plugin-image-validate-size` plugin exposes `imageValidateSizeMinWidth` / `MaxWidth` / `MinHeight` / `MaxHeight`. There is no built-in aspect-ratio prop — pin both axes to the same ratio (e.g. 1280×720 → 1920×1080 for 16:9).
Images only
Restrict to `image/*` via `acceptedFileTypes`, and pass `restrictionText="Only images allowed"` to override the auto-derived dropzone line so it doesn't enumerate every image subtype from the wildcard.
Single file
Set `allowMultiple={false}` and `maxFiles={1}`.
With caption
Pass a `caption` string to render helper copy directly under the label, like every other SDK field.

Use the `caption` prop for helper copy under the label — restriction details live inside the dropzone itself.

Error state
Pass `error={true}` for the red panel only, or a string (e.g. `error="Please upload a file."`) to also render the message inline — same `text-xs mt-1.5 text-red-500` treatment as RichTextEditor and NumberInput.
Network status + time left
Pass `showNetworkStatus` to render a live network-quality + estimated-time-left readout under the uploader while files are in flight (e.g. "Network good · 42.5% · ~1m 20s left"). The rate is sampled over a fixed window and EMA-smoothed; a stall watchdog reads as "Reconnecting…" and going offline shows "Offline — will resume". Pair with `progressDecimals` to control the percent precision (`0` → "42%", `1` → "42.5%").

Upload a file to see the live network status and estimated time left below the dropzone.

Quick add (pre-existing assets)
Pass `quickAddFiles` — an array of `{ url, filename, byte_size?, width?, height? }` — to render a horizontally-scrollable strip of one-click assets below the dropzone (e.g. an account's brand logos). Hovering a thumbnail shows its filename, dimensions, and size; clicking toggles it selected (checkmark) and folds its url into the upload output without routing it through the dropzone.

Drop your own files, or quick-add an existing brand asset below — selected assets are included in the upload output.

API Reference
Most-used FilePond props. See the upstream docs for the full surface.

Pond wrapper — label / hint / error

PropTypeDefaultDescription
labelstringField label rendered above the uploader.
captionReactNodeHelper copy shown directly under the label via FieldLabel.
errorboolean | stringStatic error state. `true` applies the red panel only; a string also renders the inline message (`text-xs mt-1.5 text-red-500`) below the uploader. Overridden by runtime FilePond messages.
showNetworkStatusbooleanfalseRender a live network-quality + estimated-time-left readout under the uploader while files are in flight (e.g. "Network good · 42.5% · ~1m 20s left"). Upload rate is sampled over a fixed window and EMA-smoothed; a stall reads as "Reconnecting…" and going offline shows "Offline — will resume".
progressDecimalsnumber0Decimal places for the overall percent in the network readout (`0` → "42%", `1` → "42.5%"). Only applies with `showNetworkStatus`.
onwarning(err, file?, status?) => voidFilePond warning callback (e.g. file count exceeded). Displays `err.body ?? err.main` as an amber-600 message below the uploader. Clears on a successful `onaddfile`.
onerror(err, file?, status?) => voidFilePond error callback (e.g. server upload failure). Displays `err.body ?? err.main` as a red-500 message below the uploader. Clears on a successful `onaddfile`.

FilePond — core

PropTypeDefaultDescription
namestringForm field name applied to the upload request body.
allowMultiplebooleanfalseAccept more than one file in a single picker.
instantUploadbooleantrueUpload as soon as the user adds a file.
serverFilePondServerConfigProps["server"]Custom upload pipeline. Most apps implement `process` and pass the response body to `load()` so it lands as the file's serverId.
creditsbooleantrueShow the FilePond branding in the corner.

FilePond — validation plugins

PropTypeDefaultDescription
maxFilesnumberCap how many files can be selected.
maxFileSizestringPer-file size limit, e.g. "10MB".
maxTotalFileSizestringCombined size cap across every selected file.
acceptedFileTypesstring[]Whitelist of MIME types, e.g. ["image/*", "application/pdf"].
allowImagePreviewbooleanRender image thumbnails for image files (filepond-plugin-image-preview) and lay items out in a 2-column grid. Defaults to true; set false to stack items full-width.
imageValidateSizeMinWidth / MinHeight / MaxWidth / MaxHeightnumberImage dimension bounds (requires filepond-plugin-image-validate-size).