Timezone Detector
Timezone Detector automatically detects the user’s browser timezone and seamlessly converts all dates between UTC and the user’s local time across tables, forms, models, Carbon, and helpers. Store everything in UTC, display it in the user’s timezone — effortlessly and consistently.
Author:
Eslam reda Div
Documentation
- Features
- Requirements
- Installation
- How It Works
- Usage
- Using Outside Filament Panels
- Configuration
- API Reference
- FAQ
- Testing
- Credits
- License
Automatically detects the user's browser timezone and provides Facade, Helpers, Carbon macros, Filament table/form macros, and an Eloquent trait for seamless timezone conversion.
Store in UTC. Display in the user's local time. Automatically.
#Features
- Auto browser detection — JavaScript detects IANA timezone via
Intl.DateTimeFormatwith 10+ fallback methods - Livewire v4 / SPA compatible — uses
Livewire.interceptRequest(),wire:navigate, fetch/XHR interceptors, cross-tab BroadcastChannel sync - Middleware — captures timezone from header, cookie, query param, or form input → stores in session
- One-liner macros —
TextColumn::toUserTimezone(),TextEntry::toUserTimezone(),DateTimePicker::fromUserTimezone() - Carbon macros —
$date->toUserTimezone(),->toSystemTimezone(),->formatInUserTimezone() - Facade & helpers —
TimezoneDetector::forDisplay(),to_user_timezone(),user_now(), etc. - Eloquent trait —
InteractsWithTimezonefor per-attribute conversion - Fully configurable — toggle every feature on/off, customize header/cookie/session names
#Requirements
- PHP >= 8.2, Laravel >= 11.0, Filament >= 5.0
#Installation
composer require eslam-reda-div/filament-timezone-detector
Register the plugin in your panel provider:
use EslamRedaDiv\TimezoneDetector\TimezoneDetectorPlugin;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->plugin(TimezoneDetectorPlugin::make());
}
Optionally publish the config:
php artisan vendor:publish --tag="timezone-detector-config"
That's it. The plugin auto-loads JS detection, registers middleware, Carbon macros, and Filament macros.
#How It Works
Browser (JS) Server (Middleware) Your Code
───────────── ────────────────── ─────────
Intl.DateTimeFormat() detects → Reads X-Timezone header → Facade / Helper / Carbon macro
timezone and sends it via: (or cookie / query param) converts any datetime between
• Livewire interceptRequest Stores in session('user_tz') user ↔ system timezone
• fetch / XHR / Axios interceptors
• Cookie + hidden form inputs
• BroadcastChannel (cross-tab)
Recommended: Store all datetimes in UTC (config/app.php → 'timezone' => 'UTC'). The plugin handles display conversion automatically.
#Usage
#Filament Table Columns
TextColumn::make('created_at')->dateTime()->toUserTimezone(),
TextColumn::make('published_at')->dateTime('M d, Y h:i A')->toUserTimezone(),
TextColumn::make('updated_at')->since()->toUserTimezone(),
#Filament Infolist Entries
TextEntry::make('created_at')->dateTime()->toUserTimezone(),
TextEntry::make('published_at')->dateTime('M d, Y h:i A')->toUserTimezone(),
TextEntry::make('updated_at')->since()->toUserTimezone(),
#Filament Form Fields
DateTimePicker::make('starts_at')->toUserTimezone(), // display + save in user TZ
DateTimePicker::make('event_time')->fromUserTimezone(), // alias, clearer intent
DatePicker::make('event_date')->toUserTimezone(),
#Facade
use EslamRedaDiv\TimezoneDetector\Facades\TimezoneDetector;
// Info
TimezoneDetector::getUserTimezone(); // "America/New_York"
TimezoneDetector::getSystemTimezone(); // "UTC"
TimezoneDetector::getOffsetFromSystem(); // -4.0
TimezoneDetector::getUserUtcOffset(); // "-04:00"
// Convert to user TZ (for display)
TimezoneDetector::toUserTimezone($model->created_at); // Carbon
TimezoneDetector::toUserTimezone('2025-06-15 14:00:00', 'M d, Y h:i A'); // "Jun 15, 2025 10:00 AM"
TimezoneDetector::forDisplay($model->created_at); // formatted string
TimezoneDetector::diffForHumans($model->created_at); // "2 hours ago"
// Convert to system TZ (for storage)
TimezoneDetector::toSystemTimezone($request->starts_at); // Carbon in UTC
TimezoneDetector::forStorage($request->input('event_time')); // alias
TimezoneDetector::fromUserInput('06/15/2025 10:00 AM', 'm/d/Y h:i A'); // parse + convert
// Between any two TZs
TimezoneDetector::convertTimezone($datetime, 'UTC', 'Asia/Tokyo', 'H:i');
// Current time
TimezoneDetector::userNow(); // Carbon in user TZ
TimezoneDetector::systemNow(); // Carbon in system TZ
#Helper Functions
user_timezone(); // "America/New_York"
system_timezone(); // "UTC"
to_user_timezone($model->created_at, 'M d, Y h:i A'); // formatted in user TZ
to_system_timezone($userInput); // Carbon in UTC
convert_timezone($datetime, 'UTC', 'Asia/Tokyo'); // between any TZs
user_now(); // Carbon in user TZ
format_user_datetime($model->created_at); // display string
diff_for_humans_user($model->created_at); // "2 hours ago"
#Carbon Macros
$date->toUserTimezone(); // Carbon in user TZ
$date->toSystemTimezone(); // Carbon in system TZ
$date->formatInUserTimezone('M d, Y h:i A'); // formatted string
$date->diffForHumansInUserTimezone(); // "2 hours ago"
// Chaining with Eloquent
$model->created_at->toUserTimezone()->format('H:i');
$model->deadline->toUserTimezone()->isPast();
#Eloquent Model Trait
use EslamRedaDiv\TimezoneDetector\Concerns\InteractsWithTimezone;
class Event extends Model
{
use InteractsWithTimezone;
protected $casts = ['starts_at' => 'datetime', 'ends_at' => 'datetime'];
}
$event->toUserTimezone('starts_at'); // Carbon in user TZ
$event->getDatetimeForUser('starts_at', 'd/m/Y H:i'); // formatted string
$event->setDatetimeFromUser('starts_at', $input); // converts to UTC and sets
$event->diffForHumansInUserTimezone('starts_at'); // "in 3 days"
$event->convertAttributeTimezone('starts_at', 'Asia/Tokyo'); // any TZ
#Blade Views
{{ to_user_timezone($post->created_at, 'M d, Y h:i A') }}
{{ $post->created_at->formatInUserTimezone('M d, Y h:i A') }}
{{ diff_for_humans_user($post->published_at) }}
Your timezone: {{ user_timezone() }}
#Controllers
// Store: convert user input → UTC
$event->starts_at = TimezoneDetector::toSystemTimezone($request->starts_at);
// Display: convert UTC → user TZ
$display = TimezoneDetector::forDisplay($event->starts_at);
#API Resources
'starts_at_local' => TimezoneDetector::toUserTimezone($this->starts_at)?->toISOString(),
'starts_at_display' => TimezoneDetector::forDisplay($this->starts_at),
#Queued Jobs
The session is unavailable in jobs. Pass the timezone explicitly:
$userTz = user_timezone();
SendReminder::dispatch($event, $userTz);
// In the job:
$localTime = convert_timezone($this->event->starts_at, system_timezone(), $this->userTimezone, 'M d, Y h:i A');
#Using Outside Filament Panels
Register the middleware manually and include the JS:
// bootstrap/app.php (Laravel 11+)
use EslamRedaDiv\TimezoneDetector\Http\Middleware\DetectUserTimezone;
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [DetectUserTimezone::class]);
})
{{-- Option 1: Use FilamentAsset (recommended, no publishing needed) --}}
<script src="{{ \Filament\Support\Facades\FilamentAsset::getScriptSrc('timezone-detector', 'eslam-reda-div/filament-timezone-detector') }}"></script>
{{-- Option 2: Publish and use from public/ --}}
<script src="{{ asset('vendor/eslam-reda-div/filament-timezone-detector/timezone-detector.js') }}"></script>
For option 2, publish first: php artisan filament:assets
#Configuration
// config/timezone-detector.php
return [
'system_timezone' => config('app.timezone', 'UTC'),
'fallback_timezone' => config('app.timezone', 'UTC'),
'header_name' => env('TIMEZONE_DETECTOR_HEADER', 'X-Timezone'),
'cookie_name' => env('TIMEZONE_DETECTOR_COOKIE', 'user_timezone'),
'session_key' => env('TIMEZONE_DETECTOR_SESSION_KEY', 'user_timezone'),
'query_param' => env('TIMEZONE_DETECTOR_QUERY_PARAM', 'timezone'),
'auto_middleware' => env('TIMEZONE_DETECTOR_AUTO_MIDDLEWARE', true),
'register_carbon_macros' => env('TIMEZONE_DETECTOR_CARBON_MACROS', true),
'register_column_macros' => env('TIMEZONE_DETECTOR_COLUMN_MACROS', true),
'register_field_macros' => env('TIMEZONE_DETECTOR_FIELD_MACROS', true),
];
#Plugin Options (Per-Panel)
->plugin(TimezoneDetectorPlugin::make()) // default
->plugin(TimezoneDetectorPlugin::make()->withoutMiddleware()) // manual middleware
->plugin(TimezoneDetectorPlugin::make()->autoMiddleware(false)) // same as above
#API Reference
#Facade / Core Methods
| Method | Returns | Description |
|---|---|---|
getUserTimezone() |
string |
User's IANA timezone |
getSystemTimezone() |
string |
System/database timezone |
toUserTimezone($dt, $format?, $fromTz?) |
Carbon|string|null |
System → user TZ |
toSystemTimezone($dt, $format?, $toTz?) |
Carbon|string|null |
User → system TZ |
convertTimezone($dt, $from, $to, $format?) |
Carbon|string|null |
Any → any TZ |
userNow() / systemNow() |
Carbon |
Current time in user/system TZ |
forDisplay($dt, $format?) |
string|null |
Shorthand display format |
forStorage($dt, $format?) |
Carbon|string|null |
Shorthand for toSystemTimezone |
fromUserInput($dt, $format?) |
Carbon |
Parse user input → system TZ |
diffForHumans($dt) |
string|null |
"2 hours ago" in user TZ |
getOffsetFromSystem() |
float |
Offset in hours (e.g., -4.0) |
getUserUtcOffset() |
string |
UTC offset (e.g., "-04:00") |
isValidTimezone($tz) |
bool |
Validate IANA timezone |
getAvailableTimezones() |
array |
All IANA identifiers |
#Carbon Macros
| Method | Returns | Description |
|---|---|---|
->toUserTimezone() |
Carbon |
Copy in user TZ |
->toSystemTimezone() |
Carbon |
Copy in system TZ |
->formatInUserTimezone($format?) |
string |
Format in user TZ |
->diffForHumansInUserTimezone() |
string |
Diff for humans in user TZ |
#Filament Macros
| Target | Method |
|---|---|
TextColumn |
->toUserTimezone() |
TextEntry |
->toUserTimezone() |
DateTimePicker |
->toUserTimezone() / ->fromUserTimezone() |
DatePicker |
->toUserTimezone() |
#Model Trait (InteractsWithTimezone)
| Method | Returns |
|---|---|
->toUserTimezone('attr') |
Carbon|null |
->toSystemTimezone('attr') |
Carbon|null |
->getDatetimeForUser('attr', $format?) |
string|null |
->setDatetimeFromUser('attr', $value, $format?) |
static |
->diffForHumansInUserTimezone('attr') |
string|null |
->convertAttributeTimezone('attr', $tz, $format?) |
Carbon|string|null |
#Helper Functions
| Function | Returns |
|---|---|
user_timezone() |
string |
system_timezone() |
string |
to_user_timezone($dt, $format?, $fromTz?) |
Carbon|string|null |
to_system_timezone($dt, $format?, $toTz?) |
Carbon|string|null |
convert_timezone($dt, $from, $to, $format?) |
Carbon|string|null |
user_now() |
Carbon |
format_user_datetime($dt, $format?) |
string|null |
diff_for_humans_user($dt) |
string|null |
#FAQ
Q: UTC shows on first page load?
Expected — JS hasn't run yet. After the first request, the real timezone is detected and used for all subsequent requests. The fallback_timezone config covers this initial load.
Q: Works with Filament SPA mode / wire:navigate?
Yes. The JS listens to livewire:navigate and livewire:navigated events and injects X-Timezone via Livewire.interceptRequest(), fetch/XHR interceptors, and cookies.
Q: Works outside Filament panels? Yes. See Using Outside Filament Panels.
Q: Timezone in queued jobs? The session is unavailable in jobs. Pass it explicitly when dispatching. See Queued Jobs.
Q: Does this change PHP's global timezone?
No. It never calls date_default_timezone_set(). Only converts individual values when you call the conversion methods.
Q: Livewire v4 compatible?
Yes. The JS uses Livewire.interceptRequest() (the new v4 API) with a fallback to Livewire.hook('request') for backward compatibility.
#Testing
composer test
#Credits
#License
MIT — see LICENSE.md
The author
From the same author
Featured Plugins
A selection of plugins curated by the Filament team
Custom Dashboards
Let your users build and share their own dashboards with a drag-and-drop interface. Define your data sources in PHP and let them do the rest.
Filament
Advanced Tables (formerly Filter Sets)
Supercharge your tables with powerful features like user-customizable views, quick filters, multi-column sorting, advanced table searching, convenient view management, and more. Compatible with Resource Panel Tables, Relation Managers, Table Widgets, and Table Builder!
Kenneth Sese
Custom Fields
Eliminate custom field migrations forever. Let your users create and manage form fields directly in Filament admin panels with 20+ built-in field types, validation, and zero database changes.
Relaticle