Claude-skill-registry integrating-formspree-forms
Use when adding forms to static websites using Formspree - provides contact forms, newsletter signups, validation, and spam protection without backend code
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/integrating-formspree-forms" ~/.claude/skills/majiayu000-claude-skill-registry-integrating-formspree-forms && rm -rf "$T"
manifest:
skills/data/integrating-formspree-forms/SKILL.mdsource content
Integrating Formspree Forms
Overview
Form handling for static sites using Formspree - no backend needed.
Why Formspree
- Dead simple setup
- Email notifications built-in
- Spam protection included
- Free tier: 50 submissions/month
- Paid tier: $10/mo for 1000 submissions
Setup
1. Create Formspree account at https://formspree.io
2. Install dependencies:
pnpm add @formspree/react
3. Get your form ID from Formspree dashboard
Basic Contact Form
// components/forms/ContactForm.tsx 'use client' import { useForm, ValidationError } from '@formspree/react' export default function ContactForm() { const [state, handleSubmit] = useForm("YOUR_FORM_ID") if (state.succeeded) { return <p>Thanks for your message! We'll get back to you soon.</p> } return ( <form onSubmit={handleSubmit}> <div> <label htmlFor="name">Name</label> <input id="name" type="text" name="name" required /> <ValidationError prefix="Name" field="name" errors={state.errors} /> </div> <div> <label htmlFor="email">Email</label> <input id="email" type="email" name="email" required /> <ValidationError prefix="Email" field="email" errors={state.errors} /> </div> <div> <label htmlFor="message">Message</label> <textarea id="message" name="message" rows={6} required /> <ValidationError prefix="Message" field="message" errors={state.errors} /> </div> <button type="submit" disabled={state.submitting}> {state.submitting ? 'Sending...' : 'Send Message'} </button> </form> ) }
Newsletter Form
'use client' import { useForm, ValidationError } from '@formspree/react' export default function NewsletterForm() { const [state, handleSubmit] = useForm("YOUR_NEWSLETTER_FORM_ID") if (state.succeeded) { return <p>Thanks for subscribing!</p> } return ( <form onSubmit={handleSubmit}> <input type="email" name="email" placeholder="Email Address" required /> <ValidationError prefix="Email" field="email" errors={state.errors} /> <button type="submit" disabled={state.submitting}> {state.submitting ? 'Subscribing...' : 'Sign Up'} </button> {/* Honeypot for spam protection */} <input type="text" name="_gotcha" style={{ display: 'none' }} tabIndex={-1} /> </form> ) }
Enhanced Validation (React Hook Form + Zod)
pnpm add react-hook-form @hookform/resolvers zod
'use client' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { z } from 'zod' import { useForm as useFormspree } from '@formspree/react' const contactSchema = z.object({ name: z.string().min(2, 'Name must be at least 2 characters'), email: z.string().email('Please enter a valid email'), message: z.string().min(10, 'Message must be at least 10 characters'), }) type ContactFormData = z.infer<typeof contactSchema> export default function ContactFormEnhanced() { const [formspreeState, submitToFormspree] = useFormspree("YOUR_FORM_ID") const { register, handleSubmit, formState: { errors, isSubmitting }, reset, } = useForm<ContactFormData>({ resolver: zodResolver(contactSchema), }) const onSubmit = async (data: ContactFormData) => { await submitToFormspree(data) if (formspreeState.succeeded) reset() } if (formspreeState.succeeded) { return <p>Thanks for your message!</p> } return ( <form onSubmit={handleSubmit(onSubmit)}> <div> <label htmlFor="name">Name</label> <input {...register('name')} id="name" type="text" /> {errors.name && <p>{errors.name.message}</p>} </div> <div> <label htmlFor="email">Email</label> <input {...register('email')} id="email" type="email" /> {errors.email && <p>{errors.email.message}</p>} </div> <div> <label htmlFor="message">Message</label> <textarea {...register('message')} id="message" rows={6} /> {errors.message && <p>{errors.message.message}</p>} </div> <button type="submit" disabled={isSubmitting}> {isSubmitting ? 'Sending...' : 'Send Message'} </button> </form> ) }
Spam Protection
Honeypot field:
<input type="text" name="_gotcha" style={{ display: 'none' }} tabIndex={-1} autoComplete="off" aria-hidden="true" />
reCAPTCHA: Configure in Formspree dashboard for additional protection.
Environment Variables
# .env.local NEXT_PUBLIC_FORMSPREE_CONTACT_ID=abcd1234 NEXT_PUBLIC_FORMSPREE_NEWSLETTER_ID=efgh5678
const [state, handleSubmit] = useForm( process.env.NEXT_PUBLIC_FORMSPREE_CONTACT_ID! )
Testing
E2E test with Playwright:
import { test, expect } from '@playwright/test' test('contact form submission works', async ({ page }) => { await page.goto('/contact') await page.fill('input[name="name"]', 'Test User') await page.fill('input[name="email"]', 'test@example.com') await page.fill('textarea[name="message"]', 'Test message') await page.click('button[type="submit"]') await expect(page.locator('text=Thanks for your message')).toBeVisible() })
Accessibility
<form aria-label="Contact form"> <div> <label htmlFor="email"> Email <span className="sr-only">(required)</span> </label> <input id="email" type="email" name="email" required aria-required="true" aria-invalid={!!errors.email} aria-describedby={errors.email ? "email-error" : undefined} /> {errors.email && ( <p id="email-error" role="alert"> {errors.email.message} </p> )} </div> <button type="submit" aria-disabled={isSubmitting}> {isSubmitting ? 'Sending...' : 'Send Message'} </button> </form>
Best Practices
- Always validate client-side - Better UX
- Use honeypot fields - Simple spam protection
- Show loading states - User feedback during submission
- Clear success messages - Confirm submission worked
- Accessible labels - All inputs need labels
- Error handling - Show helpful error messages
- Environment variables - Keep form IDs out of code
- Test thoroughly - E2E tests for critical forms