File Upload Form
Accept file attachments from users. Files are stored and linked to submissions in your Formtorch dashboard.
Don't have one yet? Sign up now and get one in less than 5 minutes.
Add your Formtorch endpoint
Paste your form ID and the code below updates instantly.
<section class="ft-upload-demo" aria-label="File upload form demo">
<div class="ft-window">
<div class="ft-window-bar" aria-hidden="true">
<span></span>
<span></span>
<span></span>
</div>
<!-- enctype="multipart/form-data" is required for file uploads -->
<form
id="upload-form"
class="ft-form"
action="https://formtorch.com/f/abc123"
method="POST"
enctype="multipart/form-data"
accept-charset="UTF-8"
>
<div class="ft-icon" aria-hidden="true">
<svg viewBox="0 0 24 24">
<path
d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z"
/>
<path d="M13 2v6h6" />
<g transform="translate(11 11)">
<rect x="0" y="0" width="11" height="11" rx="3" fill="#0b1220" />
<path d="M5.5 8V3" />
<path d="M3 5.5 5.5 3 8 5.5" />
</g>
</svg>
</div>
<div class="ft-form-heading">
<h2>File Upload Form</h2>
<p>Upload documents, images, or PDFs securely.</p>
</div>
<input type="hidden" name="_formName" value="File Upload" />
<label class="ft-dropzone" for="ft-files">
<input
id="ft-files"
class="ft-dropzone-input"
type="file"
name="files"
multiple
accept=".pdf,.doc,.docx,.png,.jpg,.jpeg"
aria-label="Upload files"
/>
<svg class="ft-dropzone-icon" aria-hidden="true" viewBox="0 0 24 24">
<path
d="M17.5 18a4.5 4.5 0 0 0 0-9 6 6 0 0 0-11.7 1.5A4 4 0 0 0 6.5 18Z"
/>
<path d="M12 12v7" />
<path d="m9 15 3-3 3 3" />
</svg>
<p class="ft-dropzone-title">Drag and drop files here</p>
<p class="ft-dropzone-hint">
or <span class="ft-browse">browse</span> from your device
</p>
</label>
<div class="ft-field">
<label for="ft-note">Add a note <em>(optional)</em></label>
<textarea
id="ft-note"
name="note"
rows="3"
placeholder="Add any additional details about your files..."
></textarea>
</div>
<button class="ft-submit" type="submit">
<svg aria-hidden="true" viewBox="0 0 24 24">
<path d="m22 2-7 20-4-9-9-4Z" />
<path d="M22 2 11 13" />
</svg>
<span>Submit Files</span>
</button>
<p id="ft-status" class="ft-status" aria-live="polite">
<svg aria-hidden="true" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" />
<path d="m9 12 2 2 4-4" />
</svg>
<span>Files are securely stored and metadata is supported.</span>
</p>
</form>
</div>
</section>
<style>
html,
body {
margin: 0;
padding: 0;
background: #05070d;
}
.ft-upload-demo {
position: relative;
box-sizing: border-box;
width: 100%;
min-height: 100vh;
padding: clamp(24px, 5vw, 64px);
display: grid;
place-items: center;
font-family:
Inter,
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
color: #f8fafc;
background:
radial-gradient(circle at 22% 18%, rgb(37 99 235 / 0.2), transparent 32%),
radial-gradient(
circle at 78% 75%,
rgb(37 99 235 / 0.18),
transparent 30%
),
#05070d;
}
.ft-upload-demo,
.ft-upload-demo * {
box-sizing: border-box;
}
.ft-upload-demo::before,
.ft-upload-demo::after {
content: "";
position: absolute;
z-index: 0;
width: clamp(90px, 16vw, 130px);
height: clamp(130px, 20vw, 190px);
pointer-events: none;
opacity: 0.45;
background-image: radial-gradient(
rgb(37 99 235 / 0.9) 1.5px,
transparent 1.5px
);
background-size: 20px 20px;
}
.ft-upload-demo::before {
top: 9%;
left: 5%;
}
.ft-upload-demo::after {
bottom: 9%;
right: 5%;
}
.ft-window {
position: relative;
z-index: 1;
width: min(100%, 560px);
overflow: hidden;
border: 1px solid rgb(148 163 184 / 0.22);
border-radius: 24px;
background:
linear-gradient(180deg, rgb(255 255 255 / 0.06), transparent 130px),
linear-gradient(145deg, rgb(15 23 42 / 0.96), rgb(2 6 23 / 0.96));
box-shadow:
0 26px 80px rgb(0 0 0 / 0.45),
0 0 0 1px rgb(255 255 255 / 0.04),
0 0 44px rgb(37 99 235 / 0.2);
}
.ft-window-bar {
position: relative;
z-index: 1;
height: 64px;
display: flex;
align-items: center;
gap: 12px;
padding: 0 32px;
border-bottom: 1px solid rgb(148 163 184 / 0.12);
background: rgb(255 255 255 / 0.025);
}
.ft-window-bar span {
width: 14px;
height: 14px;
border-radius: 999px;
background: #2563eb;
box-shadow: 0 0 20px rgb(37 99 235 / 0.55);
}
.ft-window-bar span:nth-child(2) {
background: #1d4ed8;
}
.ft-window-bar span:nth-child(3) {
background: #334155;
box-shadow: none;
}
.ft-form {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
gap: 20px;
padding: clamp(32px, 6vw, 44px);
text-align: left;
}
.ft-icon {
align-self: center;
display: flex;
align-items: center;
justify-content: center;
width: 60px;
height: 60px;
border-radius: 16px;
background: linear-gradient(
160deg,
rgb(37 99 235 / 0.22),
rgb(15 23 42 / 0.4)
);
border: 1px solid rgb(148 163 184 / 0.18);
box-shadow: 0 0 30px rgb(37 99 235 / 0.18);
}
.ft-icon svg {
width: 26px;
height: 26px;
color: #3b82f6;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.ft-form-heading {
text-align: center;
}
.ft-form-heading h2 {
margin: 0;
color: #f8fafc;
font-size: clamp(24px, 5vw, 30px);
line-height: 1.15;
letter-spacing: -0.03em;
}
.ft-form-heading p {
margin: 10px auto 0;
color: #94a3b8;
font-size: clamp(14px, 2.4vw, 15px);
line-height: 1.55;
max-width: 320px;
}
.ft-dropzone {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
padding: clamp(28px, 6vw, 44px) 20px;
text-align: center;
border: 2px dashed rgb(59 130 246 / 0.45);
border-radius: 16px;
background: rgb(59 130 246 / 0.04);
cursor: pointer;
transition:
border-color 160ms ease,
background-color 160ms ease;
}
.ft-dropzone:hover,
.ft-dropzone.is-dragover {
border-color: rgb(59 130 246 / 0.7);
background: rgb(59 130 246 / 0.08);
}
.ft-dropzone-input {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
margin: 0;
opacity: 0;
cursor: pointer;
}
.ft-dropzone-input:focus-visible ~ .ft-dropzone-icon {
outline: 2px solid #3b82f6;
outline-offset: 4px;
border-radius: 999px;
}
.ft-dropzone-icon {
width: 44px;
height: 44px;
margin-bottom: 6px;
color: #3b82f6;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
pointer-events: none;
}
.ft-dropzone-title {
margin: 0;
color: #f8fafc;
font-size: 17px;
font-weight: 600;
pointer-events: none;
}
.ft-dropzone-hint {
margin: 0;
color: #94a3b8;
font-size: 14px;
pointer-events: none;
}
.ft-browse {
color: #3b82f6;
font-weight: 600;
}
.ft-field {
width: 100%;
}
.ft-field label {
display: block;
margin-bottom: 10px;
color: #e2e8f0;
font-size: 14px;
font-weight: 600;
}
.ft-field label em {
color: #64748b;
font-style: normal;
font-weight: 500;
}
.ft-field textarea {
width: 100%;
resize: vertical;
border: 1px solid rgb(148 163 184 / 0.24);
border-radius: 10px;
background: rgb(15 23 42 / 0.52);
color: #f8fafc;
font: inherit;
font-size: 15px;
padding: 14px 16px;
outline: none;
transition:
border-color 160ms ease,
background-color 160ms ease,
box-shadow 160ms ease;
}
.ft-field textarea::placeholder {
color: #64748b;
}
.ft-field textarea:hover {
border-color: rgb(148 163 184 / 0.38);
}
.ft-field textarea:focus-visible {
border-color: #2563eb;
background: rgb(15 23 42 / 0.78);
box-shadow: 0 0 0 4px rgb(37 99 235 / 0.18);
}
.ft-submit {
appearance: none;
width: 100%;
height: 56px;
border: 0;
border-radius: 10px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 0 22px;
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
color: #fff;
font: inherit;
font-size: 16px;
font-weight: 650;
cursor: pointer;
box-shadow:
inset 0 1px 0 rgb(255 255 255 / 0.2),
0 16px 34px rgb(37 99 235 / 0.18);
transition:
transform 160ms ease,
filter 160ms ease,
box-shadow 160ms ease;
}
.ft-submit svg {
width: 20px;
height: 20px;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.ft-submit:hover {
filter: brightness(1.05);
box-shadow:
inset 0 1px 0 rgb(255 255 255 / 0.22),
0 20px 42px rgb(37 99 235 / 0.24);
}
.ft-submit:active {
transform: translateY(1px);
}
.ft-submit:focus-visible {
outline: none;
box-shadow:
0 0 0 4px rgb(37 99 235 / 0.22),
0 20px 42px rgb(37 99 235 / 0.24);
}
.ft-submit:disabled {
cursor: not-allowed;
opacity: 0.65;
filter: grayscale(0.15);
}
.ft-status {
min-height: 20px;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
color: #94a3b8;
font-size: 13px;
line-height: 1.4;
text-align: center;
}
.ft-status svg {
width: 16px;
height: 16px;
flex: 0 0 auto;
color: #34d399;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.ft-status[data-state="error"] {
color: #f87171;
}
.ft-status[data-state="error"] svg {
color: #f87171;
}
@media (max-width: 480px) {
.ft-upload-demo {
padding: 18px;
}
.ft-upload-demo::before,
.ft-upload-demo::after {
display: none;
}
.ft-window {
border-radius: 20px;
}
.ft-window-bar {
height: 54px;
padding: 0 22px;
}
.ft-form {
gap: 16px;
padding: 28px 22px;
}
.ft-dropzone {
padding: 24px 16px;
}
.ft-submit {
height: 52px;
}
}
</style>
<script>
const form = document.getElementById("upload-form");
const dropzone = form.querySelector(".ft-dropzone");
const input = document.getElementById("ft-files");
const dropzoneTitle = form.querySelector(".ft-dropzone-title");
const status = document.getElementById("ft-status");
const button = form.querySelector("button[type='submit']");
const buttonLabel = button.querySelector("span");
const defaultStatus =
'<svg aria-hidden="true" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"></circle><path d="m9 12 2 2 4-4"></path></svg><span>Files are securely stored and metadata is supported.</span>';
const errorIcon =
'<svg aria-hidden="true" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"></circle><path d="m15 9-6 6"></path><path d="m9 9 6 6"></path></svg>';
function updateFileList() {
if (input.files.length === 1) {
dropzoneTitle.textContent = input.files[0].name;
} else if (input.files.length > 1) {
dropzoneTitle.textContent = `${input.files.length} files selected`;
} else {
dropzoneTitle.textContent = "Drag and drop files here";
}
}
input.addEventListener("change", updateFileList);
dropzone.addEventListener("dragover", (event) => {
event.preventDefault();
dropzone.classList.add("is-dragover");
});
dropzone.addEventListener("dragleave", () => {
dropzone.classList.remove("is-dragover");
});
dropzone.addEventListener("drop", (event) => {
event.preventDefault();
dropzone.classList.remove("is-dragover");
if (event.dataTransfer?.files.length) {
input.files = event.dataTransfer.files;
updateFileList();
}
});
form.addEventListener("submit", async (event) => {
event.preventDefault();
status.removeAttribute("data-state");
status.innerHTML = "<span>Uploading...</span>";
button.disabled = true;
buttonLabel.textContent = "Uploading...";
try {
// Use FormData — do NOT set Content-Type manually.
// The browser sets it with the correct multipart boundary.
const response = await fetch(form.action, {
method: "POST",
headers: { "X-Requested-With": "XMLHttpRequest" },
body: new FormData(form),
});
if (!response.ok) {
throw new Error("Submission failed");
}
form.reset();
updateFileList();
status.innerHTML = defaultStatus;
buttonLabel.textContent = "Submit Files";
} catch {
status.dataset.state = "error";
status.innerHTML =
errorIcon + "<span>Something went wrong. Please try again.</span>";
buttonLabel.textContent = "Try again";
} finally {
button.disabled = false;
}
});
</script>
Why Use This Template
What is a file upload form?
A file upload form lets visitors attach a document, image, or PDF alongside their name and email, using enctype="multipart/form-data" instead of plain text fields. It's built for submissions that need more than a message: applications, support tickets, portfolios.
Why is a file upload form harder to build than a text form?
A file upload has to be received, checked, stored somewhere durable, and linked back to a download URL, which is real backend work most contact-form tutorials skip. Getting it wrong usually means a lost file or a security gap, not just a bug.
How do you add a file upload form with a backend like Formtorch?
Copy the HTML, React, Next.js, or Astro snippet above and point it at your Formtorch endpoint: Formtorch accepts the multipart submission, stores the file in object storage, and links it to the record. File storage is a Pro feature, since it carries real hosting cost; every other field type works on the free plan.
Related read: how to secure a form endpoint against malicious submissions
How it works
Create a Formtorch endpoint
Sign up and create a new form endpoint in the dashboard. You will get a unique URL in seconds.
Create endpointPaste it into your form
Set your form's action attribute to your endpoint URL. No other changes required.
Deploy and start receiving submissions
Every submission is stored in your dashboard, triggers email notifications, and can fire webhooks to any service.
Frequently asked questions
No. Formtorch works with standard HTML forms via the action attribute. No JavaScript required for basic submissions.
Start in less than 5 minutes
Create your endpoint, paste it into any template, and you are live.