Complete form builder system with 30+ field types, validation, and Blade/Inertia.js integration for Laravilt.
Laravilt Forms provides a comprehensive form building system with:
- 30+ Field Types: Text, select, date, file upload, rich editor, and more
- Fluent Builder: Chainable methods for clean, readable form definitions
- Validation: Built-in Laravel validation integration
- Blade Components: Pre-built UI components with Reka UI styling
- Inertia Integration: Seamless Vue 3 form handling
- Real-time Reactivity: Dynamic field visibility, options, and validation
- File Management: Advanced file upload with preview and validation
- Rich Content: WYSIWYG editor, Markdown editor, and code editor support
# Generate a new form class
php artisan make:form UserForm
# Generate a resource form (CRUD)
php artisan make:form UserForm --resource
# Use in Blade (server-side)
<x-laravilt::form :schema="$form" />
# Use in Inertia (client-side)
<Form :schema="form.schema" />- TextInput: Single-line text input with validation
- Textarea: Multi-line text input
- TranslatableInput: Multi-language text input with a per-locale dialog
- NumberField: Numeric input with min/max
- Select: Dropdown select with search
- Checkbox: Single checkbox
- CheckboxList: Multiple checkboxes
- Radio: Radio button group
- Toggle: Switch/toggle button
- ToggleButtons: Button group toggle
- Hidden: Hidden field
- DatePicker: Single date selection
- DateTimePicker: Date and time selection
- TimePicker: Time selection
- DateRangePicker: Date range selection
- ColorPicker: Color selection with swatches
- IconPicker: Lucide icon picker
- FileUpload: File/image upload with preview
- RichEditor: WYSIWYG editor (TipTap)
- MarkdownEditor: Markdown editor with preview
- CodeEditor: Syntax-highlighted code editor (CodeMirror)
- TagsInput: Multiple tags input
- KeyValue: Key-value pair editor
- Slider: Range slider
- RateInput: Star rating input
- PinInput: PIN/OTP input
- Repeater: Repeatable field groups
- Builder: Block-based content builder
- Label and placeholder
- Help text and hints
- Required indicator
- Validation rules
- Disabled and readonly states
- Conditional visibility
- Default values
- Dependent fields
- Custom prefixes/suffixes
- Inline layout
- PHP 8.3+
- Laravel 12+
- Blade or Inertia.js v2+
- Vue 3 (for Inertia)
composer require laravilt/formsThe service provider is auto-discovered and will register automatically.
Publish the configuration:
php artisan vendor:publish --tag=laravilt-forms-configPublish the views:
php artisan vendor:publish --tag=laravilt-forms-viewsPublish the assets:
php artisan vendor:publish --tag=laravilt-forms-assets<?php
namespace App\Forms;
use Laravilt\Forms\Form;
use Laravilt\Forms\Components\TextInput;
use Laravilt\Forms\Components\Textarea;
use Laravilt\Forms\Components\Select;
use Laravilt\Forms\Concerns\HasSchema;
class UserForm extends Form
{
use HasSchema;
protected function setUp(): void
{
parent::setUp();
$this->schema([
TextInput::make('name')
->label('Full Name')
->required()
->placeholder('Enter your name'),
TextInput::make('email')
->label('Email Address')
->email()
->required()
->unique('users', 'email'),
Textarea::make('bio')
->label('Biography')
->rows(5)
->placeholder('Tell us about yourself'),
Select::make('role')
->label('Role')
->options([
'admin' => 'Administrator',
'editor' => 'Editor',
'viewer' => 'Viewer',
])
->required(),
]);
}
}use App\Forms\UserForm;
use Inertia\Inertia;
public function create()
{
$form = UserForm::make();
return Inertia::render('Users/Create', [
'form' => $form->toLaraviltProps(),
]);
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'bio' => 'nullable|string',
'role' => 'required|in:admin,editor,viewer',
]);
User::create($validated);
return redirect()->route('users.index');
}<x-laravilt::form
:schema="$form->getSchema()"
action="/users"
method="POST"
/><template>
<div>
<Form
:schema="form.schema"
@submit="handleSubmit"
/>
</div>
</template>
<script setup>
import { useForm } from '@inertiajs/vue3'
import Form from '@/components/forms/Form.vue'
const props = defineProps({
form: Object
})
const form = useForm({})
const handleSubmit = () => {
form.post('/users')
}
</script>TextInput::make('name')
->label('Name')
->placeholder('Enter name')
->required()
->minLength(3)
->maxLength(255)
->prefix('Mr.')
->suffix('@example.com')
->helperText('Enter your full name');A multi-language text field. Its value is an array keyed by locale code
(['en' => 'Title', 'ar' => 'العنوان', 'ckb' => 'ناونیشان']). The main input edits the active
locale; a globe button inside the input opens a dialog with one input per
locale (RTL locales render with dir="rtl"), so long titles and content stay
readable. The globe is hidden when only one
locale is allowed.
TranslatableInput::make('name')
->label('Name')
->locales(['en', 'ar', 'ckb']) // defaults to the configured locales, see below
->activeLocale('en') // defaults to the app locale when allowed
->required() // every locale is required...
->requiredLocales(['en']) // ...unless you narrow it down
->maxLength(255) // applied per locale
->multiline() // textarea per locale
->rows(4);Locales resolve in this order: explicit locales(), then
config('laravilt-forms.locales') (published to config/laravilt-forms.php),
then config('app.available_locales'), then the application locale.
By default the field reuses the languages the panel already knows about. That
is the same list the Locale & Timezone
settings page shows, so a project defines its languages once in
config/app.php; label becomes the native name shown in the dialog and
dir sets dir on each input:
// config/app.php
'available_locales' => [
['value' => 'en', 'label' => 'English', 'dir' => 'ltr'],
['value' => 'ar', 'label' => 'العربية', 'dir' => 'rtl'],
['value' => 'ckb', 'label' => 'کوردی', 'dir' => 'rtl'],
],Set laravilt-forms.locales only when the languages content is written in
differ from the languages the UI is shown in, for example an English-only
admin panel that manages content in three languages. It accepts plain codes or
per-locale metadata, and an optional label overrides the short badge on the
globe button:
// config/laravilt-forms.php
'locales' => [
'en' => ['name' => 'English', 'direction' => 'ltr'],
'ar' => ['name' => 'العربية', 'direction' => 'rtl'],
'ckb' => ['name' => 'کوردی', 'direction' => 'rtl'],
],
// or simply: 'locales' => ['en', 'ar', 'ckb'],Names and directions are looked up in laravilt-forms.locales first and
app.available_locales second, so plain codes in the forms config still pick
up the names the panel defines. A code found in neither is shown as its own
code, labelled with its uppercased base code, and rendered LTR.
Every locale is validated on its own and errors are reported per locale key
(name.en, name.ar, ...), which the field shows next to the matching input.
getValidationRules() returns one rule list for the field, as the schema
expects, with a TranslationsRule carrying the per-locale rules; use
getLocaleValidationRules() for flat keys when validating by hand:
$field = TranslatableInput::make('name')->locales(['en', 'ar', 'ckb'])->required()->maxLength(120);
$field->getValidationRules();
// ['required', 'array', TranslationsRule(en|ar|ckb => ['required', 'string', 'max:120'])]
$field->getLocaleValidationRules();
// [
// 'name' => ['required', 'array'],
// 'name.en' => ['required', 'string', 'max:120'],
// 'name.ar' => ['required', 'string', 'max:120'],
// 'name.ckb' => ['required', 'string', 'max:120'],
// ]Messages use the field label plus the locale, e.g. "The Name (EN) field is
required.", and validationMessages(['required' => '...']) applies to every
locale (['en.required' => '...'] targets one).
hydrateState() / dehydrateState() accept a JSON string, an array or null
and always return an array with every allowed locale present, so the field works
with both storage styles:
// spatie/laravel-translatable
class Product extends Model
{
use \Spatie\Translatable\HasTranslations;
public $translatable = ['name'];
}
// or a plain JSON column
class Category extends Model
{
protected $casts = ['name' => 'array'];
}
// Both accept the field's value as-is:
$product->setTranslations('name', $data['name']); // or $product->name = $data['name'];
$category->name = $data['name'];Select::make('country')
->label('Country')
->options([
'us' => 'United States',
'uk' => 'United Kingdom',
'ca' => 'Canada',
])
->searchable()
->required()
->placeholder('Select a country');DatePicker::make('birth_date')
->label('Birth Date')
->required()
->minDate(now()->subYears(100))
->maxDate(now()->subYears(18))
->displayFormat('F j, Y');FileUpload::make('avatar')
->label('Profile Picture')
->image()
->maxSize(2048) // 2MB
->acceptedFileTypes(['image/jpeg', 'image/png'])
->imagePreview()
->imageCropAspectRatio('1:1')
->imageResizeTargetWidth('500')
->imageResizeTargetHeight('500');RichEditor::make('content')
->label('Content')
->required()
->toolbarButtons([
'bold',
'italic',
'underline',
'link',
'bulletList',
'orderedList',
'heading',
])
->minLength(100);use Laravilt\Forms\Components\Repeater;
Repeater::make('contacts')
->label('Contacts')
->schema([
TextInput::make('name')
->label('Name')
->required(),
TextInput::make('email')
->label('Email')
->email()
->required(),
TextInput::make('phone')
->label('Phone'),
])
->minItems(1)
->maxItems(5)
->addActionLabel('Add Contact')
->reorderable()
->collapsible();KeyValue::make('metadata')
->label('Metadata')
->keyLabel('Key')
->valueLabel('Value')
->addActionLabel('Add Field')
->reorderable();ColorPicker::make('brand_color')
->label('Brand Color')
->required()
->swatches([
'#FF0000',
'#00FF00',
'#0000FF',
]);TextInput::make('email')
->required()
->email()
->unique('users', 'email')
->confirmed();
NumberField::make('age')
->required()
->min(18)
->max(100);
TextInput::make('password')
->required()
->minLength(8)
->maxLength(255)
->password()
->confirmed();TextInput::make('username')
->rules(['required', 'string', 'alpha_dash', 'unique:users']);use Laravilt\Support\Utilities\Get;
Select::make('country')
->label('Country')
->options([
'us' => 'United States',
'ca' => 'Canada',
])
->live(), // Make field reactive
Select::make('state')
->label('State')
->options(fn (Get $get) =>
$get('country') === 'us'
? ['ca' => 'California', 'ny' => 'New York']
: ['on' => 'Ontario', 'bc' => 'British Columbia']
)
->visible(fn (Get $get) => in_array($get('country'), ['us', 'ca']))
->required();use Laravilt\Support\Utilities\Set;
Select::make('product_type')
->label('Product Type')
->options([
'physical' => 'Physical Product',
'digital' => 'Digital Product',
])
->afterStateUpdated(function ($value, Set $set) {
if ($value === 'digital') {
$set('requires_shipping', false);
}
})
->live();# Generate a basic form
php artisan make:form UserForm
# Generate a resource form with CRUD operations
php artisan make:form UserForm --resource
# Force overwrite existing file
php artisan make:form UserForm --force
# Generate a form component (custom field)
php artisan make:component CustomField- Use Form Classes: Create dedicated form classes for reusability
- Validate on Server: Always validate on server-side even with client-side validation
- Use Appropriate Field Types: Choose the right field type for better UX
- Add Helper Text: Provide clear instructions with helper text
- Group Related Fields: Use sections to organize related fields
- Mark Required Fields: Always indicate required fields
- Provide Feedback: Show validation errors clearly
- Test Accessibility: Ensure forms are keyboard-navigable
use Laravilt\Forms\Form;
use Laravilt\Forms\Components\TextInput;
class LoginForm extends Form
{
use HasSchema;
protected function setUp(): void
{
parent::setUp();
$this->schema([
TextInput::make('email')
->label('Email')
->email()
->required()
->autofocus(),
TextInput::make('password')
->label('Password')
->password()
->required(),
Checkbox::make('remember')
->label('Remember me'),
]);
}
}use Laravilt\Forms\Components\*;
use Laravilt\Schemas\Components\Section;
class ProductForm extends Form
{
use HasSchema;
protected function setUp(): void
{
parent::setUp();
$this->schema([
Section::make('Basic Information')
->schema([
TextInput::make('name')
->label('Product Name')
->required(),
Textarea::make('description')
->label('Description')
->required()
->rows(5),
Select::make('category_id')
->label('Category')
->relationship('category', 'name')
->required(),
]),
Section::make('Pricing')
->schema([
NumberField::make('price')
->label('Price')
->prefix('$')
->required()
->min(0)
->step(0.01),
NumberField::make('compare_at_price')
->label('Compare at Price')
->prefix('$')
->min(0)
->step(0.01),
Toggle::make('is_taxable')
->label('Taxable')
->default(true),
]),
Section::make('Inventory')
->schema([
NumberField::make('quantity')
->label('Quantity')
->required()
->min(0)
->default(0),
Toggle::make('track_inventory')
->label('Track Inventory')
->default(true),
]),
Section::make('Media')
->schema([
FileUpload::make('images')
->label('Product Images')
->image()
->multiple()
->maxFiles(5)
->maxSize(5120)
->imagePreview()
->reorderable(),
]),
]);
}
}- TextInput
- Textarea
- TranslatableInput
- NumberField
- Select
- Checkbox
- CheckboxList
- Radio
- Toggle
- ToggleButtons
- Hidden
- DatePicker
- DateTimePicker
- TimePicker
- DateRangePicker
- ColorPicker
- IconPicker
- FileUpload
- RichEditor
- MarkdownEditor
- CodeEditor
- TagsInput
- KeyValue
- Slider
- RateInput
- PinInput
- Repeater
- Builder
- GitHub Issues: github.com/laravilt/forms
- Documentation: docs.laravilt.com
- Discord: discord.laravilt.com