Forms
Form layouts
Form layouts
Real business forms need more than vertical field stacks. This guide shows responsive layout patterns for single-column, two-column, tabbed, and wizard forms using Tailwind CSS grid and Preline UI components.
Why layout patterns matter
Form layout affects:
- Cognitive load — grouped fields reduce mental effort
- Mobile usability — responsive grids adapt to screen size
- Completion rates — clear structure guides users through complex forms
- Validation clarity — errors appear near their fields
Laravolt forms use Tailwind CSS utilities for layout, so patterns are portable across projects.
Single-column layout
Default for most forms. Fields stack vertically with consistent spacing:
Blade
{!! PrelineForm::open('users.store')->post() !!} <div class="space-y-4"> {!! PrelineForm::text('name')->label('Full name')->required() !!} {!! PrelineForm::email('email')->label('Email')->required() !!} {!! PrelineForm::password('password')->label('Password')->required() !!} {!! PrelineForm::textarea('bio')->label('Bio')->rows(4) !!} </div> <div class="mt-6 flex gap-3"> {!! PrelineForm::submit('Create account')->primary() !!} {!! PrelineForm::link('Cancel', route('home'))->secondary() !!} </div>{!! PrelineForm::close() !!}When to use:
- Simple forms (< 6 fields)
- Mobile-first designs
- Forms with long text fields
Two-column layout
Reduces vertical scrolling for forms with many short fields:
Blade
{!! PrelineForm::open('products.store')->post() !!} <div class="grid grid-cols-1 gap-6 lg:grid-cols-2"> <div class="space-y-4"> {!! PrelineForm::text('name')->label('Product name')->required() !!} {!! PrelineForm::text('sku')->label('SKU')->required() !!} {!! PrelineForm::number('price')->label('Price')->mask('currency') !!} </div> <div class="space-y-4"> {!! PrelineForm::select('category_id', $categories)->label('Category')->required() !!} {!! PrelineForm::select('brand_id', $brands)->label('Brand') !!} {!! PrelineForm::checkbox('is_featured', 1)->label('Featured product') !!} </div> </div> <div class="mt-6"> {!! PrelineForm::textarea('description')->label('Description')->rows(4) !!} </div> <div class="mt-6 flex gap-3"> {!! PrelineForm::submit('Save product')->primary() !!} </div>{!! PrelineForm::close() !!}Responsive behavior:
- Mobile: single column (
grid-cols-1) - Desktop: two columns (
lg:grid-cols-2)
When to use:
- Forms with 6-12 short fields
- Desktop-heavy workflows
- Related field groups (e.g., billing + shipping)
Tabbed forms
Group related sections into tabs for long forms:
Blade
<div x-data="{ tab: 'basic' }"> {!! PrelineForm::open('products.store')->post() !!} {{-- Tab navigation --}} <div class="border-b border-gray-200 dark:border-neutral-700"> <nav class="flex gap-x-2"> <button type="button" @click="tab = 'basic'" :class="tab === 'basic' ? 'border-blue-600 text-blue-600' : 'border-transparent text-gray-500'" class="inline-flex items-center gap-2 border-b-2 px-1 py-4 text-sm font-medium" > Basic info </button> <button type="button" @click="tab = 'pricing'" :class="tab === 'pricing' ? 'border-blue-600 text-blue-600' : 'border-transparent text-gray-500'" class="inline-flex items-center gap-2 border-b-2 px-1 py-4 text-sm font-medium" > Pricing </button> <button type="button" @click="tab = 'inventory'" :class="tab === 'inventory' ? 'border-blue-600 text-blue-600' : 'border-transparent text-gray-500'" class="inline-flex items-center gap-2 border-b-2 px-1 py-4 text-sm font-medium" > Inventory </button> </nav> </div> {{-- Tab panels --}} <div class="mt-6"> <div x-show="tab === 'basic'" class="space-y-4"> {!! PrelineForm::text('name')->label('Product name')->required() !!} {!! PrelineForm::textarea('description')->label('Description')->rows(4) !!} {!! PrelineForm::select('category_id', $categories)->label('Category') !!} </div> <div x-show="tab === 'pricing'" class="space-y-4"> {!! PrelineForm::number('price')->label('Price')->mask('currency')->required() !!} {!! PrelineForm::number('cost')->label('Cost')->mask('currency') !!} {!! PrelineForm::number('discount')->label('Discount %')->min(0)->max(100) !!} </div> <div x-show="tab === 'inventory'" class="space-y-4"> {!! PrelineForm::text('sku')->label('SKU')->required() !!} {!! PrelineForm::number('stock')->label('Stock quantity')->min(0) !!} {!! PrelineForm::checkbox('track_inventory', 1)->label('Track inventory') !!} </div> </div> <div class="mt-6 flex gap-3"> {!! PrelineForm::submit('Save product')->primary() !!} </div> {!! PrelineForm::close() !!}</div>When to use:
- Forms with 12+ fields
- Logically distinct sections
- Optional sections (e.g., advanced settings)
Validation note: All tabs submit together. Show error indicators on tab buttons:
Blade
<button type="button" @click="tab = 'basic'" class="..."> Basic info @if($errors->has(['name', 'description', 'category_id'])) <span class="ml-1 h-2 w-2 rounded-full bg-red-500"></span> @endif</button>Wizard forms (multi-step)
Break complex forms into sequential steps:
Blade
<div x-data="{ step: 1 }"> {!! PrelineForm::open('orders.store')->post() !!} {{-- Progress indicator --}} <div class="mb-8"> <ol class="flex items-center w-full text-sm font-medium text-center text-gray-500 dark:text-gray-400"> <li :class="step >= 1 ? 'text-blue-600 dark:text-blue-500' : ''" class="flex items-center"> <span class="flex items-center justify-center w-8 h-8 border rounded-full shrink-0" :class="step >= 1 ? 'border-blue-600' : 'border-gray-500'"> 1 </span> <span class="ml-2">Customer</span> </li> <li :class="step >= 2 ? 'text-blue-600 dark:text-blue-500' : ''" class="flex items-center ml-4"> <span class="flex items-center justify-center w-8 h-8 border rounded-full shrink-0" :class="step >= 2 ? 'border-blue-600' : 'border-gray-500'"> 2 </span> <span class="ml-2">Products</span> </li> <li :class="step >= 3 ? 'text-blue-600 dark:text-blue-500' : ''" class="flex items-center ml-4"> <span class="flex items-center justify-center w-8 h-8 border rounded-full shrink-0" :class="step >= 3 ? 'border-blue-600' : 'border-gray-500'"> 3 </span> <span class="ml-2">Review</span> </li> </ol> </div> {{-- Step 1: Customer --}} <div x-show="step === 1" class="space-y-4"> {!! PrelineForm::select('customer_id', $customers)->label('Customer')->required() !!} {!! PrelineForm::date('order_date')->label('Order date')->required() !!} {!! PrelineForm::textarea('notes')->label('Notes')->rows(3) !!} </div> {{-- Step 2: Products --}} <div x-show="step === 2" class="space-y-4"> <div id="products-container"> {{-- Dynamic product rows --}} </div> <button type="button" class="text-sm text-blue-600">+ Add product</button> </div> {{-- Step 3: Review --}} <div x-show="step === 3" class="space-y-4"> <div class="rounded-lg border border-gray-200 p-4 dark:border-neutral-700"> <h3 class="font-medium">Order summary</h3> {{-- Display collected data --}} </div> </div> {{-- Navigation --}} <div class="mt-6 flex justify-between"> <button type="button" @click="step--" x-show="step > 1" class="rounded-lg border border-gray-200 px-4 py-2 text-sm font-medium" > Previous </button> <button type="button" @click="step++" x-show="step < 3" class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white" > Next </button> <button type="submit" x-show="step === 3" class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white" > Submit order </button> </div> {!! PrelineForm::close() !!}</div>When to use:
- Forms with 15+ fields
- Sequential data collection (e.g., checkout, onboarding)
- Forms where early steps determine later options
Validation strategy:
- Client-side: validate each step before allowing "Next"
- Server-side: validate all steps on final submit
- Show errors on the relevant step