Timezone Detector plugin screenshot
Dark mode ready
Multilingual support
Supports v5.x

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.

Tags: Forms Form Field Infolist Entry Tables Table Column Action
Supported versions:
5.x
Eslam reda Div avatar Author: Eslam reda Div

Documentation

Latest Version on Packagist Total Downloads 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.DateTimeFormat with 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 macrosTextColumn::toUserTimezone(), TextEntry::toUserTimezone(), DateTimePicker::fromUserTimezone()
  • Carbon macros$date->toUserTimezone(), ->toSystemTimezone(), ->formatInUserTimezone()
  • Facade & helpersTimezoneDetector::forDisplay(), to_user_timezone(), user_now(), etc.
  • Eloquent traitInteractsWithTimezone for 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