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 🙏

© 2025 – Pkg Stats / Ryan Hefner

suprform

v1.2.0

Published

A headless React form library for managing complex state, validation, and error handling

Readme

SuprForm 🚀

A headless, TypeScript-first React form library for managing complex state, validation, and conditional logic

npm version License: MIT TypeScript

FeaturesInstallationQuick StartDocumentationExamples


🎯 What is SuprForm?

SuprForm is a headless form library that gives you complete control over your form's appearance while handling all the complex logic under the hood. Built on top of react-hook-form, it provides:

  • Zero UI dependencies - Works with any design system (Material-UI, Ant Design, shadcn/ui, plain HTML)
  • 🎯 Composable components - Intuitive API with SuprForm.Control and SuprForm.ControlArray
  • 🔒 TypeScript-first - Full type inference for field names, values, and validation
  • 👁️ Conditional logic - Declarative field visibility and disability based on other fields
  • Powerful validation - Sync and async validation with helpful error messages
  • 🎛️ Imperative control - Access form methods via ref for programmatic manipulation
  • 📦 Lightweight - Only React as a peer dependency

📋 Table of Contents


✨ Key Features

🎨 Design System Agnostic

Use with any UI framework. SuprForm provides the logic, you control the design.

// Works with Material-UI
<SuprForm.Control name="email" rules={{ required: true }}>
  <TextField variant="outlined" />
</SuprForm.Control>

// Works with plain HTML
<SuprForm.Control name="email" rules={{ required: true }}>
  <input type="email" className="my-input" />
</SuprForm.Control>

// Works with shadcn/ui
<SuprForm.Control name="email">
  <Input type="email" />
</SuprForm.Control>

🎯 Composable Field Control

The SuprForm.Control component handles everything automatically:

  • ✅ Label rendering with proper htmlFor linking
  • ✅ Error message display (auto-styled but customizable)
  • ✅ Validation on blur and change
  • ✅ Field state management (value, touched, dirty)
  • ✅ Works with any input component

🔒 TypeScript-First Architecture

Full type inference for field names, values, and validation rules:

interface UserForm {
  email: string;
  age: number;
  isSubscribed: boolean;
}

<SuprForm<UserForm>
  onSubmit={(values) => {
    // ✅ values is fully typed as UserForm
    console.log(values.email.toLowerCase()); // Type-safe!
  }}
>
  <SuprForm.Control name='email' /> {/* ✅ Autocomplete works */}
  <SuprForm.Control name='age' /> {/* ✅ Type-checked */}
  <SuprForm.Control name='invalid' /> {/* ❌ TypeScript error! */}
</SuprForm>;

👁️ Conditional Field Visibility

Declaratively show/hide or enable/disable fields based on other field values:

<SuprForm.Control
  name='promoCode'
  visibility={{
    operator: 'AND',
    conditions: [
      { name: 'hasDiscount', operator: 'EQUALS', value: true },
      { name: 'orderTotal', operator: 'GREATER_THAN', value: 50 },
    ],
  }}
>
  <input type='text' />
</SuprForm.Control>

Supported operators: EQUALS, NOT_EQUALS, GREATER_THAN, LESS_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN_OR_EQUAL, STARTS_WITH, ENDS_WITH, INCLUDES, NOT_INCLUDES

✅ Powerful Validation

Sync and async validation with helpful error messages:

<SuprForm.Control
  name='username'
  rules={{
    required: 'Username is required',
    minLength: { value: 3, message: 'Min 3 characters' },
    validate: async (value) => {
      const available = await checkUsername(value);
      return available || 'Username already taken';
    },
  }}
>
  <input />
</SuprForm.Control>

🎛️ Imperative Form Control

Access form methods via ref for programmatic manipulation:

const formRef = useRef<SuprFormRef<FormData>>();

<SuprForm ref={formRef} onSubmit={handleSubmit}>
  {/* ... */}
</SuprForm>;

// Later:
formRef.current?.setValue('email', '[email protected]');
formRef.current?.trigger('email'); // Validate
formRef.current?.reset(); // Reset form

🔄 Dynamic Field Arrays

Manage repeating field groups with SuprForm.ControlArray:

<SuprForm.ControlArray name='hobbies' ref={arrayRef}>
  <SuprForm.Control name='name' label='Hobby Name'>
    <input />
  </SuprForm.Control>
  <SuprForm.Control name='years' label='Years'>
    <input type='number' />
  </SuprForm.Control>
</SuprForm.ControlArray>;

// Add/remove items:
arrayRef.current?.append({ name: '', years: 0 });
arrayRef.current?.remove(0);

📦 Installation

npm install suprform

Peer dependencies: React 18 or 19

npm install react react-dom

⚡ Quick Start

Here's a complete login form in under 30 lines:

import SuprForm from 'suprform';

function LoginForm() {
  const handleSubmit = (values: { email: string; password: string }) => {
    console.log('Form submitted:', values);
    // Call your API here
  };

  return (
    <SuprForm onSubmit={handleSubmit}>
      <SuprForm.Control
        name='email'
        label='Email Address'
        rules={{
          required: 'Email is required',
          pattern: {
            value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
            message: 'Invalid email address',
          },
        }}
      >
        <input type='email' placeholder='[email protected]' />
      </SuprForm.Control>

      <SuprForm.Control
        name='password'
        label='Password'
        rules={{
          required: 'Password is required',
          minLength: { value: 8, message: 'Min 8 characters' },
        }}
      >
        <input type='password' placeholder='••••••••' />
      </SuprForm.Control>

      <button type='submit'>Login</button>
    </SuprForm>
  );
}

That's it! SuprForm automatically handles:

  • ✅ Form submission
  • ✅ Validation on blur and submit
  • ✅ Error message display
  • ✅ Field state management
  • ✅ Label rendering

💡 Core Concepts

1. The <SuprForm> Component

The root component that wraps your form:

<SuprForm
  onSubmit={(values) => console.log(values)}
  onError={(errors) => console.error(errors)}
  formOptions={{ mode: 'onBlur' }} // react-hook-form options
  showAsterisk={true} // Show * on required fields
>
  {/* Your form fields */}
</SuprForm>

2. The <SuprForm.Control> Component

Wraps individual input fields:

<SuprForm.Control
  name="fieldName"           // Required: field identifier
  label="Field Label"        // Optional: displays above input
  rules={{                   // Optional: validation rules
    required: 'Required!',
    minLength: { value: 3, message: 'Too short' }
  }}
  visibility={{              // Optional: conditional rendering
    operator: 'AND',
    conditions: [...]
  }}
>
  <input type="text" />      {/* Your input component */}
</SuprForm.Control>

3. The <SuprForm.ControlArray> Component

Manages dynamic lists of fields:

const arrayRef = useRef<FormControlArrayRef>();

<SuprForm.ControlArray name="items" ref={arrayRef}>
  <SuprForm.Control name="title">
    <input />
  </SuprForm.Control>
  <SuprForm.Control name="quantity">
    <input type="number" />
  </SuprForm.Control>
</SuprForm.ControlArray>

<button onClick={() => arrayRef.current?.append({ title: '', quantity: 0 })}>
  Add Item
</button>

🎨 Usage Examples

1. Basic Form

import SuprForm from 'suprform';

interface ContactForm {
  name: string;
  email: string;
  message: string;
}

function ContactForm() {
  return (
    <SuprForm<ContactForm>
      onSubmit={(values) => {
        console.log('Submitting:', values);
      }}
    >
      <SuprForm.Control name='name' label='Your Name' rules={{ required: 'Name is required' }}>
        <input type='text' />
      </SuprForm.Control>

      <SuprForm.Control
        name='email'
        label='Email'
        rules={{
          required: 'Email is required',
          pattern: {
            value: /^\S+@\S+$/,
            message: 'Invalid email',
          },
        }}
      >
        <input type='email' />
      </SuprForm.Control>

      <SuprForm.Control
        name='message'
        label='Message'
        rules={{ minLength: { value: 10, message: 'Min 10 characters' } }}
      >
        <textarea rows={4} />
      </SuprForm.Control>

      <button type='submit'>Send Message</button>
    </SuprForm>
  );
}

2. With UI Libraries (Material-UI, AntD, shadcn/ui)

SuprForm works seamlessly with any component library:

Material-UI:

import { TextField, Button, Checkbox, FormControlLabel } from '@mui/material';
import SuprForm from 'suprform';

function MUIForm() {
  return (
    <SuprForm onSubmit={(values) => console.log(values)}>
      <SuprForm.Control name='username' label='Username' rules={{ required: 'Required' }}>
        <TextField variant='outlined' fullWidth />
      </SuprForm.Control>

      <SuprForm.Control name='subscribe'>
        <FormControlLabel control={<Checkbox />} label='Subscribe to newsletter' />
      </SuprForm.Control>

      <Button variant='contained' type='submit'>
        Submit
      </Button>
    </SuprForm>
  );
}

shadcn/ui:

import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import SuprForm from 'suprform';

function ShadcnForm() {
  return (
    <SuprForm onSubmit={(values) => console.log(values)}>
      <SuprForm.Control name='email' label='Email' rules={{ required: true }}>
        <Input type='email' placeholder='Email' />
      </SuprForm.Control>

      <Button type='submit'>Submit</Button>
    </SuprForm>
  );
}

3. Conditional Field Visibility

Show/hide fields based on other field values:

interface PaymentForm {
  paymentMethod: 'card' | 'paypal' | 'cash';
  cardNumber?: string;
  paypalEmail?: string;
}

function PaymentForm() {
  return (
    <SuprForm<PaymentForm> onSubmit={(values) => console.log(values)}>
      <SuprForm.Control name='paymentMethod' label='Payment Method'>
        <select>
          <option value='card'>Credit Card</option>
          <option value='paypal'>PayPal</option>
          <option value='cash'>Cash</option>
        </select>
      </SuprForm.Control>

      {/* Only show when card is selected */}
      <SuprForm.Control
        name='cardNumber'
        label='Card Number'
        visibility={{
          operator: 'AND',
          conditions: [{ name: 'paymentMethod', operator: 'EQUALS', value: 'card' }],
        }}
        rules={{ required: 'Card number required' }}
      >
        <input type='text' placeholder='1234 5678 9012 3456' />
      </SuprForm.Control>

      {/* Only show when PayPal is selected */}
      <SuprForm.Control
        name='paypalEmail'
        label='PayPal Email'
        visibility={{
          operator: 'AND',
          conditions: [{ name: 'paymentMethod', operator: 'EQUALS', value: 'paypal' }],
        }}
        rules={{ required: 'PayPal email required' }}
      >
        <input type='email' />
      </SuprForm.Control>

      <button type='submit'>Submit Payment</button>
    </SuprForm>
  );
}

Complex conditions with OR:

<SuprForm.Control
  name='discountCode'
  visibility={{
    operator: 'OR', // Show if ANY condition is true
    conditions: [
      { name: 'isVip', operator: 'EQUALS', value: true },
      { name: 'orderTotal', operator: 'GREATER_THAN', value: 100 },
    ],
  }}
>
  <input placeholder='Enter discount code' />
</SuprForm.Control>

4. Dynamic Field Arrays

Manage repeating field groups:

import { useRef } from 'react';
import SuprForm, { FormControlArrayRef } from 'suprform';

interface Education {
  school: string;
  degree: string;
  year: number;
}

interface ResumeForm {
  name: string;
  education: Education[];
}

function ResumeForm() {
  const educationRef = useRef<FormControlArrayRef<ResumeForm, 'education'>>();

  return (
    <SuprForm<ResumeForm>
      onSubmit={(values) => console.log(values)}
      formOptions={{
        defaultValues: {
          name: '',
          education: [{ school: '', degree: '', year: 2024 }]
        }
      }}
    >
      <SuprForm.Control name="name" label="Full Name">
        <input type="text" />
      </SuprForm.Control>

      <h3>Education</h3>
      <SuprForm.ControlArray name="education" ref={educationRef}>
        <div className="education-item">
          <SuprForm.Control name="school" label="School">
            <input type="text" />
          </SuprForm.Control>

          <SuprForm.Control name="degree" label="Degree">
            <input type="text" />
          </SuprForm.Control>

          <SuprForm.Control name="year" label="Graduation Year">
            <input type="number" />
          </SuprForm.Control>

          <button
            type="button"
            onClick={() => {
              const index = /* get current index */;
              educationRef.current?.remove(index);
            }}
          >
            Remove
          </button>
        </div>
      </SuprForm.ControlArray>

      <button
        type="button"
        onClick={() => {
          educationRef.current?.append({ school: '', degree: '', year: 2024 });
        }}
      >
        + Add Education
      </button>

      <button type="submit">Save Resume</button>
    </SuprForm>
  );
}

Field Array Methods (via ref):

  • append(value) - Add item to end
  • prepend(value) - Add item to beginning
  • insert(index, value) - Insert at position
  • remove(index) - Remove item
  • move(from, to) - Reorder items
  • update(index, value) - Update item
  • replace(values) - Replace all items

5. Async Validation

Validate against your backend:

function SignupForm() {
  const checkUsernameAvailability = async (username: string) => {
    const response = await fetch(`/api/check-username?name=${username}`);
    const data = await response.json();
    return data.available;
  };

  return (
    <SuprForm onSubmit={(values) => console.log(values)}>
      <SuprForm.Control
        name='username'
        label='Username'
        rules={{
          required: 'Username is required',
          minLength: { value: 3, message: 'Min 3 characters' },
          validate: async (value) => {
            const available = await checkUsernameAvailability(value);
            return available || 'Username already taken';
          },
        }}
      >
        <input type='text' />
      </SuprForm.Control>

      <SuprForm.Control
        name='email'
        label='Email'
        rules={{
          required: true,
          validate: async (value) => {
            const response = await fetch(`/api/check-email?email=${value}`);
            const data = await response.json();
            return data.available || 'Email already registered';
          },
        }}
      >
        <input type='email' />
      </SuprForm.Control>

      <button type='submit'>Sign Up</button>
    </SuprForm>
  );
}

6. Programmatic Form Control

Access form methods via ref:

import { useRef } from 'react';
import SuprForm, { SuprFormRef } from 'suprform';

interface UserForm {
  email: string;
  password: string;
  rememberMe: boolean;
}

function LoginFormWithRef() {
  const formRef = useRef<SuprFormRef<UserForm>>();

  const prefillForm = () => {
    formRef.current?.setValue('email', '[email protected]');
    formRef.current?.setValue('password', 'password123');
    formRef.current?.setValue('rememberMe', true);
  };

  const clearForm = () => {
    formRef.current?.reset();
  };

  const validateEmail = () => {
    formRef.current?.trigger('email');
  };

  const watchEmail = () => {
    const email = formRef.current?.getValues('email');
    console.log('Current email:', email);
  };

  return (
    <div>
      <div className='actions'>
        <button onClick={prefillForm}>Prefill Demo</button>
        <button onClick={clearForm}>Clear</button>
        <button onClick={validateEmail}>Validate Email</button>
        <button onClick={watchEmail}>Get Email Value</button>
      </div>

      <SuprForm<UserForm> ref={formRef} onSubmit={(values) => console.log(values)}>
        <SuprForm.Control name='email' label='Email'>
          <input type='email' />
        </SuprForm.Control>

        <SuprForm.Control name='password' label='Password'>
          <input type='password' />
        </SuprForm.Control>

        <SuprForm.Control name='rememberMe'>
          <label>
            <input type='checkbox' /> Remember me
          </label>
        </SuprForm.Control>

        <button type='submit'>Login</button>
      </SuprForm>
    </div>
  );
}

📖 API Reference

<SuprForm>

Root form component.

Props

| Prop | Type | Description | | -------------- | --------------------- | ------------------------------------------------- | | children | ReactNode | Form fields and elements | | onSubmit | (values: T) => void | Called when form is valid and submitted | | onError | (errors) => void | Called when form submission fails validation | | formOptions | UseFormProps | Options passed to react-hook-form's useForm() | | className | string | CSS class for <form> element | | style | CSSProperties | Inline styles for <form> | | showAsterisk | boolean | Show red asterisk on required field labels | | ref | Ref<SuprFormRef> | Access form methods imperatively | | onChange | (values: T) => void | Called whenever any field value changes |

Example

<SuprForm
  onSubmit={(values) => console.log('Submit:', values)}
  onError={(errors) => console.error('Errors:', errors)}
  formOptions={{
    mode: 'onBlur',          // When to validate
    defaultValues: {...},     // Initial values
  }}
  showAsterisk={true}
  onChange={(values) => console.log('Changed:', values)}
>
  {/* fields */}
</SuprForm>

<SuprForm.Control>

Field wrapper component.

Props

| Prop | Type | Description | | ------------------ | ----------------------- | --------------------------------------- | | name ⚠️ | string | Required. Field name (type-checked) | | children ⚠️ | ReactElement | Required. Your input component | | rules | RegisterOptions | Validation rules (see below) | | label | string | Label text rendered above input | | className | string | CSS class for wrapper <div> | | id | string | HTML id (auto-generated if omitted) | | disabled | boolean \| Visibility | Disable field (can be conditional) | | visibility | boolean \| Visibility | Show/hide field (can be conditional) | | shouldUnregister | boolean | Unregister field when unmounted |

Example

<SuprForm.Control
  name='email'
  label='Email Address'
  rules={{
    required: 'Email is required',
    pattern: { value: /^\S+@\S+$/, message: 'Invalid email' },
  }}
  className='mb-4'
>
  <input type='email' />
</SuprForm.Control>

<SuprForm.ControlArray>

Dynamic field array component.

Props

| Prop | Type | Description | | ------------- | ---------------------------- | -------------------------------- | | name ⚠️ | string | Required. Array field name | | children ⚠️ | ReactNode | Required. Field template | | ref | Ref<FormControlArrayRef> | Access array methods | | rules | RegisterOptions | Validation rules for array | | className | string | CSS class for wrapper | | visibility | Record<string, Visibility> | Conditional visibility per field | | disabled | Record<string, Visibility> | Conditional disability per field |

Example

<SuprForm.ControlArray name='hobbies' ref={arrayRef}>
  <SuprForm.Control name='name'>
    <input />
  </SuprForm.Control>
  <SuprForm.Control name='years'>
    <input type='number' />
  </SuprForm.Control>
</SuprForm.ControlArray>

Validation Rules

SuprForm uses react-hook-form validation rules:

| Rule | Type | Description | | ----------- | ----------------------------------------- | -------------------------- | | required | string \| boolean | Field is required | | min | { value: number, message: string } | Minimum numeric value | | max | { value: number, message: string } | Maximum numeric value | | minLength | { value: number, message: string } | Minimum string length | | maxLength | { value: number, message: string } | Maximum string length | | pattern | { value: RegExp, message: string } | Regex pattern match | | validate | (value) => boolean \| string \| Promise | Custom validation function |

Examples

// Simple required
rules={{ required: true }}

// Required with message
rules={{ required: 'This field is required' }}

// Multiple rules
rules={{
  required: 'Email is required',
  pattern: {
    value: /^\S+@\S+$/,
    message: 'Invalid email format'
  }
}}

// Custom validation
rules={{
  validate: (value) => {
    if (value.length < 8) return 'Too short';
    if (!/[A-Z]/.test(value)) return 'Need uppercase';
    return true; // Valid
  }
}}

// Async validation
rules={{
  validate: async (value) => {
    const response = await fetch(`/api/check/${value}`);
    const data = await response.json();
    return data.valid || 'Invalid value';
  }
}}

Conditional Visibility

The visibility prop accepts:

{
  operator: 'AND' | 'OR',
  conditions: [
    {
      name: 'fieldName',        // Field to check
      operator: '...',          // Comparison operator
      value: any                // Value to compare
    }
  ]
}

Operators

String operators: EQUALS, NOT_EQUALS, STARTS_WITH, ENDS_WITH, INCLUDES, NOT_INCLUDES

Number operators: EQUALS, NOT_EQUALS, GREATER_THAN, LESS_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN_OR_EQUAL

Boolean operators: EQUALS, NOT_EQUALS

Examples

// Show when checkbox is checked
visibility={{
  operator: 'AND',
  conditions: [
    { name: 'agreeToTerms', operator: 'EQUALS', value: true }
  ]
}}

// Show when amount > 100 OR user is VIP
visibility={{
  operator: 'OR',
  conditions: [
    { name: 'amount', operator: 'GREATER_THAN', value: 100 },
    { name: 'isVip', operator: 'EQUALS', value: true }
  ]
}}

// Show when username starts with 'admin'
visibility={{
  operator: 'AND',
  conditions: [
    { name: 'username', operator: 'STARTS_WITH', value: 'admin' }
  ]
}}

Form Ref Methods

When you pass a ref to <SuprForm>, you get:

interface SuprFormRef<T> {
  setValue: (name: keyof T, value: any) => void;
  setError: (name: keyof T, error: ErrorOption) => void;
  clearErrors: (name?: keyof T | keyof T[]) => void;
  getValues: (name?: keyof T) => any;
  reset: (values?: Partial<T>) => void;
  setFocus: (name: keyof T) => void;
  resetField: (name: keyof T) => void;
  trigger: (name?: keyof T | keyof T[]) => Promise<boolean>;
  unregister: (name: keyof T) => void;
  watch: (name?: keyof T) => any;
  handleSubmit: (
    onValid: (data: T) => void,
    onInvalid?: (errors: any) => void
  ) => (e?: Event) => void;
}

Examples

const formRef = useRef<SuprFormRef<FormData>>();

// Set field value
formRef.current?.setValue('email', '[email protected]');

// Set field error
formRef.current?.setError('email', {
  type: 'manual',
  message: 'This email is already taken',
});

// Clear all errors
formRef.current?.clearErrors();

// Get current values
const values = formRef.current?.getValues();
const email = formRef.current?.getValues('email');

// Reset form
formRef.current?.reset();
formRef.current?.reset({ email: '[email protected]' });

// Focus field
formRef.current?.setFocus('email');

// Trigger validation
const isValid = await formRef.current?.trigger();
await formRef.current?.trigger('email'); // Validate single field

// Watch field value
const watchedEmail = formRef.current?.watch('email');

Field Array Ref Methods

When you pass a ref to <SuprForm.ControlArray>:

interface FormControlArrayRef<T, TArrayName> {
  fields: Array<Record<'id', string> & FieldArrayItem>;
  append: (value: FieldArrayItem) => void;
  prepend: (value: FieldArrayItem) => void;
  insert: (index: number, value: FieldArrayItem) => void;
  remove: (index?: number | number[]) => void;
  move: (from: number, to: number) => void;
  update: (index: number, value: FieldArrayItem) => void;
  replace: (values: FieldArrayItem[]) => void;
}

Examples

const arrayRef = useRef<FormControlArrayRef>();

// Add item to end
arrayRef.current?.append({ name: '', value: '' });

// Add item to beginning
arrayRef.current?.prepend({ name: '', value: '' });

// Insert at position
arrayRef.current?.insert(1, { name: 'New', value: 'Item' });

// Remove item(s)
arrayRef.current?.remove(0);
arrayRef.current?.remove([0, 2, 4]);

// Move item
arrayRef.current?.move(0, 3);

// Update item
arrayRef.current?.update(1, { name: 'Updated', value: 'Value' });

// Replace all items
arrayRef.current?.replace([
  { name: 'Item 1', value: '1' },
  { name: 'Item 2', value: '2' },
]);

// Access current fields
console.log(arrayRef.current?.fields);

🔷 TypeScript Guide

SuprForm is built with TypeScript-first design.

Type Your Form Data

interface SignupForm {
  username: string;
  email: string;
  age: number;
  agreeToTerms: boolean;
  hobbies: Array<{ name: string; years: number }>;
}

<SuprForm<SignupForm>
  onSubmit={(values) => {
    // values is typed as SignupForm
    console.log(values.username); // ✅ Type-safe
  }}
>
  {/* Field names are type-checked */}
  <SuprForm.Control name='username'>
    {' '}
    {/* ✅ Valid */}
    <input />
  </SuprForm.Control>

  <SuprForm.Control name='invalidField'>
    {' '}
    {/* ❌ TypeScript error */}
    <input />
  </SuprForm.Control>
</SuprForm>;

Type Form Refs

const formRef = useRef<SuprFormRef<SignupForm>>();

// All methods are type-safe
formRef.current?.setValue('username', 'john'); // ✅
formRef.current?.setValue('invalid', 'x'); // ❌ Error

Type Field Array Refs

const arrayRef = useRef<FormControlArrayRef<SignupForm, 'hobbies'>>();

// Methods know the shape of array items
arrayRef.current?.append({ name: '', years: 0 }); // ✅
arrayRef.current?.append({ invalid: 'x' }); // ❌ Error

Infer Types from Default Values

const defaultValues = {
  name: '',
  age: 0,
  email: '',
};

<SuprForm
  formOptions={{ defaultValues }}
  onSubmit={(values) => {
    // values is inferred as { name: string; age: number; email: string }
  }}
>
  {/* ... */}
</SuprForm>;

🎨 Styling

SuprForm is design system agnostic. Style it however you want:

CSS Classes

SuprForm adds these classes for customization:

.controlled-field {
  /* Wrapper for each field */
}

.controlled-field-label {
  /* Label element */
}

.controlled-field-error {
  /* Error message */
  /* Default: color: red; font-size: 13px; */
}

Tailwind CSS

<SuprForm.Control name='email' className='mb-6'>
  <input className='w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500' />
</SuprForm.Control>

CSS-in-JS (styled-components, emotion)

import styled from 'styled-components';

const StyledField = styled.div`
  .controlled-field {
    margin-bottom: 1rem;
  }

  .controlled-field-label {
    font-weight: 600;
    color: #333;
    margin-bottom: 0.5rem;
  }

  .controlled-field-error {
    color: #dc2626;
    font-size: 0.875rem;
    margin-top: 0.25rem;
  }
`;

<SuprForm.Control name='email' className={StyledField}>
  <input />
</SuprForm.Control>;

Global Styles

/* styles.css */
.controlled-field {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 16px;
}

.controlled-field-label {
  font-weight: 500;
  color: #1a1a1a;
  font-size: 14px;
}

.controlled-field-error {
  color: #ef4444;
  font-size: 13px;
  margin-top: 4px;
}

Override Component Styles

<SuprForm.Control name='email' className='custom-field'>
  <input
    style={{
      padding: '12px',
      border: '2px solid #e5e7eb',
      borderRadius: '8px',
    }}
  />
</SuprForm.Control>

❓ Frequently Asked Questions

Can I use SuprForm with Material-UI / Ant Design / shadcn/ui?

Yes! SuprForm is design system agnostic. Just pass your component library's input components as children:

// Material-UI
<SuprForm.Control name="email">
  <TextField variant="outlined" />
</SuprForm.Control>

// Ant Design
<SuprForm.Control name="username">
  <Input placeholder="Username" />
</SuprForm.Control>

// shadcn/ui
<SuprForm.Control name="password">
  <Input type="password" />
</SuprForm.Control>

How do I set default values?

Use the formOptions.defaultValues prop:

<SuprForm
  formOptions={{
    defaultValues: {
      email: '[email protected]',
      age: 25,
      agreeToTerms: false,
    },
  }}
  onSubmit={handleSubmit}
>
  {/* fields */}
</SuprForm>

How do I reset the form after submission?

Use a ref and call reset():

const formRef = useRef<SuprFormRef>();

<SuprForm
  ref={formRef}
  onSubmit={(values) => {
    console.log(values);
    formRef.current?.reset(); // Reset after submit
  }}
>
  {/* fields */}
</SuprForm>;

How do I validate on change instead of on blur?

Pass mode in formOptions:

<SuprForm formOptions={{ mode: 'onChange' }} onSubmit={handleSubmit}>
  {/* fields */}
</SuprForm>

Options: onBlur (default), onChange, onSubmit, onTouched, all

Can I use custom validation functions?

Yes! Use the validate rule:

<SuprForm.Control
  name='password'
  rules={{
    validate: (value) => {
      if (value.length < 8) return 'Min 8 characters';
      if (!/[A-Z]/.test(value)) return 'Need uppercase letter';
      if (!/[0-9]/.test(value)) return 'Need number';
      return true;
    },
  }}
>
  <input type='password' />
</SuprForm.Control>

How do I access form values outside the form?

Use a ref and getValues():

const formRef = useRef<SuprFormRef>();

<SuprForm ref={formRef} onSubmit={handleSubmit}>
  {/* fields */}
</SuprForm>

<button onClick={() => {
  const values = formRef.current?.getValues();
  console.log(values);
}}>
  Log Values
</button>

Can I watch field values in real-time?

Yes! Use the onChange prop:

<SuprForm
  onChange={(values) => {
    console.log('Form changed:', values);
  }}
  onSubmit={handleSubmit}
>
  {/* fields */}
</SuprForm>

Or use the ref's watch() method:

const formRef = useRef<SuprFormRef>();

useEffect(() => {
  const email = formRef.current?.watch('email');
  console.log('Email:', email);
}, []);

How do I handle form submission errors?

Use the onError prop:

<SuprForm
  onSubmit={(values) => console.log('Success:', values)}
  onError={(errors) => {
    console.error('Validation errors:', errors);
    // Show notification, etc.
  }}
>
  {/* fields */}
</SuprForm>

Can I show a loading state during async validation?

Yes! Track the form's isValidating state:

function MyForm() {
  const [isValidating, setIsValidating] = useState(false);

  return (
    <SuprForm onSubmit={handleSubmit}>
      <SuprForm.Control
        name='username'
        rules={{
          validate: async (value) => {
            setIsValidating(true);
            const available = await checkUsername(value);
            setIsValidating(false);
            return available || 'Username taken';
          },
        }}
      >
        <input disabled={isValidating} />
      </SuprForm.Control>

      {isValidating && <span>Checking...</span>}
    </SuprForm>
  );
}

How do I conditionally disable fields?

Use the disabled prop with a visibility object:

<SuprForm.Control
  name='creditCard'
  disabled={{
    operator: 'AND',
    conditions: [{ name: 'paymentMethod', operator: 'NOT_EQUALS', value: 'card' }],
  }}
>
  <input />
</SuprForm.Control>

Or use a simple boolean:

<SuprForm.Control name='email' disabled={true}>
  <input />
</SuprForm.Control>

🚀 Advanced Topics

Nested Objects

SuprForm supports nested field names using dot notation:

interface UserForm {
  name: string;
  address: {
    street: string;
    city: string;
    zip: string;
  };
}

<SuprForm<UserForm> onSubmit={handleSubmit}>
  <SuprForm.Control name='name'>
    <input />
  </SuprForm.Control>

  <SuprForm.Control name='address.street'>
    <input />
  </SuprForm.Control>

  <SuprForm.Control name='address.city'>
    <input />
  </SuprForm.Control>

  <SuprForm.Control name='address.zip'>
    <input />
  </SuprForm.Control>
</SuprForm>;

Custom Error Messages

Customize error display:

import { useFormContext } from 'react-hook-form';

function CustomFormControl({ name, children, label, rules }) {
  const {
    control,
    formState: { errors },
  } = useFormContext();
  const error = errors[name];

  return (
    <div>
      <label>{label}</label>
      {children}
      {error && <div className='custom-error'>⚠️ {error.message}</div>}
    </div>
  );
}

Server-Side Validation

Handle server errors:

const formRef = useRef<SuprFormRef>();

const handleSubmit = async (values) => {
  try {
    await api.submitForm(values);
  } catch (error) {
    // Set server errors on fields
    if (error.field === 'email') {
      formRef.current?.setError('email', {
        type: 'server',
        message: error.message,
      });
    }
  }
};

<SuprForm ref={formRef} onSubmit={handleSubmit}>
  {/* fields */}
</SuprForm>;

Dependent Field Validation

Validate one field based on another:

<SuprForm.Control
  name="password"
  rules={{ required: 'Password required' }}
>
  <input type="password" />
</SuprForm.Control>

<SuprForm.Control
  name="confirmPassword"
  rules={{
    required: 'Confirm password',
    validate: (value, formValues) => {
      return value === formValues.password || 'Passwords must match';
    }
  }}
>
  <input type="password" />
</SuprForm.Control>

🤝 Contributing

Contributions are welcome! Here's how to get started:

Development Setup

# Clone the repository
git clone https://github.com/Albinbritto/suprform.git
cd suprform

# Install dependencies
npm install

# Run tests
npm test

# Run tests in watch mode
npm run test:watch

# Build the library
npm run build

Project Structure

suprform/
├── src/
│   ├── components/
│   │   ├── SuprForm.tsx          # Root form component
│   │   ├── FormControl.tsx       # Field wrapper
│   │   ├── FormControlArray.tsx  # Dynamic arrays
│   │   ├── ConditionChecker.tsx  # Visibility logic
│   │   └── DisabilityChecker.tsx # Disability logic
│   ├── context/
│   │   └── SuprFormContext.tsx   # Form context
│   ├── type.ts                   # TypeScript types
│   ├── util.ts                   # Helper functions
│   └── index.ts                  # Public exports
├── dist/                         # Build output
├── package.json
└── README.md

Running Storybook

npm run storybook

Publishing

# Login to npm
npm login

# Build and publish
npm run build
npm version patch  # or minor/major
npm publish --access public

Submitting Pull Requests

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/your-feature
  3. Make your changes
  4. Write/update tests
  5. Run tests: npm test
  6. Commit: git commit -m "Add your feature"
  7. Push: git push origin feature/your-feature
  8. Open a Pull Request

📄 License

MIT © Albin Britto


🔗 Links


💖 Support

If you find SuprForm helpful, please:

  • ⭐ Star the repository
  • 🐛 Report bugs
  • 💡 Suggest features
  • 📖 Improve documentation
  • 🤝 Contribute code

Made with ❤️ by Albin Britto