camroncade/timezone
Laravel package for working with timezones. It provides curated timezone identifier lists, select-menu helpers, and validation support, making it easy to build timezone pickers and user settings without maintaining your own arrays. Includes offset helpers for timezone-aware forms.
Install and confirm it loads.
composer require camroncade/timezone
On Laravel 5.5+ the service provider is discovered automatically; on older apps (or if discovery is disabled) register it manually:
// config/app.php
'providers' => [
Camroncade\Timezone\TimezoneServiceProvider::class,
],
'aliases' => [
'Timezone' => Camroncade\Timezone\Facades\Timezone::class,
],
Publish the config file (it is short — read it before you touch anything else):
php artisan vendor:publish --provider="Camroncade\Timezone\TimezoneServiceProvider"
Where to look first. The whole package is a thin wrapper around DateTimeZone::listIdentifiers(). Before writing any code, do this in php artisan tinker:
Timezone::toArray(); // inspect the value/text keys
Timezone::toSelect('timezone', 'Europe/London'); // inspect the markup it emits
That tells you exactly what shape your Blade templates and validation rules need to consume. Also open vendor/camroncade/timezone/src/Timezone.php — it is ~100 lines, and knowing precisely which DateTimeZone constant it passes matters for the backward-compatibility gotcha below.
First real use case: the user profile form.
{{-- resources/views/profile/edit.blade.php --}}
<form method="POST" action="{{ route('profile.update') }}">
@csrf
{!! Timezone::toSelect(
'timezone',
old('timezone', $user->timezone ?? config('app.timezone')),
['class' => 'form-control', 'id' => 'timezone']
) !!}
</form>
Then persist it and validate it with Laravel's built-in rule:
$validated = $request->validate([
'timezone' => ['required', 'string', 'timezone'], // 'timezone' validates against DateTimeZone::listIdentifiers()
]);
$user->update($validated);
Pattern 1 — Let the package emit the markup (fastest path). toSelect() returns a complete <select> string, so render it unescaped with {!! !!}. Always feed it old() so validation failures don't reset the dropdown. This is fine for a handful of admin/profile forms and is the least code.
Pattern 2 — Take the data, own the markup. For anything beyond a trivial form (grouping, search attributes, Tailwind classes, @selected), skip toSelect() and use toArray():
// app/Http/Controllers/ProfileController.php
public function edit(Request $request)
{
return view('profile.edit', [
'user' => $request->user(),
'timezones' => Timezone::toArray(), // [['value' => '...', 'text' => '...'], ...]
]);
}
<select name="timezone" id="timezone" class="form-select">
@foreach ($timezones as $timezone)
<option value="{{ $timezone['value'] }}"
@selected(old('timezone', $user->timezone) === $timezone['value'])>
{{ $timezone['text'] }}
</option>
@endforeach
</select>
For Laravel < 9, replace @selected(...) with {{ old('timezone', $user->timezone) === $timezone['value'] ? 'selected' : '' }}.
Pattern 3 — Ship the list to the frontend. Because toArray() is plain JSON-serializable data, it doubles as a select payload for Vue/React/Alpine:
Route::get('/api/timezones', fn () => Timezone::toArray())->middleware('auth');
Cache it if you're serving it hot — it never changes within a PHP release:
$timezones = Cache::rememberForever('timezones.v1', fn () => Timezone::toArray());
Pattern 4 — Pair it with real timezone handling. The package only picks an identifier; conversion is yours. Store the identifier (a string(64) column is ample — the longest IANA name is 32 chars) and convert at the edges:
// display
$user->created_at->setTimezone($user->timezone)->format('M j, Y g:i A');
// accept local input, store UTC
$startsAt = Carbon::parse($request->input('starts_at'), $user->timezone)->utc();
If you let users enter a local time, render the form in their zone and normalize to UTC on save. Keep config('app.timezone') as UTC and treat the user column as presentation-only.
Pattern 5 — Scheduling / localization. For a scheduling UI, combine the dropdown with a live preview so users can confirm the offset: render Timezone::toSelect() alongside a small Alpine/JS snippet that reads Intl.DateTimeFormat().resolvedOptions().timeZone and pre-selects the browser zone as a default (old('timezone', $user->timezone ?? $detected)). That single touch removes most "wrong timezone" support tickets.
It is unmaintained. Last release was April 2019, targeting the Laravel 5.x/PHP 7.x era. Check vendor/camroncade/timezone/composer.json for the php and illuminate/support constraints before assuming it installs cleanly on PHP 8.2 / Laravel 11. If Composer refuses, you're usually better off replacing it with a two-line helper than fighting platform requirements:
$timezones = collect(DateTimeZone::listIdentifiers())
->map(fn ($tz) => ['value' => $tz, 'text' => $tz]);
That is genuinely all the package does — weigh that against the dependency.
Raw HTML output. toSelect() returns a string, so {{ Timezone::toSelect(...) }} will print escaped markup. You must use {!! !!}. Do not pass user-supplied values into the $attributes array without escaping; the options themselves are safe (they come from PHP), but attributes may be interpolated naively.
old() is not automatic. The package has no request awareness — it will happily re-select the persisted value after a failed validation and silently hide the user's correction. Always pass old('timezone', $user->timezone) as the selected argument.
Backward-compatible identifiers. DateTimeZone::listIdentifiers() excludes deprecated aliases like US/Eastern and GMT by default, while ALL_WITH_BC includes them. If the package uses the plain ALL constant and you have legacy rows storing US/Eastern, those users will fail the timezone validation rule and their saved value won't appear as selected. Audit existing data before rollout (SELECT DISTINCT timezone FROM users WHERE timezone NOT IN (...)), or normalize on read.
400+ ungrouped options is poor UX. The list is alphabetical by identifier and not localized — America/Argentina/Buenos_Aires is not a label a human wants to scan. Either add a JS searchable select (Select2, Choices.js, Tom Select), or group by region with optgroup derived from the first path segment. The package gives you no grouping helpers, so this is on you.
The label is the identifier. There's no friendly-name/offset formatting beyond whatever the published config exposes. If you want "New York (GMT-5)", build it yourself from ->text plus Carbon::now($tz)->format('P') — and remember the offset changes with DST, so compute it at render time, never cache it.
Don't use it for validation. Use Laravel's native timezone rule (or Rule::in($timezones->pluck('value')) if you've already got the array) rather than re-deriving the list. One source of truth, validated server-side.
Extension point. The config file is the only supported customization surface; everything else means overriding the facade binding or writing your own Blade component. If you find yourself wanting more than the config offers, that is the signal to drop the package and keep a local Timezone helper class — you'll spend less time on it than on a fork.
How can I help you explore Laravel packages today?