Lightweight, zero-dependency Add to Calendar web component. Shadow DOM, Apache-2.0, 19 KB minified (11 KB slim).
Three ways to use it:
- Web component —
<twist-cal>renders a styled dropdown button - Declarative triggers —
data-twistcalattributes on any element - Programmatic API — import the URL generators and ICS builder directly
npm install @freshjuice/twistcalOr load from a CDN — no build step required:
<script src="https://cdn.jsdelivr.net/npm/@freshjuice/twistcal/dist/twistcal.min.js"></script>| File | Format | Size | Icons | Languages |
|---|---|---|---|---|
dist/twistcal.min.js |
IIFE | 19 KB | Yes | All 12 |
dist/twistcal.slim.min.js |
IIFE | 11 KB | No | All 12 |
dist/twistcal.esm.min.js |
ESM | 19 KB | Yes | All 12 |
dist/twistcal.slim.esm.min.js |
ESM | 11 KB | No | All 12 |
dist/twistcal.{lang}.min.js |
IIFE | 17 KB | Yes | Single lang |
dist/twistcal.{lang}.slim.min.js |
IIFE | 9 KB | No | Single lang |
CDN (self-executing, no build step):
<!-- Full build with icons -->
<script src="https://cdn.jsdelivr.net/npm/@freshjuice/twistcal/dist/twistcal.min.js"></script>
<!-- Slim build, no icons -->
<script src="https://cdn.jsdelivr.net/npm/@freshjuice/twistcal/dist/twistcal.slim.min.js"></script>
<!-- Single language (e.g. German only) -->
<script src="https://cdn.jsdelivr.net/npm/@freshjuice/twistcal/dist/twistcal.de.min.js"></script>Drop <twist-cal> on the page. It renders a button with a dropdown menu. All styles live in a Shadow DOM root — no bleed in either direction.
<script src="https://cdn.jsdelivr.net/npm/@freshjuice/twistcal/dist/twistcal.min.js"></script>
<twist-cal
title="Product Launch Webinar"
start="2026-02-10T14:00:00"
end="2026-02-10T15:30:00"
location="Online"
description="Join us for the launch of TwistCal v1.">
</twist-cal>| Attribute | Required | Description |
|---|---|---|
title |
yes | Event title (calendar subject) |
start |
yes | Start datetime — any value new Date() understands |
end |
yes | End datetime |
description |
no | Event description (calendar body) |
location |
no | Event location |
url |
no | Event URL (included in .ics) |
timezone |
no | IANA timezone (e.g. Europe/Oslo). When set, naive start/end are interpreted as wall-clock in that zone, converted to UTC. DST-aware. Ignored if start/end already carry an offset. |
label |
no | Button text. Default: auto-detected from lang or Add to Calendar |
lang |
no | Language code (en, de, es, fr, it, pt, nl, pl, uk, ru, ja, zh). Auto-detected from <html lang> or navigator.language if omitted. |
variant |
no | Button style: solid (default) or outline (white bg, dark border, dark text) |
show-icons |
no | true (default) or false — show/hide brand icons in dropdown |
calendars |
no | Comma-separated list of calendars to show, in order: google,outlook,yahoo,ics. Default: all four. Aliases: ics or ical both work. Also accepts services as an alias. |
Show only the calendars you want, in the order you want:
<!-- Google + Apple Calendar only -->
<twist-cal calendars="google,ics" title="..." start="..." end="...">
</twist-cal>
<!-- Single calendar — effectively a download button -->
<twist-cal calendars="ics" label="Download .ics" title="..." start="..." end="...">
</twist-cal>For long descriptions or structured data, pass event as JSON inside the element:
<twist-cal label="Add to my calendar">
<script type="application/json">
{
"title": "Quarterly Review — Q1 2026",
"start": "2026-03-15T09:00:00",
"end": "2026-03-15T10:00:00",
"location": "Conference Room B, 4th Floor",
"description": "Bring your Q1 metrics.\nDial-in: +1-555-0100, code 4521#",
"url": "https://twistcal.com/demo"
}
</script>
</twist-cal>The component is style-isolated by Shadow DOM. Override CSS custom properties on the host:
twist-cal {
--tc-btn-bg: #dc2626;
--tc-btn-bg-hover: #b91c1c;
--tc-radius: 4px;
}Button:
| Property | Default | Description |
|---|---|---|
--tc-btn-bg |
#2563eb |
Button background |
--tc-btn-bg-hover |
#1d4ed8 |
Button background on hover |
--tc-btn-border |
none |
Button border (use for custom variants) |
--tc-btn-padding |
8px 16px |
Button padding |
--tc-btn-fs |
14px |
Button font size |
--tc-btn-fw |
600 |
Button font weight |
--tc-btn-gap |
6px |
Gap between label and caret |
--tc-text |
#fff |
Button text color |
--tc-border |
#111 |
Outline variant border color |
--tc-caret-size |
4px |
Caret triangle size |
Menu:
| Property | Default | Description |
|---|---|---|
--tc-menu-bg |
#fff |
Menu background |
--tc-menu-border |
#e5e7eb |
Menu border color |
--tc-menu-shadow |
0 4px 16px rgba(0,0,0,0.12) |
Menu box shadow |
--tc-menu-padding |
4px |
Menu inner padding |
--tc-menu-min-w |
200px |
Menu minimum width |
--tc-menu-gap |
4px |
Gap between button and menu |
Menu items:
| Property | Default | Description |
|---|---|---|
--tc-item-color |
#1f2937 |
Item text color |
--tc-item-hover-bg |
#f3f4f6 |
Item hover background |
--tc-item-padding |
8px 12px |
Item padding |
--tc-item-fs |
14px |
Item font size |
--tc-item-gap |
8px |
Gap between icon and text |
--tc-item-radius |
6px |
Item border radius |
--tc-icon-size |
16px |
Icon size |
Shared:
| Property | Default | Description |
|---|---|---|
--tc-radius |
8px |
Button + menu border radius |
--tc-font |
system stack | Font family |
--tc-focus |
2px solid #60a5fa |
Focus ring outline |
--tc-focus-offset |
2px |
Focus ring offset |
Use your own buttons, links, or divs. Add data-twistcal to any element and it becomes a calendar trigger — no JS needed. Auto-initializes on DOMContentLoaded.
<script src="https://cdn.jsdelivr.net/npm/@freshjuice/twistcal/dist/twistcal.min.js"></script>
<!-- Google Calendar -->
<button data-twistcal
data-twistcal-action="google"
data-twistcal-title="Team Sync"
data-twistcal-start="2026-01-15T10:00:00"
data-twistcal-end="2026-01-15T11:00:00"
data-twistcal-location="Room A">
Add to Google
</button>
<!-- Outlook -->
<a href="#" data-twistcal
data-twistcal-action="outlook"
data-twistcal-title="Team Sync"
data-twistcal-start="2026-01-15T10:00:00"
data-twistcal-end="2026-01-15T11:00:00">
Add to Outlook
</a>
<!-- Download .ics -->
<button data-twistcal
data-twistcal-action="ics"
data-twistcal-title="Team Sync"
data-twistcal-start="2026-01-15T10:00:00"
data-twistcal-end="2026-01-15T11:00:00">
Download .ics
</button>| Attribute | Required | Description |
|---|---|---|
data-twistcal |
yes | Marks the element as a trigger |
data-twistcal-action |
yes | google | outlook | yahoo | ics |
data-twistcal-title |
yes | Event title |
data-twistcal-start |
yes | Start datetime |
data-twistcal-end |
yes | End datetime |
data-twistcal-description |
no | Event description |
data-twistcal-location |
no | Event location |
data-twistcal-url |
no | Event URL (.ics only) |
data-twistcal-timezone |
no | IANA timezone |
If you add triggers dynamically (e.g. after an AJAX load), call autoInit on the new container:
import { autoInit } from '@freshjuice/twistcal';
// After injecting HTML:
autoInit(document.getElementById('new-content'));Or bind a single element with an explicit event object (skips attribute parsing):
import { bindTrigger } from '@freshjuice/twistcal';
const event = { title: 'Team Sync', start: '2026-01-15T10:00:00', end: '2026-01-15T11:00:00' };
bindTrigger(document.getElementById('my-btn'), 'google', event);Full control — generate URLs or ICS strings yourself, wire them to anything:
import {
createButton,
generateICS,
googleUrl,
outlookUrl,
yahooUrl,
downloadICS,
detectLanguage,
getTranslation,
supportedLanguages
} from '@freshjuice/twistcal';
const event = {
title: 'Coffee with the Team',
start: '2026-01-20T11:00:00',
end: '2026-01-20T11:45:00',
location: 'Blue Bottle, 3rd St',
description: 'Casual sync. No agenda.'
};
// Mount a full dropdown button
createButton(document.getElementById('mount'), event);
// Get a deep link URL string
const url = googleUrl(event); // → "https://calendar.google.com/calendar/render?..."
window.open(url, '_blank', 'noopener');
// Generate .ics string
const ics = generateICS(event); // → "BEGIN:VCALENDAR\r\n..."
// Or download .ics directly
downloadICS(event);Drop the icons (~8 KB) if you don't need them:
import { createButton } from '@freshjuice/twistcal/slim';| Export | Signature | Returns |
|---|---|---|
createButton |
(target: HTMLElement, event: object) => HTMLElement |
Mounted <twist-cal> element |
bindTrigger |
(el: HTMLElement, action: string, event?: object) => void |
Click handler bound |
autoInit |
(scope?: HTMLElement | Document) => void |
Wires all [data-twistcal] in scope |
generateICS |
(event: object) => string | null |
RFC 5545 ICS string |
googleUrl |
(event: object) => string | null |
Google Calendar compose URL |
outlookUrl |
(event: object) => string | null |
Outlook compose URL |
yahooUrl |
(event: object) => string | null |
Yahoo Calendar compose URL |
downloadICS |
(event: object) => void |
Triggers .ics file download |
detectLanguage |
(configLang?: string) => string |
Auto-detect language code |
getTranslation |
(lang: string) => object |
Translation for a language |
supportedLanguages |
string[] |
List of supported language codes |
TwistCal ships with 12 languages: English, German, Spanish, French, Italian, Portuguese, Dutch, Polish, Ukrainian, Russian, Japanese, Chinese.
Language is auto-detected in this priority:
langattribute on<twist-cal>(e.g.lang="de")<html lang="...">attributenavigator.language- English fallback
<!-- Force German -->
<twist-cal lang="de" title="..." start="..." end="...">
</twist-cal>
<!-- Auto-detect from <html lang="ru"> -->
<twist-cal title="..." start="..." end="...">
</twist-cal>For single-language builds, use dist/twistcal.{lang}.min.js — only that language is bundled, saving ~2 KB.
Pass datetimes in ISO 8601. With a timezone offset (2026-02-10T14:00:00Z or 2026-02-10T14:00:00-05:00) the calendar services get an absolute time. Without one (2026-02-10T14:00:00) the browser treats it as local time and converts to UTC — usually what you want for a page visited from multiple time zones.
When the event happens in a specific timezone (not the visitor's), pass the IANA name:
<twist-cal
title="Oslo Team Sync"
start="2026-01-15T10:00:00"
end="2026-01-15T11:00:00"
timezone="Europe/Oslo">
</twist-cal>A visitor in Miami clicks the button — their Google Calendar gets 20260115T090000Z (09:00 UTC = 10:00 Oslo = 04:00 Miami). DST is handled via Intl.DateTimeFormat, so the offset is correct for the event's date, not the visitor's current date.
If start/end already carry an offset (Z or +05:00), the timezone attribute is ignored — the explicit offset wins.
| Service | Method | Notes |
|---|---|---|
| Google Calendar | Deep link | Opens calendar.google.com/calendar/render compose view |
| Microsoft Outlook | Deep link | Opens outlook.live.com/calendar/0/deeplink/compose |
| Yahoo Calendar | Deep link | Opens calendar.yahoo.com compose view |
| Apple Calendar | .ics download |
RFC 5545 ICS file, downloaded via Blob URL |
The .ics format also works with any calendar app that supports iCalendar (Thunderbird, Proton Calendar, Notion Calendar, etc.).
npm run dev
# open http://localhost:8080/demo/Modern browsers (Chrome 54+, Firefox 63+, Safari 10.1+). Uses Custom Elements v1, Shadow DOM v1, and ES modules. No polyfills shipped — the web is the platform.
Apache-2.0 © FreshJuice
