Installation
$Terminal
npx shadcn@latest add https://sdk-components.thesqd.com/r/file-upload-beta.jsonUsage
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-previewTSImport
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.jsonStyling
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
@importafter any non-@importrule, 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 theneutral-200utility. Lean apps won't trigger emission, so the border collapses totransparent. Use the literal hex (#e5e5e5) — that's what the spec ships. - Override
.filepond--root, not.filepond--panel-root. FilePond renders four elements with thepanel-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. !importantis required.filepond.min.cssis imported inside the client component, so it loads afterglobals.cssand 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.
Please upload at least one file to continue.
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
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Field label rendered above the uploader. |
caption | ReactNode | — | Helper copy shown directly under the label via FieldLabel. |
error | boolean | string | — | Static 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. |
showNetworkStatus | boolean | false | 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"). Upload rate is sampled over a fixed window and EMA-smoothed; a stall reads as "Reconnecting…" and going offline shows "Offline — will resume". |
progressDecimals | number | 0 | Decimal places for the overall percent in the network readout (`0` → "42%", `1` → "42.5%"). Only applies with `showNetworkStatus`. |
onwarning | (err, file?, status?) => void | — | FilePond 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?) => void | — | FilePond 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
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | — | Form field name applied to the upload request body. |
allowMultiple | boolean | false | Accept more than one file in a single picker. |
instantUpload | boolean | true | Upload as soon as the user adds a file. |
server | FilePondServerConfigProps["server"] | — | Custom upload pipeline. Most apps implement `process` and pass the response body to `load()` so it lands as the file's serverId. |
credits | boolean | true | Show the FilePond branding in the corner. |
FilePond — validation plugins
| Prop | Type | Default | Description |
|---|---|---|---|
maxFiles | number | — | Cap how many files can be selected. |
maxFileSize | string | — | Per-file size limit, e.g. "10MB". |
maxTotalFileSize | string | — | Combined size cap across every selected file. |
acceptedFileTypes | string[] | — | Whitelist of MIME types, e.g. ["image/*", "application/pdf"]. |
allowImagePreview | boolean | — | Render 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 / MaxHeight | number | — | Image dimension bounds (requires filepond-plugin-image-validate-size). |