You picked SvelteKit partly to avoid React, and now the drop-in uploader you want is a React component. Mounting it means adding react and react-dom, creating a React root inside a Svelte component, and remembering to unmount it. That path works and it's documented. It's also not the only path. The chunked, resumable upload engine underneath every one of these widgets is plain JavaScript, with no view layer at all.
TL;DR
Two routes, and the server half is identical either way. A +server.ts endpoint returns a signed URL, with credentials read through $env/static/private. SvelteKit then refuses at build time to bundle them for the browser. Route one mounts the React uploader inside onMount. That costs react and react-dom plus an onDestroy unmount, and every sub-component has to go through createElement, because React context can't cross into Svelte markup. Route two drives the plain @fastpix/resumable-uploads engine and keeps the UI in runes. Playback never has this problem: <fastpix-player> is a web component.
SvelteKit video upload without React: the two routes
This is the distinction that makes the second route possible. Chunking a file, retrying a failed piece, resuming after a network change and reporting progress are all view-agnostic operations.
The React component is a UI wrapped around that engine. Nothing about the chunking requires React, and the engine ships as its own package precisely so it can be driven from any framework.
| What you want | React wrapper | Engine plus your own UI |
|---|---|---|
| Working uploader quickly | Yes, one component | No, you build the UI |
| Extra dependencies | react, react-dom | None |
| Progress, pause, resume | Built in | Your own, from events |
| UI matches your design system | Through props and CSS variables | Exactly, because you wrote it |
| Server-side rendering | Must be kept out of SSR | Nothing to keep out |
What you need before you start
- A SvelteKit application with server-side rendering enabled.
- A FastPix account with an Access Token ID and Secret Key from activate your account.
- A decision about which of the two routes you're taking, because they don't share code.
Both routes need the same +server.ts endpoint
Both routes need the same signed upload URL, and SvelteKit gives it the cleanest home of any framework here.
Credentials go in a .server.ts module read through $env/static/private, and SvelteKit refuses to import that module into client code. Not a convention, a build error:
// src/lib/fastpix.server.ts
import { Fastpix } from "@fastpix/fastpix-node";
import { FASTPIX_USERNAME, FASTPIX_PASSWORD } from "$env/static/private";
export const fastpix = new Fastpix({
security: { username: FASTPIX_USERNAME, password: FASTPIX_PASSWORD },
});You can test that boundary against a real upload before committing to it. The free plan covers 10 videos and 100K streaming minutes a month with no credit card. Next.js and plain Vite rely on you never prefixing a Secret Key with the wrong string. SvelteKit makes the mistake impossible to ship, as do React Router's .server modules and Astro's astro:env/server. The build stops with Cannot import $env/static/private into code that runs in the browser, as this could leak sensitive information. which is it telling you that it caught one.
The endpoint itself is a +server.ts file:
// src/routes/api/upload-url/+server.ts
import { json } from "@sveltejs/kit";
import { fastpix } from "$lib/fastpix.server";
export async function POST() {
const upload = await fastpix.inputVideo.upload(
{ corsOrigin: "*", pushMediaSettings: { accessPolicy: "public" } },
{ headers: { "X-Client-Type": "web-browser" } },
);
if (!("data" in upload)) throw new Error("Could not create an upload.");
return json({ url: upload.data!.url, mediaId: upload.data!.uploadId });
}Route one: mount the React uploader in SvelteKit
If you want a working uploader today, this is it. The component mounts into a <div> your Svelte component owns, inside onMount so it never runs during server rendering:
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import { createElement } from "react";
import { createRoot, type Root } from "react-dom/client";
import { FastPixUploader } from "@fastpix/fp-react-uploader";
import "@fastpix/fp-react-uploader/styles.css";
let container: HTMLDivElement;
let root: Root | undefined;
async function createUpload() {
const response = await fetch("/api/upload-url", { method: "POST" });
const { url } = await response.json();
return url as string;
}
onMount(() => {
root = createRoot(container);
root.render(createElement(FastPixUploader, { endpoint: createUpload, accept: "video/*" }));
});
onDestroy(() => root?.unmount());
</script>
<div bind:this={container}></div>createElement avoids needing JSX tooling in a Svelte project. onMount keeps React DOM out of server rendering, without which the render fails with document is not defined. And onDestroy unmounts the root, without which navigating away leaks it.
The sharper constraint arrives when you want a custom layout. The uploader's sub-components read state through React context, and context can't cross into Svelte markup. Every component that needs upload state is created with createElement, in the same root.render call:
root.render(
createElement(
FastPixUploader,
{ endpoint: createUpload, accept: "video/*", autoStart: false },
createElement(FastPixDropZone, { overlay: true }, "Drag a video here"),
createElement(FastPixStatus),
createElement(FastPixTrack, { showLabel: true }),
createElement(FastPixStartButton),
),
);Write those as Svelte markup instead and they render outside the provider, where they fail to find their state. At that point you're composing a React tree by hand inside a Svelte file. That's the moment most people start looking at route two.
Route two: drive the resumable upload engine from Svelte
The @fastpix/resumable-uploads engine is plain JavaScript. You give it a file and a signed URL, subscribe to its events, and render whatever you like:
<script lang="ts">
import { Uploader } from "@fastpix/resumable-uploads";
let percentage = $state(0);
let status = $state<"idle" | "uploading" | "done">("idle");
async function createUpload() {
const response = await fetch("/api/upload-url", { method: "POST" });
const { url } = await response.json();
return url as string;
}
async function handleFile(event: Event) {
const file = (event.target as HTMLInputElement).files?.[0];
if (!file) return;
status = "uploading";
const url = await createUpload();
const uploader = Uploader.init({
endpoint: url,
file,
chunkSize: 5120,
});
uploader.on("progress", (event) => {
percentage = Math.round(event.detail.progress);
});
uploader.on("success", () => {
status = "done";
});
uploader.on("error", (event) => {
console.error(event.detail);
});
}
</script>
<input type="file" accept="video/*" onchange={handleFile} />
{#if status !== "idle"}
<progress value={percentage} max="100"></progress>
{/if}$state drives the progress bar directly from the event handler, with no context provider and no bridge between two reactivity systems.
The drop zone, the pause and resume buttons, the status text and the styling all become yours to write. That's an afternoon rather than a sprint, and it buys a component that belongs in your codebase. The engine's full API is documented in resumable uploads for web.
SvelteKit video playback needs no wrapper
<fastpix-player> is a web component, so it goes straight into Svelte markup with no wrapper, no React and no bridge.
The one rule is that the import belongs in onMount rather than at module scope. Player 1.0.21 and later guard their customElements registration, so this is about weight rather than a crash: a module-scope import puts the player bundle in the page for everyone. Fetch the playback ID on the server:
<script lang="ts">
import { onMount } from "svelte";
let { data } = $props();
onMount(() => { import("@fastpix/fp-player"); });
</script>
<fastpix-player playback-id={data.playbackId} stream-type="on-demand"></fastpix-player>The playback ID comes from a +page.server.ts load function, so credentials stay on the server and only the ID reaches the browser. That half of the integration is genuinely framework-native, and the two build errors it can still produce are covered in playing HLS in SvelteKit.
Which SvelteKit upload route to take
Take the React wrapper if you need the uploader working this week and the default layout is close enough. The cost is two dependencies and three lines of lifecycle code, and it's a reasonable trade.
Take the engine if you were going to restyle the uploader anyway. Or if adding React to a Svelte codebase is a conversation you don't want to have twice. The moment you find yourself composing a React tree with createElement to get a custom layout, route two is already cheaper.
Build the upload flow your SvelteKit app actually wants
Start with the +server.ts endpoint, since both routes need it and SvelteKit makes the credential handling safe by construction. Then pick: mount the React component for speed, or drive the resumable engine and keep the UI in Svelte. The free plan covers 10 videos and 100K streaming minutes a month, no credit card. Try both routes against real uploads before you commit.
Frequently Asked Questions (FAQs)
Can I upload video in SvelteKit without React?
Yes. The prebuilt uploader is a React component, but the chunked, resumable upload engine underneath is plain JavaScript with no view layer. Drive that engine from a Svelte component, subscribe to its progress events, and build the UI with runes. You give up the prebuilt drop zone and buttons and gain a component with no React dependency.
How do I use a React component in SvelteKit?
Create a React root inside onMount, against a <div> your Svelte component owns. Render the component with createElement, so the project needs no JSX tooling. Unmount it in onDestroy. onMount matters because React DOM must not run during server rendering, and onDestroy matters because an unmounted root leaks on navigation.
Why do the uploader sub-components show no state in Svelte?
Because they're created outside the React tree, usually by writing them as Svelte markup. They read upload state through React context, and context cannot cross into Svelte. Every component that needs that state has to be built with createElement inside the same root.render call.
Why does my SvelteKit build fail on an import from $env/static/private?
The full message is Cannot import $env/static/private into code that runs in the browser, as this could leak sensitive information. It means a component that runs in the browser is importing your server module. Only +server.ts, +page.server.ts and other server files may import it. This is SvelteKit catching a credential leak at build time. React Router and Astro offer the same guarantee through .server modules and astro:env/server; Next.js and plain Vite do not.
Does the FastPix player need React in Svelte?
No. <fastpix-player> is a web component and drops straight into Svelte markup. Import it inside onMount rather than at module scope, which keeps the player bundle out of the page for visitors who never reach a video.






