Modern Contact Forms
Chapter 8
2 min read
The final piece of your portfolio is the Contact Form. By using Next.js Server Actions, you can handle form submissions directly on the server without creating a separate API route or requiring client-side JS for the submission to work.
actions.ts)This is a secure, server-side function that processes the form data. You can integrate it with Nodemailer or a service like Resend.
// src/app/contact/actions.ts
"use server";
export async function sendEmail(formData: FormData) {
const name = formData.get('name');
const email = formData.get('email');
const message = formData.get('message');
// Logic to send email (e.g., via Resend)
console.log(`Message from ${name} (${email}): ${message}`);
return { success: true };
}We use a Client Component only to handle the "Pending" state and "Success" notifications for a better user experience.
"use client";
import { useFormStatus } from 'react-dom';
import { sendEmail } from './actions';
import styles from './ContactForm.module.scss';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending} className={styles.submitBtn}>
{pending ? 'Sending...' : 'Send Message'}
</button>
);
}
export default function ContactForm() {
return (
<form action={sendEmail} className={styles.form}>
<div className={styles.field}>
<label htmlFor="name">Name</label>
<input type="text" id="name" name="name" required />
</div>
<div className={styles.field}>
<label htmlFor="email">Email</label>
<input type="email" id="email" name="email" required />
</div>
<div className={styles.field}>
<label htmlFor="message">Message</label>
<textarea id="message" name="message" rows={5} required />
</div>
<SubmitButton />
</form>
);
}We’ll use our @include dark mixin to ensure the form inputs remain readable in all themes.
.form {
max-width: 500px;
margin: $spacing-lg auto;
.field {
margin-bottom: $spacing-sm;
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
}
input, textarea {
width: 100%;
padding: 0.8rem;
border: 1px solid #ddd;
border-radius: 4px;
background: $color-white;
@include dark {
background: #1a1a1a;
border-color: #333;
color: $text-dark;
}
}
}
.submitBtn {
background: $color-crimson;
color: white;
padding: 1rem 2rem;
border: none;
cursor: pointer;
transition: opacity 0.2s;
&:disabled {
opacity: 0.5;
}
}
}Why Server Actions?
- Zero Bundle Size: No need for large form libraries like Formik or Yup for simple validations.
- Progressive Enhancement: The form works even if the user has disabled JavaScript.
- Security: Logic is executed on the server, keeping your API keys and environment variables safe.