rilaykit

Quick Start

Get up and running with RilayKit in 5 minutes. Perfect for developers who want to jump straight into code.

Quick Start

A working, validated form in 5 minutes.

1. Install

pnpm add rilaykit
npm install rilaykit
yarn add rilaykit

2. Create a Component

A renderer receives a ComponentRenderContext: your props, plus a field binding (value, onChange, onBlur, error, touched).

components/Input.tsx
import type { ComponentRenderContext } from 'rilaykit';

interface InputProps {
  label: string;
  type?: string;
  placeholder?: string;
}

export function Input({ id, props, field }: ComponentRenderContext<InputProps>) {
  return (
    <div className="mb-4">
      <label htmlFor={id} className="block text-sm font-medium mb-1">
        {props.label}
      </label>
      <input
        id={id}
        type={props.type ?? 'text'}
        value={(field?.value as string) ?? ''}
        onChange={(e) => field?.onChange(e.target.value)}
        onBlur={field?.onBlur}
        placeholder={props.placeholder}
        className="w-full p-2 border rounded"
      />
      {field?.error && (
        <p role="alert" className="text-red-500 text-sm mt-1">
          {field.error[0].message}
        </p>
      )}
    </div>
  );
}

3. Register Components

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

export const rilay = ril.create().component('input', { renderer: Input });

4. Build the Form

forms/login.ts
import { email, required } from 'rilaykit';
import { rilay } from '../lib/rilay';

export const loginForm = rilay.form('login')
  .add({
    id: 'email',
    type: 'input',
    props: { label: 'Email', type: 'email' },
    validation: { validate: [required(), email()] },
  })
  .add({
    id: 'password',
    type: 'input',
    props: { label: 'Password', type: 'password' },
    validation: { validate: [required()] },
  })
  .build();

By default fields validate on first blur, then re-validate on every keystroke. Tune with .setValidation({ mode, reValidateMode }).

5. Render It

Components and hooks import from rilaykit/react:

components/LoginForm.tsx
import { Form, FormField } from 'rilaykit/react';
import { loginForm } from '../forms/login';

export function LoginForm() {
  return (
    <Form of={loginForm} onSubmit={(data) => console.log('Login:', data)}>
      <FormField id="email" />
      <FormField id="password" />
      <button type="submit">Sign In</button>
    </Form>
  );
}

Done — mount <LoginForm /> anywhere.

Common Patterns

Multiple Input Types

export const rilay = ril.create()
  .component('input', { renderer: Input })
  .component('textarea', { renderer: TextareaInput })
  .component('select', { renderer: SelectInput });

Validation with Zod

Any Standard Schema library works directly — no adapter:

import { z } from 'zod';

const userForm = rilay.form('user')
  .add({
    id: 'email',
    type: 'input',
    validation: { validate: z.string().email() },
  });

Conditional Fields

import { when } from 'rilaykit';

const accountForm = rilay.form('account')
  .add({
    id: 'accountType',
    type: 'select',
    props: { options: [{ value: 'business', label: 'Business' }] },
  })
  .add({
    id: 'companyName',
    type: 'input',
    conditions: { visible: when('accountType').equals('business') },
  });

Cross-Field Validation

Form-level rules run on the same cadence as field validation. An issue whose path names a field attaches to that field; pathless issues land in the __form__ bucket, read via useFormErrors():

import { z } from 'zod';

const signupForm = rilay.form('signup')
  .add({ id: 'password', type: 'input', props: { label: 'Password', type: 'password' } })
  .add({ id: 'confirmPassword', type: 'input', props: { label: 'Confirm', type: 'password' } })
  .setValidation({
    validate: z
      .object({ password: z.string(), confirmPassword: z.string() })
      .refine((d) => d.password === d.confirmPassword, {
        message: 'Passwords do not match',
        path: ['confirmPassword'],
      }),
  })
  .build();

What's Next?

On this page