jdc-react-mailer
v0.1.0
Published
Reusable, themeable React contact form with Next.js handler and Nodemailer
Readme
jdc-react-mailer
A reusable, themeable React contact form component with a Next.js App Router API route handler powered by Nodemailer. Capture form submissions and send them to a designated email address.
- React 18+ and TypeScript
- Themeable via CSS custom properties or inline
themeprop - Next.js-first: ships a
<ContactForm>component andcreateMailerHandler()for App Router - Nodemailer for SMTP (Gmail, SendGrid, SES, or any SMTP provider)
- Accessible, validated, honeypot spam protection, loading/success/error UX
Install
pnpm add jdc-react-mailer
# or
npm i jdc-react-mailerPeer dependencies: react and react-dom (>=18). For the Next.js handler you need next (>=14) in your app.
Quick start
1. Add the API route (Next.js App Router)
Create app/api/contact/route.ts:
import { createMailerHandler } from 'jdc-react-mailer/handler';
export const { POST } = createMailerHandler({
smtp: {
host: process.env.SMTP_HOST!,
port: Number(process.env.SMTP_PORT ?? 587),
secure: false,
auth: {
user: process.env.SMTP_USER!,
pass: process.env.SMTP_PASS!,
},
},
to: '[email protected]',
from: '[email protected]',
});Set SMTP_HOST, SMTP_PORT, SMTP_USER, and SMTP_PASS in your environment (e.g. .env.local).
2. Use the form in a page
import { ContactForm } from 'jdc-react-mailer';
import 'jdc-react-mailer/style.css';
export default function ContactPage() {
return (
<ContactForm
endpoint="/api/contact"
successMessage="Thanks! I'll be in touch."
/>
);
}Import the stylesheet once (e.g. in your layout or this page). The form POSTs to endpoint with a JSON body; the handler validates it, checks the honeypot, and sends email via Nodemailer.
Component API
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| endpoint | string | required | POST URL (e.g. /api/contact) |
| fields | ('name' \| 'email' \| 'subject' \| 'message')[] | ['name','email','subject','message'] | Field order and inclusion |
| labels | Partial<Record<FormFieldName, string>> | — | Override labels per field |
| placeholders | Partial<Record<FormFieldName, string>> | — | Override placeholders |
| theme | ThemeOverrides | — | Inline CSS variable overrides (see Theming) |
| successMessage | string | "Thanks! I'll be in touch." | Message shown after successful submit |
| submitLabel | string | "Send message" | Submit button text |
| onSuccess | (data: ContactFormPayload) => void | — | Called when submit succeeds |
| onError | (error: Error) => void | — | Called when submit fails |
| className | string | — | Extra class on the form root |
Example with overrides:
<ContactForm
endpoint="/api/contact"
fields={['name', 'email', 'message']}
labels={{ name: 'Full Name', message: 'Your message' }}
placeholders={{ message: 'Say hi...' }}
theme={{ primary: '#0070f3', radius: '10px' }}
successMessage="Got it, thanks!"
onSuccess={(data) => console.log('Sent', data)}
onError={(err) => console.error(err)}
className="my-contact-form"
/>Theming
Styles are driven by CSS custom properties. Import the default stylesheet and override variables where you need:
/* Override in your app (e.g. global.css or a wrapper) */
:root {
--jdcm-primary: #0070f3;
--jdcm-primary-hover: #005bb5;
--jdcm-bg: #ffffff;
--jdcm-surface: #f9fafb;
--jdcm-border: #e2e8f0;
--jdcm-text: #1a202c;
--jdcm-muted: #718096;
--jdcm-error: #e53e3e;
--jdcm-success: #38a169;
--jdcm-radius: 8px;
--jdcm-spacing: 1rem;
--jdcm-font: inherit;
}Or pass a theme prop for inline overrides (no global CSS needed):
<ContactForm
endpoint="/api/contact"
theme={{
primary: '#0070f3',
primaryHover: '#005bb5',
radius: '10px',
fontFamily: 'var(--font-sans)',
}}
/>When theme is set, the root gets data-theme-set so the built-in dark-mode media query does not override your variables.
Handler options
createMailerHandler(config) accepts:
| Option | Type | Description |
|--------|------|-------------|
| smtp | SmtpConfig | Nodemailer transport options (host, port, secure, auth) |
| to | string | Recipient email |
| from | string | Sender (e.g. "Site <[email protected]>") |
| rateLimit | number | Max requests per minute per IP (optional) |
| allowedOrigins | string[] | CORS allowed origins (optional) |
| emailTemplate | (payload: ContactPayload) => string | Custom HTML body (optional) |
Example with rate limit and custom template:
export const { POST } = createMailerHandler({
smtp: { ... },
to: '[email protected]',
from: '[email protected]',
rateLimit: 10,
allowedOrigins: ['https://yoursite.com'],
emailTemplate: (payload) => `
<h2>New message from ${payload.name ?? payload.email}</h2>
<p><strong>Email:</strong> ${payload.email}</p>
<p>${payload.message.replace(/\n/g, '<br>')}</p>
`,
});Exports
- Default (client):
ContactForm, and typesContactFormProps,FormFieldName,FormFieldsConfig,ThemeOverrides,ContactFormPayload,SubmitState. jdc-react-mailer/handler:createMailerHandler, and typesMailerHandlerConfig,ContactPayload,SmtpConfig.jdc-react-mailer/style.css: Default styles (import once in your app).
Scripts
pnpm build— build ESM + CJS and copystyle.csstodistpnpm dev— watch buildpnpm test— run Vitestpnpm lint— ESLintpnpm format— Prettier
Changelog
See CHANGELOG.md.
