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
Previous
Custom fields