rilaykit
Getting Started

Your First Form

Build a complete contact form in under 10 minutes with validation, conditional logic, and type safety.

Build a contact form with validation, a conditionally visible field, and full type safety.

1. Create a Component Renderer

RilayKit is headless: it handles the logic, you provide the UI. A renderer is a function that receives a ComponentRenderContext — the field's id, your props, and a field binding (value, onChange, onBlur, error, touched) — and returns your markup.

lib/components.tsx
import type { ChangeEvent } from 'react';
import type { ComponentRenderContext } from 'rilaykit';

interface InputProps {
  label: string;
  type?: 'text' | 'email';
  placeholder?: string;
  required?: boolean;
  multiline?: boolean;
  rows?: number;
}

export function InputRenderer({ id, props, field }: ComponentRenderContext<InputProps>) {
  const error = field?.error?.[0];
  const shared = {
    id,
    value: (field?.value as string) ?? '',
    onChange: (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) =>
      field?.onChange(e.target.value),
    onBlur: field?.onBlur,
    placeholder: props.placeholder,
    className: error ? 'input input-error' : 'input',
  };

  return (
    <div className="space-y-1">
      <label htmlFor={id}>
        {props.label}
        {props.required && <span aria-hidden> *</span>}
      </label>
      {props.multiline ? (
        <textarea {...shared} rows={props.rows ?? 3} />
      ) : (
        <input {...shared} type={props.type ?? 'text'} />
      )}
      {error && <p role="alert">{error.message}</p>}
    </div>
  );
}

Register it on a ril instance with .component():

lib/rilay.ts
import { ril } from 'rilaykit';
import { InputRenderer } from './components';

export const rilay = ril.create().component('input', {
  name: 'Input Field',
  renderer: InputRenderer,
  defaultProps: { label: 'Input Field', type: 'text' },
});

2. Build the Form Configuration

Each .add() call declares a field: its component type, props for your renderer, validation.validate (an array of Standard Schema validators), and optional conditions.

lib/contactForm.ts
import { email, minLength, required, when } from 'rilaykit';
import { rilay } from './rilay';

export const contactForm = rilay
  .form('contact-us')
  .add({
    id: 'name',
    type: 'input',
    props: { label: 'Full Name', required: true },
    validation: {
      validate: [required('Name is required'), minLength(2, 'At least 2 characters')],
    },
  })
  .add({
    id: 'email',
    type: 'input',
    props: { label: 'Email Address', type: 'email', required: true },
    validation: {
      validate: [required('Email is required'), email()],
    },
  })
  .add({
    id: 'message',
    type: 'input',
    props: { label: 'Your Message', required: true, multiline: true, rows: 4 },
    validation: {
      validate: [required('Message is required'), minLength(10, 'At least 10 characters')],
    },
    conditions: {
      visible: when('email').exists(), // only shown once email has a value
    },
  })
  .build();

By default a field first validates on blur (mode: 'onTouched'), then errors re-check on every keystroke (reValidateMode: 'onChange'). Change this form-wide with .setValidation({ mode, reValidateMode }).

3. Render the Form

Components and hooks come from rilaykit/react. Pass the built config (or the builder itself) to <Form of={...}>.

components/ContactForm.tsx
import { Form, FormField, FormSubmit } from 'rilaykit/react';
import { contactForm } from '@/lib/contactForm';

export function ContactForm() {
  return (
    <Form
      of={contactForm}
      onSubmit={(data) => console.log('Form data:', data)}
      className="space-y-4"
    >
      <FormField id="name" />
      <FormField id="email" />
      <FormField id="message" />
      <FormSubmit className="btn">Send Message</FormSubmit>
    </Form>
  );
}

onSubmit only fires when every visible field passes validation. FormSubmit renders a submit button that disables while submitting.

Next Steps

On this page