Remix Form Handling — Progressive Enhancement Forms
In this tutorial, you will learn about Remix Form Handling. We cover key concepts, practical examples, and best practices to help you master this topic.
Learn Remix form handling: use HTML forms with progressive enhancement, upload files, handle nested forms, and build accessible form components.
In this lesson, you'll build forms that work without JavaScript first, then enhance them with Remix's client-side navigation for a smooth user experience.
What You'll Learn
How Remix forms work with and without JavaScript, build accessible forms, handle file uploads, and implement complex form patterns.
Why It Matters
Progressive enhancement means your forms work for every user regardless of network quality or JavaScript availability. This is especially important for admin tools and internal applications.
Real-World Use
DodaZIP's file upload interface uses enhanced Remix forms that work without JavaScript for reliability, then add drag-and-drop when JS loads.
flowchart LR
A[HTML Form] --> B[JavaScript Disabled]
A --> C[JavaScript Enabled]
B --> D[Native Submit]
C --> E[Enhanced Submit]
D --> F[Page Reload]
E --> G[Client Transition]
style A fill:#121212,color:#fff
Basic Form
import { Form } from "@remix-run/react";
export default function Contact() {
return (
<Form method="post">
<label>
Name:
<input type="text" name="name" required />
</label>
<label>
Message:
<textarea name="message" required />
</label>
<button type="submit">Send</button>
</Form>
);
}
Without JavaScript: The form submits via native POST, causing a full page reload. With JavaScript: Remix intercepts the submission for a smooth transition.
File Upload Form
import { Form, useActionData } from "@remix-run/react";
export const action = async ({ request }) => {
const formData = await request.formData();
const file = formData.get("avatar");
if (!file || file.size === 0) {
return json({ error: "File is required" }, { status: 400 });
}
// Process file (upload to S3, save locally, etc.)
// ...
return redirect("/profile");
};
export default function UploadAvatar() {
const actionData = useActionData<typeof action>();
return (
<Form method="post" encType="multipart/form-data">
<label>
Avatar:
<input type="file" name="avatar" accept="image/*" />
</label>
{actionData?.error && <p>{actionData.error}</p>}
<button type="submit">Upload</button>
</Form>
);
}
Nested Forms
Handle multiple forms on one page with different actions:
export const action = async ({ request }) => {
const formData = await request.formData();
const intent = formData.get("intent");
switch (intent) {
case "update-profile":
return updateProfile(formData);
case "change-password":
return changePassword(formData);
case "delete-account":
return deleteAccount(formData);
default:
throw new Response("Invalid intent", { status: 400 });
}
};
export default function Settings() {
return (
<div>
<Form method="post">
<input type="hidden" name="intent" value="update-profile" />
{/* profile fields */}
<button type="submit">Update Profile</button>
</Form>
<Form method="post">
<input type="hidden" name="intent" value="change-password" />
{/* password fields */}
<button type="submit">Change Password</button>
</Form>
</div>
);
}
Form Accessibility
Always include labels, error messages, and proper types:
<Form method="post">
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
name="email"
required
aria-describedby="email-error"
/>
{errors?.email && (
<p id="email-error" role="alert">{errors.email}</p>
)}
</div>
<button type="submit">Submit</button>
</Form>
Common Mistakes
- Forgetting
encType="multipart/form-data"on file uploads: File uploads require this enctype. Without it, files aren't sent. - Using
fetchinstead of<Form>: The<Form>component provides progressive enhancement. Manualfetchloses this benefit. - Not using
intentfor multiple forms: Without intent identification, multiple forms on one page can conflict. - Ignoring accessibility: Forms without labels and ARIA attributes are inaccessible to screen reader users.
- Not resetting the form after success: After a successful submission, reset form fields or redirect to indicate completion.
Practice Questions
What happens when a Remix form submits without JavaScript? Answer: The browser performs a native POST request, causing a full page reload. The action runs and returns a response.
How do you handle multiple forms on one page? Answer: Add a hidden
intentinput to each form. The action checks this value to determine which operation to perform.What
encTypeis required for file uploads? Answer:multipart/form-data. This enables binary file data to be transmitted in the request body.Why is progressive enhancement important for forms? Answer: It ensures forms work for all users regardless of JavaScript availability, network conditions, or device capabilities.
Challenge
Build a profile settings page with three forms: update name/email, change password, and upload avatar. Each form should use an intent field and validate server-side.
Mini Project
Create a blog comment system with: a comment form that works without JS, reply-to-comment functionality, file attachments for images, and moderation actions (approve/reject) using intent-based forms.
FAQ
What's Next
Learn about Remix Form Validation for server-side and client-side validation patterns.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro