npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

performa

v1.0.6

Published

Type-safe React forms with server-side validation and framework adapters

Downloads

19

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 performa

Framework-Specific Installation

For Next.js (App Router):

npm install performa react-dom

For React Router:

npm install performa react-router

For TanStack Start:

npm install performa @tanstack/start

Setup

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 input
  • email - Email input with validation
  • password - Password input with masking
  • url - URL input
  • tel - Telephone number input
  • number - Numeric input
  • date - Date picker
  • time - 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 checkbox
  • toggle - Toggle switch

File Inputs

  • file - File upload with validation and preview

Special Types

  • datetime - Combined date and time picker
  • hidden - Hidden input field
  • none - 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 baseUrl prop is provided to the form component
  • The file is an image type
  • A defaultValue exists (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 FormConfig

Multi-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 string

Security 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

  1. MIME Type Validation: Files are validated by actual MIME type, not just extension
  2. Size Limits: Enforce maximum file sizes to prevent DoS attacks
  3. File Storage: Always validate and sanitize filenames before storage
  4. Virus Scanning: Implement virus scanning for uploaded files in production

XSS Prevention

  • All form inputs are properly escaped when rendered
  • The denyHtml validation rule prevents HTML injection
  • Use the pattern rule 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-react for 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