claude/skills/js-timezone-best-practices/SKILL.md
JavaScript/TypeScript timezone handling best practices, focusing on JST(UTC+9) <-> UTC conversion. Use this skill whenever the user is working with timezone conversion in JS/TS, dealing with Date object timezone issues, implementing DateRangePicker or date input components that need TZ-aware handling, debugging "9 hours off" bugs, using toLocaleString or Intl.DateTimeFormat with timezone concerns, or storing/retrieving dates between browser and DB. Also trigger when you see anti-patterns like `new Date(year, month, day)` used for cross-timezone scenarios, `setHours`/`setMinutes` for timezone conversion, or `toLocaleString` without a `timeZone` option. Even if the user doesn't mention "timezone" explicitly, if they're dealing with date/time mismatches between client and server, or dates shifting by hours when saved to DB, this skill applies.
npx skillsauth add lilpacy/dotfiles js-timezone-best-practicesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Date internally holds a UTC timestamp (ms since 1970-01-01T00:00:00Z), but many constructors and methods implicitly use the browser's local timezone. The same code produces different UTC values depending on the user's TZ setting:
// JST browser: 2026-01-20T12:00:00.000Z
// UTC browser: 2026-01-20T21:00:00.000Z
new Date(2026, 0, 20, 21, 0).toISOString();
This is the root cause of "9 hours off" bugs in JST-targeted services.
Scope note: JST is a fixed +9 offset with no daylight saving time, so the hour - 9 / + 9h arithmetic below is safe. For timezones with DST, use Intl.DateTimeFormat instead of fixed offset math.
Date.UTC — never new Date(y, m, d, h, min) for cross-TZ scenariosgetUTC* methods — never getHours(), getDate(), etc.timeZone option — never bare toLocaleString("ja-JP")When a user picks "2026-01-20 21:00" in a DateRangePicker, that's JST. Convert to UTC:
const JST_OFFSET_HOURS = 9;
function createDateFromJST(
year: number,
month: number, // 0-11
day: number,
hour: number,
minute: number
): Date {
// hour - 9 can go negative; Date.UTC handles underflow automatically
const ms = Date.UTC(year, month, day, hour - JST_OFFSET_HOURS, minute, 0, 0);
return new Date(ms);
}
// createDateFromJST(2026, 0, 20, 21, 0).toISOString()
// => "2026-01-20T12:00:00.000Z"
Date.UTC interprets arguments as UTC regardless of browser TZ. The -9 converts JST intent to UTC.
When populating a DateRangePicker's initial value from a DB-stored UTC date:
const JST_OFFSET_MS = 9 * 60 * 60 * 1000;
function getJSTComponents(date: Date) {
const jstMs = date.getTime() + JST_OFFSET_MS;
const jst = new Date(jstMs);
return {
year: jst.getUTCFullYear(),
month: jst.getUTCMonth(), // 0-11
day: jst.getUTCDate(),
hour: jst.getUTCHours(),
minute: jst.getUTCMinutes(),
};
}
The key: after shifting by +9h, read with getUTC* methods so the browser's local TZ never enters the picture.
Generalizable alternative using Intl.DateTimeFormat.formatToParts (works for any timezone, including DST zones):
function getZonedComponents(date: Date, timeZone: string) {
const dtf = new Intl.DateTimeFormat("en-CA", {
timeZone,
year: "numeric", month: "2-digit", day: "2-digit",
hour: "2-digit", minute: "2-digit", hour12: false,
});
const parts = dtf.formatToParts(date);
const map = Object.fromEntries(parts.map(p => [p.type, p.value]));
return {
year: Number(map.year),
month: Number(map.month) - 1, // 0-11
day: Number(map.day),
hour: Number(map.hour),
minute: Number(map.minute),
};
}
function formatDateTimeAsJST(date: Date): string {
return date.toLocaleString("ja-JP", {
timeZone: "Asia/Tokyo",
year: "numeric", month: "2-digit", day: "2-digit",
hour: "2-digit", minute: "2-digit", hour12: false,
});
}
// formatDateTimeAsJST(new Date("2026-01-20T12:00:00.000Z"))
// => "2026/01/20 21:00"
The timeZone: "Asia/Tokyo" makes the output deterministic regardless of browser TZ.
toLocaleString is for display only. For persistence or comparison, use toISOString() or epoch ms.
// BAD: result depends on browser TZ
const date = new Date(2026, 0, 20, 21, 0);
date.setHours(21);
Fix: use Date.UTC as shown above.
// BAD: fragile, breaks in non-JST environments
const localDate = new Date("2026-01-20T21:00:00"); // local TZ interpretation!
const utcDate = new Date(localDate.getTime() - 9 * 60 * 60 * 1000);
The string "2026-01-20T21:00:00" (no Z, no offset) is parsed as local time — already environment-dependent. Subtracting 9h on top of that only works if the browser happens to be in JST.
Fix: use Date.UTC from the start, or always include timezone in ISO strings (Z or +09:00).
timeZone in display// BAD: shows different times for users in different TZs
date.toLocaleString("ja-JP"); // no timeZone option
Fix: always pass timeZone: "Asia/Tokyo" (or the appropriate zone).
// DANGEROUS: no TZ indicator, parsed as local time
new Date("2026-01-20T21:00:00");
// SAFE: explicit TZ
new Date("2026-01-20T21:00:00Z"); // UTC
new Date("2026-01-20T21:00:00+09:00"); // JST
JST 00:00-08:59 maps to the previous UTC day. This is where most bugs hide:
| JST Input | Expected UTC | Why it matters | |---------------------|-----------------------|-----------------------------| | 2026/01/21 00:00 | 2026/01/20 15:00 | Date rolls back | | 2026/01/21 08:59 | 2026/01/20 23:59 | Just before same-day cutoff | | 2026/01/21 09:00 | 2026/01/21 00:00 | Same-day boundary | | 2026/01/01 00:00 | 2025/12/31 15:00 | Year rolls back | | 2024/02/29 00:00 | 2024/02/28 15:00 | Leap year + day rollback |
When implementing timezone conversion, write tests covering these boundaries. The Date.UTC underflow handling makes them pass naturally, but explicit tests prevent regressions.
User Input (JST) --[Date.UTC, hour-9]--> Date (UTC internally) --[save]--> DB (UTC)
DB (UTC) --[fetch]--> Date (UTC internally) --[timeZone:"Asia/Tokyo"]--> Display (JST)
Keep this flow unidirectional. Never mix local-TZ methods into the pipeline.
The Temporal API (TC39 stage 3) will eventually replace Date with explicit timezone-aware types like Temporal.ZonedDateTime. For new projects where Temporal is available, prefer it over Date. Until then, the patterns above are the safest approach.
data-ai
ユーザー向けの回答、要件整理、説明、計画、レビュー、仕様、要約を作成するときに使う。自然言語だけでは条件、状態、多重度、期間、時刻、境界値、制約、推論、計算が曖昧になりうる場合、表、デシジョンテーブル、Mermaid 図、計算式、境界値表で一意化する。推測で補った図表要素は必ず明示する。
tools
Cross-agent messaging via SQLite. Send messages between Claude Code, Codex, Gemini CLI, and other agents. No daemon, no network, no dependencies beyond bash and sqlite3.
development
Anti-AI-slop design skill for greenfield pages, audits, redesigns, and design extraction from URLs or screenshots. Use when the user asks to build a new app or landing page, wants to redesign something, invokes Hallmark by name, or uses audit/redesign/study.
development
Reviews animation and motion code against a high craft bar derived from Emil Kowalski's design engineering philosophy. Default to flagging; approval is earned.