performa
v1.0.6
Published
Type-safe React forms with server-side validation and framework adapters
Downloads
19
Maintainers
Readme
performa
A lightweight, framework-agnostic React form library with built-in server-side validation, TypeScript support, and adapters for popular React frameworks.
Features
- Framework Agnostic: Works with Next.js, React Router, TanStack Start, and any React framework
- Server-Side Validation: Secure form validation with comprehensive validation rules
- TypeScript First: Full type safety with excellent IDE autocomplete
- Accessible: Built-in ARIA attributes and keyboard navigation
- Themeable: Fully customisable with Tailwind CSS or custom styling
- Lightweight: Core library is only 7.69 KB (brotli compressed)
- File Uploads: Built-in support for file validation and previews
- Dark Mode: Native dark mode support out of the box
Installation
npm install performaFramework-Specific Installation
For Next.js (App Router):
npm install performa react-domFor React Router:
npm install performa react-routerFor TanStack Start:
npm install performa @tanstack/startSetup
performa uses Tailwind CSS for styling plus a custom CSS file for themeable colors.
Step 1: Install Tailwind CSS
Follow the Tailwind CSS installation guide for your framework.
Step 2: Import performa CSS
Import the performa CSS file in your app:
// In your main app file or layout
import 'performa/dist/performa.css';Step 3: Configure Tailwind to scan performa
Add performa as a source in your CSS file:
@import "tailwindcss";
/* Add performa library as a source */
@source "../node_modules/performa/dist";Customize Colors (Optional)
Override CSS variables in your own CSS file to change theme colors:
:root {
--performa-primary: #10b981; /* green-500 */
--performa-primary-hover: #059669; /* green-600 */
--performa-primary-focus: #10b981;
--performa-error: #ef4444;
--performa-success: #22c55e;
}Available CSS Variables:
--performa-primary/--performa-primary-hover/--performa-primary-focus- Primary colors--performa-error/--performa-error-bg/--performa-error-border- Error states--performa-success/--performa-success-bg/--performa-success-border- Success states--performa-warning/--performa-warning-bg/--performa-warning-border- Warning states--performa-info/--performa-info-bg/--performa-info-border- Info states--performa-border/--performa-bg/--performa-text- Base styling--performa-text-secondary/--performa-placeholder/--performa-disabled-bg- Additional states
Colors work at runtime - no Tailwind rebuild needed!
Browser Compatibility:
The CSS uses color-mix() for focus ring effects, which requires:
- Chrome 111+ (Feb 2023)
- Safari 16.2+ (Dec 2022)
- Firefox 113+ (May 2023)
If you need to support older browsers, you can override the focus styles with fallback values.
Quick Start
1. Define Your Form Configuration
import type { FormConfig } from 'performa';
const loginForm = {
key: 'login',
method: 'post',
action: '/api/login',
fields: {
email: {
type: 'email',
label: 'Email Address',
placeholder: 'Enter your email',
rules: {
required: true,
isEmail: true,
},
},
password: {
type: 'password',
label: 'Password',
placeholder: 'Enter your password',
rules: {
required: true,
minLength: 8,
},
},
remember: {
type: 'checkbox',
label: 'Remember me',
},
submit: {
type: 'submit',
label: 'Sign In',
},
},
} satisfies FormConfig;2. Choose Your Framework Adapter
Next.js (App Router)
// app/login/page.tsx
'use client';
import { NextForm } from 'performa/nextjs';
import { loginForm } from './form-config';
export default function LoginPage() {
return (
<NextForm
config={loginForm}
action={submitLogin}
onSuccess={(data) => {
console.log('Login successful', data);
}}
/>
);
}// app/login/actions.ts
'use server';
import { validateForm } from 'performa/server';
import { loginForm } from './form-config';
export async function submitLogin(prevState: any, formData: FormData) {
const result = await validateForm(
new Request('', { method: 'POST', body: formData }),
loginForm
);
if (result.hasErrors) {
return { errors: result.errors };
}
// Process login with result.values
const { email, password } = result.values;
// Your authentication logic here
return { success: true };
}React Router
// routes/login.tsx
import { ReactRouterForm } from 'performa/react-router';
import { loginForm } from './form-config';
export default function LoginRoute() {
return <ReactRouterForm config={loginForm} />;
}
// Action handler
import { validateForm } from 'performa/server';
export async function action({ request }: ActionFunctionArgs) {
const result = await validateForm(request, loginForm);
if (result.hasErrors) {
return { errors: result.errors };
}
// Process login
return redirect('/dashboard');
}TanStack Start
import { TanStackStartForm } from 'performa/tanstack-start';
import { createServerFn } from '@tanstack/start';
import { validateForm } from 'performa/server';
const submitLogin = createServerFn({ method: 'POST' }).handler(
async ({ data }: { data: FormData }) => {
const result = await validateForm(
new Request('', { method: 'POST', body: data }),
loginForm
);
if (result.hasErrors) {
return { errors: result.errors };
}
return { success: true };
}
);
export default function LoginPage() {
return <TanStackStartForm config={loginForm} action={submitLogin} />;
}Form Configuration
Field Types
performa supports the following field types:
Text Inputs
text- Standard text inputemail- Email input with validationpassword- Password input with maskingurl- URL inputtel- Telephone number inputnumber- Numeric inputdate- Date pickertime- Time picker
Textareas
textarea- Multi-line text input
Select Inputs
select- Dropdown selection with options
Radio Inputs
radio- Radio button group
Checkboxes and Toggles
checkbox- Single checkboxtoggle- Toggle switch
File Inputs
file- File upload with validation and preview
Special Types
datetime- Combined date and time pickerhidden- Hidden input fieldnone- Display-only field (no input)submit- Submit button
Validation Rules
All validation rules are applied server-side for security:
{
rules: {
required: true, // Field is required
minLength: 8, // Minimum character length
maxLength: 100, // Maximum character length
pattern: /^[A-Z0-9]+$/, // Custom regex pattern
matches: 'password', // Must match another field
isEmail: true, // Valid email format
isUrl: true, // Valid URL format
isPhone: true, // Valid phone number
isDate: true, // Valid date
isTime: true, // Valid time
isNumber: true, // Valid number
isInteger: true, // Valid integer
isAlphanumeric: true, // Only letters and numbers
isSlug: true, // Valid URL slug
isUUID: true, // Valid UUID
denyHtml: true, // Reject HTML tags
weakPasswordCheck: true, // Check against known breached passwords
minValue: 0, // Minimum numeric value
maxValue: 100, // Maximum numeric value
mustBeEither: ['admin', 'user'], // Value must be one of the options
mimeTypes: ['JPEG', 'PNG', 'PDF'], // Allowed file types
maxFileSize: 5 * 1024 * 1024, // Maximum file size in bytes
}
}Field Configuration Options
{
type: 'text', // Field type (required)
label: 'Username', // Field label (required)
placeholder: 'Enter username', // Placeholder text
defaultValue: '', // Default value
defaultChecked: false, // Default checked state (checkbox/toggle)
disabled: false, // Disable the field
className: 'custom-class', // Additional CSS classes
before: 'Help text above', // Content before the field
beforeClassName: 'text-sm', // Classes for before content
after: 'Help text below', // Content after the field
afterClassName: 'text-sm', // Classes for after content
uploadDir: 'avatars', // Upload directory for files
width: 'full', // Field width (full, half, third, quarter)
options: [ // Options for select/radio
{ value: 'opt1', label: 'Option 1' },
{ value: 'opt2', label: 'Option 2' },
],
rules: { /* validation rules */ },
}Server-Side Validation
Validation Function
The validateForm function performs server-side validation and returns typed results:
import { validateForm } from 'performa/server';
const result = await validateForm(request, formConfig);
if (result.hasErrors) {
// result.errors contains field-specific error messages
return { errors: result.errors };
}
// result.values contains validated and typed form data
const { email, password } = result.values;Custom Error Messages
You can customise validation error messages:
import { defaultErrorMessages } from 'performa/server';
// customise individual messages
defaultErrorMessages.required = (label) => `${label} is mandatory`;
defaultErrorMessages.minLength = (label, min) =>
`${label} needs at least ${min} characters`;Return Type
errors: { fieldName?: string; // Error message for each field __server?: string; // Server-level error message } | undefined; values: { fieldName: string | boolean | File; // Validated values }; hasErrors: boolean; // Quick check for validation failure }
## File Uploads
### Configuration
```typescript
{
avatar: {
type: 'file',
label: 'Profile Picture',
rules: {
required: true,
mimeTypes: ['JPEG', 'PNG', 'WEBP'],
maxFileSize: 2 * 1024 * 1024, // 2MB
},
uploadDir: 'avatars', // Optional: subdirectory for uploads
}
}Supported MIME Types
- Images:
JPEG,PNG,GIF,WEBP,SVG,BMP,TIFF - Documents:
PDF,DOC,DOCX,XLS,XLSX,PPT,PPTX,TXT,CSV - Archives:
ZIP,RAR,TAR,GZIP - Media:
MP3,MP4,WAV,AVI,MOV
File Preview
Files are automatically previewed if:
- A
baseUrlprop is provided to the form component - The file is an image type
- A
defaultValueexists (for editing forms)
<NextForm
config={formConfig}
action={submitForm}
fileBaseUrl="https://cdn.example.com"
/>Handling File Uploads
export async function submitForm(prevState: any, formData: FormData) {
const result = await validateForm(
new Request('', { method: 'POST', body: formData }),
formConfig
);
if (result.hasErrors) {
return { errors: result.errors };
}
const file = result.values.avatar as File;
// Upload file to storage
const buffer = Buffer.from(await file.arrayBuffer());
const filename = `${Date.now()}-${file.name}`;
// Save to file system, S3, etc.
await saveFile(filename, buffer);
return { success: true };
}Theming
Default Theme
performa comes with a default theme optimized for Tailwind CSS with dark mode support:
import { FormThemeProvider } from 'performa';
function App() {
return (
<FormThemeProvider>
{/* Your forms here */}
</FormThemeProvider>
);
}Custom Theme
Override the default theme with your own styles:
import { FormThemeProvider } from 'performa';
const customTheme = {
formGroup: 'mb-6',
label: {
base: 'block text-sm font-semibold mb-2',
required: 'text-red-600 ml-1',
},
input: {
base: 'w-full px-4 py-3 border rounded-lg focus:ring-2',
error: 'border-red-500 focus:ring-red-500',
},
error: 'text-red-600 text-sm mt-2',
button: {
primary: 'bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700',
},
};
function App() {
return (
<FormThemeProvider theme={customTheme}>
{/* Your forms here */}
</FormThemeProvider>
);
}Theme Structure
The complete theme object structure:
{
form: string; // Form element styles
formGroup: string; // Field group wrapper
fieldset: string; // Fieldset styles
label: {
base: string; // Label base styles
required: string; // Required asterisk styles
};
input: {
base: string; // Input base styles
error: string; // Error state styles
};
textarea: {
base: string;
error: string;
};
select: {
base: string;
error: string;
};
checkbox: {
base: string;
label: string;
error: string;
};
radio: {
group: string;
base: string;
label: string;
};
toggle: {
wrapper: string;
base: string;
slider: string;
label: string;
};
file: {
dropzone: string;
dropzoneActive: string;
dropzoneError: string;
icon: string;
text: string;
hint: string;
};
datetime: {
input: string;
iconButton: string;
dropdown: string;
navButton: string;
monthYear: string;
weekday: string;
day: string;
daySelected: string;
timeLabel: string;
timeInput: string;
formatButton: string;
periodButton: string;
periodButtonActive: string;
};
button: {
primary: string;
secondary: string;
};
alert: {
base: string;
error: string;
success: string;
warning: string;
info: string;
};
error: string;
}Custom Labels
customise form labels and messages:
import { FormThemeProvider } from 'performa';
const customLabels = {
fileUpload: {
clickToUpload: 'Choose file',
dragAndDrop: 'or drag and drop',
allowedTypes: 'Supported formats:',
maxSize: 'Maximum size:',
},
datetime: {
weekdays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
clear: 'Clear',
done: 'Done',
timeFormat: 'Time Format',
},
};
function App() {
return (
<FormThemeProvider labels={customLabels}>
{/* Your forms here */}
</FormThemeProvider>
);
}Advanced Usage
Using Hooks for Custom Forms
Next.js
import { useNextForm } from 'performa/nextjs';
function CustomForm() {
const { formAction, errors, isPending, isSuccess } = useNextForm(
submitForm,
(data) => {
console.log('Success!', data);
}
);
return (
<form action={formAction}>
<input name="email" type="email" />
{errors?.email && <p>{errors.email}</p>}
<button disabled={isPending}>
{isPending ? 'Submitting...' : 'Submit'}
</button>
{isSuccess && <p>Form submitted successfully!</p>}
</form>
);
}React Router
import { useReactRouterForm } from 'performa/react-router';
function CustomForm() {
const { fetcher, errors, isSubmitting } = useReactRouterForm('my-form');
return (
<fetcher.Form method="post">
<input name="email" type="email" />
{errors?.email && <p>{errors.email}</p>}
<button disabled={isSubmitting}>Submit</button>
</fetcher.Form>
);
}TanStack Start
import { useTanStackStartForm } from 'performa/tanstack-start';
function CustomForm() {
const { handleSubmit, errors, isPending } = useTanStackStartForm(
submitForm
);
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" />
{errors?.email && <p>{errors.email}</p>}
<button disabled={isPending}>Submit</button>
</form>
);
}Conditional Fields
const formConfig = {
key: 'signup',
fields: {
accountType: {
type: 'select',
label: 'Account Type',
options: [
{ value: 'personal', label: 'Personal' },
{ value: 'business', label: 'Business' },
],
rules: { required: true },
},
// Only show company name for business accounts
...(accountType === 'business' && {
companyName: {
type: 'text',
label: 'Company Name',
rules: { required: true },
},
}),
},
} satisfies FormConfigMulti-Step Forms
const step1Config = {
key: 'registration-step1',
fields: {
email: { type: 'email', label: 'Email', rules: { required: true } },
password: { type: 'password', label: 'Password', rules: { required: true } },
submit: { type: 'submit', label: 'Continue' },
},
} satisfies FormConfig;
const step2Config = {
key: 'registration-step2',
fields: {
firstName: { type: 'text', label: 'First Name', rules: { required: true } },
lastName: { type: 'text', label: 'Last Name', rules: { required: true } },
submit: { type: 'submit', label: 'Complete Registration' },
},
} satisfies FormConfig;
function MultiStepForm() {
const [step, setStep] = useState(1);
return (
<>
{step === 1 && (
<NextForm
config={step1Config}
action={submitStep1}
onSuccess={() => setStep(2)}
/>
)}
{step === 2 && (
<NextForm
config={step2Config}
action={submitStep2}
/>
)}
</>
);
}Dynamic Field Options
function DynamicForm() {
const [categories, setCategories] = useState([]);
useEffect(() => {
fetch('/api/categories')
.then(res => res.json())
.then(data => setCategories(data));
}, []);
const formConfig = {
key: 'product',
fields: {
category: {
type: 'select',
label: 'Category',
options: categories.map(cat => ({
value: cat.id,
label: cat.name,
})),
rules: { required: true },
},
},
} satisfies FormConfig
return <NextForm config={formConfig} action={submitProduct} />;
}TypeScript
performa is built with TypeScript and provides full type safety:
import { FormConfig } from 'performa';
import { validateForm } from 'performa/server';
// Type-safe form configuration using 'satisfies'
// This gives you both type checking AND proper inference
const formConfig = {
key: 'contact',
fields: {
name: {
type: 'text',
label: 'Name',
rules: { required: true },
},
email: {
type: 'email',
label: 'Email',
rules: { required: true, isEmail: true },
},
},
} satisfies FormConfig;
// Type-safe validation result
// TypeScript automatically infers the correct types from formConfig
const result = await validateForm(request, formConfig);
if (result.hasErrors) {
// result.errors is properly typed with field names
console.log(result.errors.name); // string | undefined
console.log(result.errors.email); // string | undefined
}
// result.values is properly typed based on field types
const name: string = result.values.name; // TypeScript knows this is a string
const email: string = result.values.email; // TypeScript knows this is a stringSecurity Considerations
Server-Side Validation
All validation is performed server-side. Client-side HTML5 validation attributes are added for better UX, but should not be relied upon for security.
File Upload Security
- MIME Type Validation: Files are validated by actual MIME type, not just extension
- Size Limits: Enforce maximum file sizes to prevent DoS attacks
- File Storage: Always validate and sanitize filenames before storage
- Virus Scanning: Implement virus scanning for uploaded files in production
XSS Prevention
- All form inputs are properly escaped when rendered
- The
denyHtmlvalidation rule prevents HTML injection - Use the
patternrule to restrict input to safe characters
CSRF Protection
Implement CSRF protection at the framework level:
// Next.js example with csrf-token
import { headers } from 'next/headers';
export async function submitForm(prevState: any, formData: FormData) {
const headersList = headers();
const csrfToken = headersList.get('x-csrf-token');
// Validate CSRF token
const result = await validateForm(request, formConfig);
// ...
}Performance
Bundle Sizes
- Core library: 7.69 KB (brotli)
- Server validation: 367 B (brotli)
- React Router adapter: 18.8 KB (brotli)
- Next.js adapter: 7.3 KB (brotli)
- TanStack Start adapter: 7.36 KB (brotli)
Optimization
All components are memoized with React.memo for optimal performance. The library uses:
- Tree-shaking for unused code elimination
- Code splitting between client and server modules
- Minimal dependencies (only
lucide-reactfor icons)
Accessibility
performa is built with accessibility in mind:
- Proper ARIA attributes on all form elements
- Keyboard navigation support
- Screen reader friendly error messages
- Focus management
- Semantic HTML structure
- High contrast mode support
Browser Support
- Chrome/Edge (latest)
- Firefox (latest)
- Safari (latest)
- iOS Safari (latest)
- Chrome Android (latest)
Contributing
Contributions are welcome! Please see our contributing guidelines.
License
MIT License - see LICENSE file for details
Support
- GitHub Issues: https://github.com/matttehat/performa/issues
- Documentation: https://github.com/matttehat/performa#readme
