Examples Gallery
Complete examples showcasing RilayKit's capabilities with different UI libraries and use cases.
Examples Gallery
A collection of production-ready examples demonstrating RilayKit's flexibility across UI libraries, frameworks, and use cases. Each example is complete and ready to copy-paste into your project.
For complete production scenarios (SaaS onboarding, KYC verification, dynamic pricing), see the Real-World Examples guide.
Simple Contact Form
A basic contact form using built-in validators with the validation: { validate: [...] } format.
import { ril, required, email, minLength } from '@rilaykit/core';
import { Form, FormField } from '@rilaykit/forms';
import type { ComponentRenderer } from '@rilaykit/core';
// Define a simple input component
interface InputProps {
label: string;
type?: string;
placeholder?: string;
required?: boolean;
}
const Input: ComponentRenderer<InputProps> = ({
id, value, onChange, onBlur, error, props, disabled,
}) => (
<div className="mb-4">
<label htmlFor={id} className="block text-sm font-medium text-gray-700">
{props.label} {props.required && <span className="text-red-500">*</span>}
</label>
<input
id={id}
type={props.type || 'text'}
value={value || ''}
onChange={(e) => onChange?.(e.target.value)}
onBlur={onBlur}
disabled={disabled}
placeholder={props.placeholder}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
/>
{error && <p className="mt-1 text-sm text-red-600">{error[0].message}</p>}
</div>
);
// Configure RilayKit
const rilay = ril.create()
.addComponent('input', { renderer: Input });
// Build the form
const contactForm = rilay
.form('contact')
.add({
id: 'name',
type: 'input',
props: { label: 'Full Name', required: true },
validation: { validate: [required(), minLength(2)] },
})
.add({
id: 'email',
type: 'input',
props: { label: 'Email', type: 'email', required: true },
validation: { validate: [required(), email()] },
})
.add({
id: 'message',
type: 'input',
props: { label: 'Message', required: true },
validation: { validate: [required(), minLength(10)] },
});
export function ContactForm() {
const handleSubmit = (data: { name: string; email: string; message: string }) => {
console.log('Contact form submitted:', data);
};
return (
<div className="max-w-md mx-auto">
<Form formConfig={contactForm} onSubmit={handleSubmit}>
<FormField fieldId="name" />
<FormField fieldId="email" />
<FormField fieldId="message" />
<button
type="submit"
className="w-full bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700"
>
Send Message
</button>
</Form>
</div>
);
}Registration with Conditional Fields
Using when() for conditional visibility and Zod as a direct Standard Schema validator -- no adapter needed.
import { ril, required, email, when } from '@rilaykit/core';
import { Form, FormField } from '@rilaykit/forms';
import { z } from 'zod';
const rilay = ril.create()
.addComponent('input', { renderer: Input })
.addComponent('select', { renderer: Select });
const registrationForm = rilay
.form('registration')
.add({
id: 'email',
type: 'input',
props: { label: 'Email', type: 'email' },
validation: {
validate: [required(), email()],
validateOnBlur: true,
},
})
.add({
id: 'password',
type: 'input',
props: { label: 'Password', type: 'password' },
validation: {
validate: [z.string().min(8, 'Password must be at least 8 characters')],
validateOnChange: true,
},
})
.add({
id: 'accountType',
type: 'select',
props: {
label: 'Account Type',
options: [
{ value: 'personal', label: 'Personal' },
{ value: 'business', label: 'Business' },
],
},
validation: { validate: [required()] },
})
.add({
id: 'companyName',
type: 'input',
props: { label: 'Company Name' },
validation: { validate: [required('Company name is required')] },
conditions: {
visible: when('accountType').equals('business'),
},
});
export function RegistrationForm() {
const handleSubmit = (data: Record<string, unknown>) => {
console.log('Registration submitted:', data);
};
return (
<Form formConfig={registrationForm} onSubmit={handleSubmit}>
<FormField fieldId="email" />
<FormField fieldId="password" />
<FormField fieldId="accountType" />
<FormField fieldId="companyName" />
<button type="submit" className="w-full bg-blue-600 text-white py-2 px-4 rounded">
Create Account
</button>
</Form>
);
}When a field is hidden via conditions.visible, its validation is automatically skipped. The companyName field above is only validated when accountType is "business".
Material-UI Integration
Integrate RilayKit with Material-UI by creating typed component renderers.
import { TextField } from '@mui/material';
import type { ComponentRenderer } from '@rilaykit/core';
interface MaterialInputProps {
label: string;
variant?: 'outlined' | 'filled' | 'standard';
multiline?: boolean;
rows?: number;
}
const MaterialInput: ComponentRenderer<MaterialInputProps> = ({
id,
value,
onChange,
onBlur,
error,
props,
disabled,
}) => (
<TextField
id={id}
label={props.label}
variant={props.variant || 'outlined'}
multiline={props.multiline}
rows={props.rows}
value={value || ''}
onChange={(e) => onChange?.(e.target.value)}
onBlur={onBlur}
error={!!error}
helperText={error?.[0]?.message}
disabled={disabled}
fullWidth
margin="normal"
/>
);
export { MaterialInput, type MaterialInputProps };import { ril } from '@rilaykit/core';
import { MaterialInput } from '@/components/MaterialInput';
import { MaterialSelect } from '@/components/MaterialSelect';
export const muiRilay = ril.create()
.addComponent('input', {
renderer: MaterialInput,
defaultProps: { variant: 'outlined' },
})
.addComponent('select', {
renderer: MaterialSelect,
defaultProps: { variant: 'outlined' },
})
.addComponent('textarea', {
renderer: MaterialInput,
defaultProps: { multiline: true, rows: 4 },
});import { Form, FormField } from '@rilaykit/forms';
import { required } from '@rilaykit/core';
import { muiRilay } from '@/lib/mui-rilay';
const profileForm = muiRilay
.form('user-profile')
.add({
id: 'name',
type: 'input',
props: { label: 'Full Name' },
validation: { validate: [required()] },
})
.add({
id: 'bio',
type: 'textarea',
props: { label: 'Biography', rows: 6 },
});
export function MaterialForm() {
return (
<Form formConfig={profileForm} onSubmit={console.log}>
<FormField fieldId="name" />
<FormField fieldId="bio" />
<button type="submit">Save Profile</button>
</Form>
);
}Shadcn/UI Integration
Integrate RilayKit with shadcn/ui components for a clean, accessible design system.
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import type { ComponentRenderer } from '@rilaykit/core';
interface ShadcnInputProps {
label: string;
type?: string;
placeholder?: string;
}
const ShadcnInput: ComponentRenderer<ShadcnInputProps> = ({
id,
value,
onChange,
onBlur,
error,
props,
disabled,
}) => (
<div className="grid w-full gap-1.5">
<Label htmlFor={id}>{props.label}</Label>
<Input
id={id}
type={props.type || 'text'}
value={value || ''}
onChange={(e) => onChange?.(e.target.value)}
onBlur={onBlur}
placeholder={props.placeholder}
disabled={disabled}
className={error ? 'border-destructive' : ''}
/>
{error && (
<p className="text-sm text-destructive">{error[0].message}</p>
)}
</div>
);
export { ShadcnInput, type ShadcnInputProps };import { ril } from '@rilaykit/core';
import { ShadcnInput } from '@/components/ShadcnInput';
import { ShadcnSelect } from '@/components/ShadcnSelect';
export const shadcnRilay = ril.create()
.addComponent('input', { renderer: ShadcnInput })
.addComponent('select', { renderer: ShadcnSelect });import { Form, FormField } from '@rilaykit/forms';
import { required, email } from '@rilaykit/core';
import { shadcnRilay } from '@/lib/shadcn-rilay';
const contactForm = shadcnRilay
.form('contact')
.add({
id: 'name',
type: 'input',
props: { label: 'Name', placeholder: 'Your name' },
validation: { validate: [required()] },
})
.add({
id: 'email',
type: 'input',
props: { label: 'Email', type: 'email', placeholder: 'you@example.com' },
validation: { validate: [required(), email()] },
});
export function ShadcnForm() {
return (
<Form formConfig={contactForm} onSubmit={console.log}>
<div className="space-y-4">
<FormField fieldId="name" />
<FormField fieldId="email" />
<button
type="submit"
className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
>
Submit
</button>
</div>
</Form>
);
}Multi-Step Workflow
Build a multi-step user onboarding workflow using rilay.flow().
import { ril, required, email, minLength } from '@rilaykit/core';
import { Workflow } from '@rilaykit/workflow';
const rilay = ril.create()
.addComponent('input', { renderer: Input })
.addComponent('checkbox', { renderer: Checkbox });
// Step 1: Account creation
const accountForm = rilay
.form('account')
.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(), minLength(8)] },
});
// Step 2: Profile details
const profileForm = rilay
.form('profile')
.add(
{ id: 'firstName', type: 'input', props: { label: 'First Name' } },
{ id: 'lastName', type: 'input', props: { label: 'Last Name' } },
);
// Step 3: Confirmation
const confirmForm = rilay
.form('confirm')
.add({
id: 'terms',
type: 'checkbox',
props: { label: 'I agree to the terms and conditions' },
validation: {
validate: [required('You must accept the terms')],
},
});
// Build the workflow
const onboardingWorkflow = rilay
.flow('onboarding', 'User Onboarding')
.addStep({
id: 'account',
title: 'Create Account',
description: 'Set up your credentials',
formConfig: accountForm,
})
.addStep({
id: 'profile',
title: 'Your Profile',
description: 'Tell us about yourself',
formConfig: profileForm,
allowSkip: true,
})
.addStep({
id: 'confirm',
title: 'Confirmation',
description: 'Review and accept terms',
formConfig: confirmForm,
})
.configure({
analytics: {
onWorkflowStart: (id) => console.log(`Started: ${id}`),
onWorkflowComplete: (id, totalTime) => console.log(`Completed: ${id} in ${totalTime}ms`),
},
});
export function OnboardingWorkflow() {
const handleComplete = (data: Record<string, unknown>) => {
console.log('Workflow completed:', data);
};
return <Workflow workflowConfig={onboardingWorkflow} onWorkflowComplete={handleComplete} />;
}Async Validation
Use the async() validator from @rilaykit/core for server-side checks like email uniqueness.
import { ril, required, email, async as asyncValidator } from '@rilaykit/core';
import { Form, FormField } from '@rilaykit/forms';
// Custom async validator using the built-in async() helper
const checkEmailAvailability = asyncValidator(
async (value: string) => {
const response = await fetch(`/api/check-email?email=${value}`);
const { available } = await response.json();
return available;
},
'This email is already taken',
);
const rilay = ril.create()
.addComponent('input', { renderer: Input });
const signupForm = rilay
.form('signup')
.add({
id: 'email',
type: 'input',
props: { label: 'Email', type: 'email' },
validation: {
validate: [required(), email(), checkEmailAvailability],
validateOnBlur: true,
debounceMs: 500,
},
})
.add({
id: 'password',
type: 'input',
props: { label: 'Password', type: 'password' },
validation: {
validate: [required(), minLength(8)],
},
});
export function AsyncValidationForm() {
return (
<Form formConfig={signupForm} onSubmit={console.log}>
<FormField fieldId="email" />
<FormField fieldId="password" />
<button type="submit" className="bg-blue-600 text-white py-2 px-4 rounded">
Sign Up
</button>
</Form>
);
}You can also achieve async validation using Zod's .refine() with an async callback, which works as a Standard Schema validator. See the Validation guide for details.
Next.js App Router
RilayKit works seamlessly with Next.js App Router. Since forms are interactive, mark your component file with 'use client'.
'use client';
import { ril, required, email } from '@rilaykit/core';
import { Form, FormField } from '@rilaykit/forms';
import type { ComponentRenderer } from '@rilaykit/core';
// Component definition
const Input: ComponentRenderer<{ label: string; type?: string }> = ({
id, value, onChange, onBlur, error, props,
}) => (
<div className="mb-4">
<label htmlFor={id} className="block text-sm font-medium">{props.label}</label>
<input
id={id}
type={props.type || 'text'}
value={value || ''}
onChange={(e) => onChange?.(e.target.value)}
onBlur={onBlur}
className="mt-1 w-full rounded-md border p-2"
/>
{error && <p className="text-red-500 text-sm mt-1">{error[0].message}</p>}
</div>
);
// RilayKit setup
const rilay = ril.create()
.addComponent('input', { renderer: Input });
const contactForm = rilay
.form('contact')
.add({
id: 'name',
type: 'input',
props: { label: 'Name' },
validation: { validate: [required()] },
})
.add({
id: 'email',
type: 'input',
props: { label: 'Email', type: 'email' },
validation: { validate: [required(), email()] },
});
export default function ContactPage() {
async function handleSubmit(data: { name: string; email: string }) {
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error('Failed to submit');
}
}
return (
<div className="container mx-auto max-w-md py-8">
<h1 className="text-3xl font-bold mb-8">Contact Us</h1>
<Form formConfig={contactForm} onSubmit={handleSubmit}>
<FormField fieldId="name" />
<FormField fieldId="email" />
<button
type="submit"
className="w-full bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700"
>
Send
</button>
</Form>
</div>
);
}In a real application, extract your ril instance and form configurations into separate files (e.g., lib/rilay.ts and config/forms.ts) to keep them reusable across pages.
Testing with Vitest
RilayKit forms can be tested with Vitest and Testing Library using standard patterns.
import { describe, it, expect, vi } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ContactForm } from './ContactForm';
describe('ContactForm', () => {
it('should display validation errors for empty required fields', async () => {
render(<ContactForm />);
const submitButton = screen.getByRole('button', { name: /send message/i });
await userEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/required/i)).toBeInTheDocument();
});
});
it('should validate email format on blur', async () => {
const user = userEvent.setup();
render(<ContactForm />);
const emailInput = screen.getByLabelText(/email/i);
await user.type(emailInput, 'not-an-email');
await user.tab(); // triggers onBlur
await waitFor(() => {
expect(screen.getByText(/valid email/i)).toBeInTheDocument();
});
});
it('should call onSubmit with valid data', async () => {
const user = userEvent.setup();
const handleSubmit = vi.fn();
render(<ContactForm onSubmit={handleSubmit} />);
await user.type(screen.getByLabelText(/name/i), 'Jane Doe');
await user.type(screen.getByLabelText(/email/i), 'jane@example.com');
await user.type(screen.getByLabelText(/message/i), 'Hello, this is a test message.');
await user.click(screen.getByRole('button', { name: /send message/i }));
await waitFor(() => {
expect(handleSubmit).toHaveBeenCalledWith({
name: 'Jane Doe',
email: 'jane@example.com',
message: 'Hello, this is a test message.',
});
});
});
});More examples and starter templates are available in the GitHub repository.