File upload form
Attachments are a Pro feature. On the free tier the file is dropped and the rest of the submission still arrives, so nothing is silently lost while you decide. The one thing to copy either way is enctype="multipart/form-data": without it the browser sends the file name and not the file.
HTML · CSS · Attachments need a Pro plan
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Send us a file</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
body {
margin: 0;
padding: 32px 16px;
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
background: #f4f4f5;
color: #18181b;
}
.card {
max-width: 460px;
margin: 0 auto;
padding: 28px;
background: #fff;
border: 1px solid #e4e4e7;
border-radius: 12px;
}
h1 { margin: 0 0 6px; font-size: 20px; }
.sub { margin: 0 0 22px; font-size: 14px; color: #71717a; }
.field { margin-bottom: 16px; }
label { display: block; margin-bottom: 6px; font-size: 13px; font-weight: 500; }
input, textarea, select {
width: 100%;
padding: 10px 12px;
font: inherit;
font-size: 14px;
color: #18181b;
background: #fff;
border: 1px solid #d4d4d8;
border-radius: 8px;
}
input:focus, textarea:focus, select:focus {
outline: 2px solid #18181b;
outline-offset: 1px;
border-color: transparent;
}
textarea { min-height: 110px; resize: vertical; }
button {
width: 100%;
padding: 11px 16px;
font: inherit;
font-size: 14px;
font-weight: 500;
color: #fff;
background: #18181b;
border: 0;
border-radius: 8px;
cursor: pointer;
}
button:hover { background: #3f3f46; }
button:active { transform: scale(0.99); }
button[disabled] { opacity: 0.6; cursor: default; }
/* Hidden from people, filled in by bots. Never remove it. */
.hp { display: none; }
input[type='file'] { padding: 8px; font-size: 13px; }
input[type='file']::file-selector-button {
margin-right: 10px;
padding: 6px 10px;
font: inherit;
font-size: 13px;
color: #18181b;
background: #f4f4f5;
border: 1px solid #d4d4d8;
border-radius: 6px;
cursor: pointer;
}
</style>
</head>
<body>
<!-- enctype is the whole trick. Without it you get a filename, not a file. -->
<form class="card" action="https://app.portform.co/api/submit" method="POST" enctype="multipart/form-data">
<h1>Send us a file</h1>
<p class="sub">PDF, PNG or JPG, up to 5 MB.</p>
<input type="hidden" name="access_key" value="YOUR_ACCESS_KEY" />
<input type="checkbox" name="botcheck" class="hp" tabindex="-1" autocomplete="off" />
<div class="field">
<label for="name">Name</label>
<input id="name" type="text" name="name" autocomplete="name" required />
</div>
<div class="field">
<label for="email">Email</label>
<input id="email" type="email" name="email" autocomplete="email" required />
</div>
<div class="field">
<label for="attachment">Attachment</label>
<input id="attachment" type="file" name="attachment" accept=".pdf,.png,.jpg,.jpeg" required />
</div>
<div class="field">
<label for="message">Message</label>
<textarea id="message" name="message"></textarea>
</div>
<button type="submit">Send</button>
</form>
</body>
</html>Replace YOUR_ACCESS_KEY with the access key from your dashboard. Create one free. The endpoint and the honeypot are already in place.
Useful for
The part behind every one of these is a form backend. This template posts to a PortForm form endpoint, which stores each submission and sends it to your email; the docs cover the fields and responses.
Other templates
- Basic HTML contact formHTML · CSS
- Ajax contact formHTML · CSS · JavaScript
- JavaScript form validationHTML · CSS · JavaScript
- Multi-column contact formHTML · CSS
- Masked phone inputHTML · CSS · JavaScript
- Contact form with dark modeHTML · CSS
- Raw contact form (no CSS)HTML
- Multi-step NPS surveyHTML · CSS · JavaScript
- Multi-step survey formVue · HTML · CSS
- Contact form popupTailwindCSS · Alpine.js · HTML
- Popover feedback widgetTailwindCSS · Alpine.js · HTML
- JavaScript file upload formHTML · CSS · JavaScript
- React contact formReact · HTML · CSS