hash-calendar is a privacy-first, client-only calendar.
Your data lives in the URL hash, so sharing is just copying a link.
Live site: https://hash-calendar.netlify.app/
GitHub: https://github.com/supunlakmal/hash-calendar
- Multi-language UI (English, Sinhala, Tamil, Italian)
- Mobile drawer menu with quick icon actions
- Timeline view with zoom controls and minimap navigation
- Command palette (
Ctrl/Cmd+K) and dedicated event search modal - Starter template gallery for opening prebuilt schedules
- Optional browser notifications for upcoming events
- World Planner modal (multi-timezone planning grid with scrubber and 12h/24h toggle)
- App launcher menu in the top bar
- PWA support (manifest + service worker cache)
- JSON bridge page (
json.html) for raw JSON -> hash URL redirect - Direct-link event creation from human-readable URL paths
- Day, week, month, year, agenda, and timeline views
- Create, edit, delete, recurring, all-day, and cross-day timed events
- Color palette support and editable calendar title
- Focus mode overlay with timer and upcoming list
- "Up Next" countdown widget
- Timeline controls: zoom in/out, zoom slider, minimap scrubber, and "Center Today"
- Command palette (
Ctrl/Cmd+K) for quick actions and view switching - Event search modal with keyboard navigation
- Advanced event filters: date range, recurrence, color, and timezone
- Template gallery modal with curated ready-to-use calendar links
- Dedicated read-only toggle (switch between edit and read-only)
- In read-only mode, edit-heavy controls are hidden on desktop and mobile:
- Add event actions
- Add world-clock/timezone actions
- Share and export section
- Danger zone section
- Read-only state is saved in the URL hash (
s.r) so shared links open with the same mode
- URL-hash state with LZ-String compression
- Copy-link and QR share (with URL length guard)
- View/copy JSON and raw hash from modal
- Export JSON
- Import
.ics(supports daily/weekly/monthly/yearly RRULE frequency mapping) - One-time import from hash-path URLs, then auto-save to compressed hash
- World Clock sidebar with saved zones
- Timezone search by city/region or UTC offset
- World Planner with timezone comparison grid
- Optional password lock using AES-GCM + PBKDF2 (150k iterations, SHA-256)
- Encrypted links start with
#ENC: - No backend, no accounts, no server-side storage
- Analytics is configured to avoid sending URL hash data
- Theme toggle (light/dark)
- Week start toggle (Sunday/Monday)
- Optional browser notifications for upcoming events
- URL length meter + warning
- Mobile hamburger drawer + quick action buttons
- Installable PWA with offline caching of app assets
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
Open index.html directly, or run a local server:
npx serve .Alternative:
python -m http.server 8080docker compose up --buildDefault ports are 80 and 443 (override with HOST_PORT_HTTP / HOST_PORT_HTTPS).
Install dependencies and browsers once:
npm install
npm run test:e2e:installRun the E2E suite:
npm run test:e2eUseful local modes:
npm run test:e2e:chromium
npm run test:e2e:mobile
npm run test:e2e:cross
npm run test:e2e:headed
npm run test:e2e:ui
npm run test:e2e:debug
npm run test:e2e:reportNotes:
test:e2eruns all configured Playwright projects.- Firefox/WebKit run the cross-browser smoke suite (
tests/e2e/smoke.cross.e2e.spec.js). - Full feature coverage runs on Chromium desktop, plus mobile-focused coverage on the mobile Chromium project.
Use json.html to pass JSON payloads via query params (json, data, state, payload) and auto-redirect to a compressed hash URL.
You can create events directly from hash-path URLs. Use a hash-path beginning with /, for example:
#/2025/12/25/10/30/Open-Presents
Supported patterns:
| Pattern | Scenario | Example |
|---|---|---|
YYYY/MM/DD/HH/mm/Title |
Full precision | #/2025/12/25/10/30/Open-Presents |
YYYY/MM/DD/Title |
All-day | #/2025/12/25/Christmas-Day |
YYYY/MM/DD/HH/mm+Minutes/Title |
Timed with explicit duration | #/2025/12/25/10/00+90/Family-Brunch |
Event1,Event2 |
Multi-event in one link | #/2025/01/01/Gym,2025/01/01/10/00/Clean |
Behavior notes:
- URL segments are decoded (
decodeURIComponent), and dashes in titles become spaces. - If title is missing, it falls back to
New Event (URL). - Parsed events are appended once, then the hash-path is cleaned and state is persisted as the normal compressed URL hash.
- State is serialized to JSON.
- JSON is compacted and compressed into the URL hash.
- Optional password encryption wraps the compressed payload.
- Opening the link restores the full calendar state in-browser.
If there is no non-default calendar data (events, saved timezones, or changed settings), the hash is cleared automatically.
Example (before compression):
{
"t": "Acme Schedule",
"c": { "1": "2ecc71", "3": "e74c3c" },
"e": [
[28930080, 30, "Daily standup", 1, "d"],
[28930200, 0, "Launch day", 3],
[28930800, 90, "Design review"]
],
"s": { "d": 1, "m": 1, "v": "week", "l": "en", "r": 1, "n": 1 },
"mp": {
"h": "America/Los_Angeles",
"z": ["UTC", "Asia/Tokyo", "Europe/London"],
"s": 1769721600000,
"d": "2026-01-30",
"f24": 1
}
}Key fields:
t: titlec: color overrides by palette index (hex without#)e: events as[startMin, duration, title, colorIndex?, recurrence?]s: settings (dtheme,mweek start,vlast view,llanguage,rread-only mode,nnotifications toggle)mp: optional world-time snapshot (hhome zone,zsaved zones,sscrubber timestamp,dplanner date,f2424-hour mode)
Recurrence values: d (daily), w (weekly), m (monthly), y (yearly)
Legacy compatibility: older payloads with top-level z / timezones / tz are still read and migrated into mp.z.
index.html- main app UIscript.js- app state, events, rendering orchestrationjson.html- JSON payload bridge pagestyles.css,styles-i18n.css,styles-logo.css- stylessw.js,manifest.json- PWA setuplocales/- translation dictionariesmodules/calendarRender.js- month/week/day/year renderingagendaRender.js- agenda renderingtimelineRender.js- timeline rendering and event lane layoutconstants.js- shared app constantscacheElements.js- centralized DOM element lookuprecurrenceEngine.js- recurrence expansioncountdownManager.js- "Up Next" widgeteventSearchModule.js- event search modal controllerfocusMode.js- focus overlaymodalManager.js- reusable password/JSON modal controllersstateSaveManager.js- debounced state persistence orchestrationresponsiveFeatures.js- mobile drawer and responsive helperstimezoneManager.js- timezone helpersworldPlannerModule.js- world planner modal logicqrCodeManager.js- QR modal logicicsImporter.js-.icsparsertemplateGallery.js- template gallery modal + link handlingtemplate-gallery-links.json- built-in starter templateshashcalUrlManager.js- compact/read/write hash statecryptoManager.js- encryption/decryption helpersi18n.js- localization engineapp_launcher.js- app launcher menupathImportManager.js- one-time hash-path import + rewrite logicurlPathEventParser.js- readable URL path parser for event creationlz-string.min.js- compression library
demo/- screenshot assetstests/e2e/- Playwright end-to-end coverage for desktop/mobile/cross-browser flowsDockerfile,docker-compose.yaml,Caddyfile- containerized deployment
MIT. See LICENSE.












