xiaohongshu
xiaohongshu
form-design
Build accessible, user-friendly forms with validation. Covers react-hook-form, Zod schemas, error handling UX, multi-step forms, input patterns, and form accessibility. Use for registration forms, checkout flows, data entry, and user input.
Full skill instructions
Create accessible, validated forms with excellent user experience.
npm install react-hook-form zod @hookform/resolvers
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
// 1. Define schema
const signupSchema = z.object({
email: z
.string()
.min(1, 'Email is required')
.email('Please enter a valid email'),
password: z
.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Password must contain an uppercase letter')
.regex(/[0-9]/, 'Password must contain a number'),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ['confirmPassword'],
});
type SignupForm = z.infer<typeof signupSchema>;
// 2. Create form
function SignupForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<SignupForm>({
resolver: zodResolver(signupSchema),
});
const onSubmit = async (data: SignupForm) => {
try {
await api.signup(data);
// Handle success
} catch (error) {
// Handle error
}
};
return (
<form onSubmit={handleSubmit(onSubmit)} noValidate>
<FormField
label="Email"
type="email"
error={errors.email?.message}
{...register('email')}
/>
<FormField
label="Password"
type="password"
error={errors.password?.message}
{...register('password')}
/>
<FormField
label="Confirm Password"
type="password"
error={errors.confirmPassword?.message}
{...register('confirmPassword')}
/>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Creating account...' : 'Sign Up'}
</button>
</form>
);
}
import { forwardRef } from 'react';
interface FormFieldProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string;
error?: string;
hint?: string;
}
export const FormField = forwardRef<HTMLInputElement, FormFieldProps>(
({ label, error, hint, id, type = 'text', ...props }, ref) => {
const inputId = id || label.toLowerCase().replace(/\s+/g, '-');
const errorId = `${inputId}-error`;
const hintId = `${inputId}-hint`;
return (
<div className="space-y-1.5">
<label
htmlFor={inputId}
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{label}
{props.required && <span className="text-red-500 ml-1">*</span>}
</label>
<input
ref={ref}
id={inputId}
type={type}
className={`
w-full px-3 py-2 rounded-lg border transition-colors
${error
? 'border-red-500 focus:ring-red-500 focus:border-red-500'
: 'border-gray-300 dark:border-gray-600 focus:ring-blue-500 focus:border-blue-500'
}
bg-white dark:bg-gray-800
text-gray-900 dark:text-gray-100
placeholder-gray-400
focus:outline-none focus:ring-2
disabled:bg-gray-100 disabled:cursor-not-allowed
`}
aria-invalid={!!error}
aria-describedby={
error ? errorId : hint ? hintId : undefined
}
{...props}
/>
{hint && !error && (
<p id={hintId} className="text-sm text-gray-500">
{hint}
</p>
)}
{error && (
<p id={errorId} className="text-sm text-red-600 flex items-center gap-1" role="alert">
<ExclamationCircleIcon className="w-4 h-4" />
{error}
</p>
)}
</div>
);
}
);
interface SelectFieldProps {
label: string;
options: { value: string; label: string }[];
error?: string;
placeholder?: string;
}
export const SelectField = forwardRef<HTMLSelectElement, SelectFieldProps>(
({ label, options, error, placeholder, ...props }, ref) => {
const id = label.toLowerCase().replace(/\s+/g, '-');
return (
<div className="space-y-1.5">
<label htmlFor={id} className="block text-sm font-medium text-gray-700">
{label}
</label>
<select
ref={ref}
id={id}
className={`
w-full px-3 py-2 rounded-lg border
${error ? 'border-red-500' : 'border-gray-300'}
bg-white focus:ring-2 focus:ring-blue-500
`}
aria-invalid={!!error}
{...props}
>
{placeholder && (
<option value="" disabled>
{placeholder}
</option>
)}
{options.map(opt => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
{error && <p className="text-sm text-red-600">{error}</p>}
</div>
);
}
);
interface CheckboxGroupProps {
label: string;
options: { value: string; label: string }[];
value: string[];
onChange: (value: string[]) => void;
error?: string;
}
function CheckboxGroup({ label, options, value, onChange, error }: CheckboxGroupProps) {
const handleChange = (optionValue: string, checked: boolean) => {
if (checked) {
onChange([...value, optionValue]);
} else {
onChange(value.filter(v => v !== optionValue));
}
};
return (
<fieldset>
<legend className="text-sm font-medium text-gray-700 mb-2">{label}</legend>
<div className="space-y-2">
{options.map(opt => (
<label key={opt.value} className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={value.includes(opt.value)}
onChange={(e) => handleChange(opt.value, e.target.checked)}
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-gray-700">{opt.label}</span>
</label>
))}
</div>
{error && <p className="text-sm text-red-600 mt-1">{error}</p>}
</fieldset>
);
}
interface RadioGroupProps {
label: string;
options: { value: string; label: string; description?: string }[];
value: string;
onChange: (value: string) => void;
error?: string;
}
function RadioGroup({ label, options, value, onChange, error }: RadioGroupProps) {
return (
<fieldset>
<legend className="text-sm font-medium text-gray-700 mb-3">{label}</legend>
<div className="space-y-3">
{options.map(opt => (
<label
key={opt.value}
className={`
flex items-start gap-3 p-4 rounded-lg border cursor-pointer
${value === opt.value
? 'border-blue-500 bg-blue-50 ring-2 ring-blue-500'
: 'border-gray-200 hover:border-gray-300'
}
`}
>
<input
type="radio"
name={label}
value={opt.value}
checked={value === opt.value}
onChange={() => onChange(opt.value)}
className="mt-0.5 w-4 h-4 text-blue-600 focus:ring-blue-500"
/>
<div>
<span className="font-medium text-gray-900">{opt.label}</span>
{opt.description && (
<p className="text-sm text-gray-500">{opt.description}</p>
)}
</div>
</label>
))}
</div>
{error && <p className="text-sm text-red-600 mt-2">{error}</p>}
</fieldset>
);
}
import { useState } from 'react';
import { useForm, FormProvider } from 'react-hook-form';
const steps = [
{ id: 'account', title: 'Account', component: AccountStep },
{ id: 'profile', title: 'Profile', component: ProfileStep },
{ id: 'preferences', title: 'Preferences', component: PreferencesStep },
];
function MultiStepForm() {
const [currentStep, setCurrentStep] = useState(0);
const methods = useForm({ mode: 'onChange' });
const CurrentStepComponent = steps[currentStep].component;
const isLastStep = currentStep === steps.length - 1;
const next = async () => {
const isValid = await methods.trigger(); // Validate current step
if (isValid && !isLastStep) {
setCurrentStep(prev => prev + 1);
}
};
const back = () => {
if (currentStep > 0) {
setCurrentStep(prev => prev - 1);
}
};
const onSubmit = async (data: FormData) => {
await api.submit(data);
};
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
{/* Progress indicator */}
<nav aria-label="Progress" className="mb-8">
<ol className="flex items-center">
{steps.map((step, index) => (
<li key={step.id} className="flex items-center">
<span className={`
w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium
${index < currentStep
? 'bg-blue-600 text-white'
: index === currentStep
? 'border-2 border-blue-600 text-blue-600'
: 'border-2 border-gray-300 text-gray-500'
}
`}>
{index < currentStep ? '✓' : index + 1}
</span>
<span className="ml-2 text-sm font-medium text-gray-900">
{step.title}
</span>
{index < steps.length - 1 && (
<div className="w-12 h-0.5 mx-4 bg-gray-200" />
)}
</li>
))}
</ol>
</nav>
{/* Current step */}
<CurrentStepComponent />
{/* Navigation */}
<div className="flex justify-between mt-8">
<button
type="button"
onClick={back}
disabled={currentStep === 0}
className="px-4 py-2 border rounded-lg disabled:opacity-50"
>
Back
</button>
{isLastStep ? (
<button type="submit" className="px-4 py-2 bg-blue-600 text-white rounded-lg">
Submit
</button>
) : (
<button type="button" onClick={next} className="px-4 py-2 bg-blue-600 text-white rounded-lg">
Continue
</button>
)}
</div>
</form>
</FormProvider>
);
}
// Show error immediately below field
{error && (
<p className="text-sm text-red-600 mt-1 flex items-center gap-1">
<ExclamationCircleIcon className="w-4 h-4 flex-shrink-0" />
{error}
</p>
)}
// Show all errors at top of form
function ErrorSummary({ errors }: { errors: Record<string, { message?: string }> }) {
const errorList = Object.entries(errors).filter(([_, v]) => v.message);
if (errorList.length === 0) return null;
return (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6" role="alert">
<h3 className="text-red-800 font-medium mb-2">
Please fix the following errors:
</h3>
<ul className="list-disc list-inside text-sm text-red-700 space-y-1">
{errorList.map(([field, error]) => (
<li key={field}>
<a href={`#${field}`} className="underline">
{error.message}
</a>
</li>
))}
</ul>
</div>
);
}
<label> with htmlForaria-describedbyaria-invalid="true"role="alert" for screen readersxiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience