Add a date range picker to a Laravel form
A start and end date in one Blade field: validation that agrees with it, bookings from today, report periods up to today, your own field names, and Livewire wire:model.
php artisan bladewell:add date-range-picker
Uses Date range picker. New to Bladewell? Install it first.
One field, two dates
The date range picker is one field with a calendar popover that shows two months side by side (one on a phone). People pick a start, then an end, with the mouse or the arrow keys. It submits two plain Y-m-d values: name="period" sends period[start] and period[end].
<form method="GET" action="{{ route('reports.index') }}" class="flex flex-wrap items-end gap-4">
<x-widget.date-range-picker name="period" label="Report period" required />
<x-widget.button type="submit">Show report</x-widget.button>
</form>
By default nothing after today can be picked, which suits reports, leave requests and anything else that already happened. Dates and month names follow your app's locale, and the week starts on the locale's first day. Try it live.
Validate it
Validate each end under its own key, and the end against the start. Messages filed under period, period.start or period.end all show under the one field, with old input filled back in.
namespace App\Http\Requests;
use App\Rules\NotAfterToday;
use Illuminate\Foundation\Http\FormRequest;
class ReportRequest extends FormRequest
{
public function rules(): array
{
return [
'period.start' => ['required', 'date_format:Y-m-d', new NotAfterToday],
'period.end' => ['required', 'date_format:Y-m-d', new NotAfterToday, 'after_or_equal:period.start'],
];
}
}
Why NotAfterToday and not before_or_equal:today? The picker's "today" is the visitor's own day, and your server's is usually UTC's. A visitor east of UTC can be a day ahead, so Laravel's rule would reject a genuine today. NotAfterToday compares against the first timezone to reach a new day, so it never does, and still rejects anything in the future. It comes with the picker, copied into app/Rules.
Bookings: from today, with no upper limit
For stays, rentals or events, turn the limits around: min="today" and :max="false".
<x-widget.date-range-picker name="stay" label="Stay" min="today" :max="false" info="Check-in to check-out." required />
return [
// yesterday, not today: see below.
'stay.start' => ['required', 'date_format:Y-m-d', 'after_or_equal:yesterday'],
'stay.end' => ['required', 'date_format:Y-m-d', 'after:stay.start'],
];
The same timezone gap applies the other way. A visitor west of your server can still be on yesterday by its clock, so after_or_equal:today would turn down their today. yesterday allows for it. Whether those nights are free is still for your own availability check to decide. after:stay.start asks for at least one night; use after_or_equal where a single day is a valid range.
Your own field names
If your query or model already uses two flat fields, name them with start-name and end-name, and validate from and to directly. :start and :end show a saved range, and take a date object or a Y-m-d string.
<x-widget.date-range-picker start-name="from" end-name="to" label="Dates" :start="$filters['from'] ?? null" :end="$filters['to'] ?? null" />
In a Livewire component
Bind it to an array with start and end keys. Both ends update together, in one request with wire:model.live, and a range you set or clear in PHP shows on the next render.
namespace App\Livewire;
use App\Rules\NotAfterToday;
use Livewire\Component;
class ReportPeriod extends Component
{
public array $period = ['start' => null, 'end' => null];
public function updatedPeriod(): void
{
$this->validate([
'period.start' => ['required', 'date_format:Y-m-d', new NotAfterToday],
'period.end' => ['required', 'date_format:Y-m-d', new NotAfterToday, 'after_or_equal:period.start'],
]);
}
public function render()
{
return view('livewire.report-period');
}
}
<div>
<x-widget.date-range-picker label="Report period" wire:model.live="period" />
</div>
Only need one date?
The datepicker has the same calendar, limits and keyboard support for a single date, and a birthday mode with a minimum age.