Astrocal
Guides

Embeddable Widget

Drop-in booking widget for any website with one script tag.

Embeddable Widget

@astrocal/widget is a drop-in booking UI for any website. It renders a full calendar, time slot picker, and booking form inside a Shadow DOM, so it works on any page without CSS conflicts.

npm · GitHub

Installation

bash npm install @astrocal/widget
bash pnpm add @astrocal/widget
bash yarn add @astrocal/widget
<script src="https://cdn.astrocal.dev/widget/v1/astrocal.js"></script>

Quick Start

Open a booking modal with one call:

import { open } from "@astrocal/widget";

open({
  eventTypeId: "YOUR-EVENT-TYPE-ID",
  mode: "popup",
});

Inline Mode

Mount the widget into a container element:

import { open, destroy } from "@astrocal/widget";

// Mount
open({
  eventTypeId: "YOUR-EVENT-TYPE-ID",
  mode: "inline",
  target: "#booking-container",
});

// Clean up when done
destroy("#booking-container");

CDN (No Build Step)

For sites without a bundler, load the widget via a script tag:

<script src="https://cdn.astrocal.dev/widget/v1/astrocal.js"></script>
<script>
  Astrocal.open({
    eventTypeId: "YOUR-EVENT-TYPE-ID",
    mode: "popup",
  });
</script>

Or use auto-initialization with data attributes. No JavaScript required:

<script src="https://cdn.astrocal.dev/widget/v1/astrocal.js"></script>
<div data-astrocal-event-type-id="YOUR-EVENT-TYPE-ID" data-astrocal-mode="inline"></div>

Configuration

PropertyTypeDefaultDescription
eventTypeIdstringrequiredEvent type UUID to display
apiUrlstring"https://api.astrocal.dev"API base URL
mode"inline" | "popup""popup"Render mode
targetstring | HTMLElement-DOM element or CSS selector (inline mode)
timezonestringauto-detectedIANA timezone override
themeThemeConfig-CSS custom property overrides
colorScheme"light" | "dark" | "auto""auto"Color scheme
demobooleanfalseDemo mode (mock data, no API calls)
onBookingCreated(booking: BookingResult) => void-Booking success callback
onBookingRescheduled(booking: BookingResult) => void-Reschedule success callback (reschedule mode)
reschedule{ bookingId, token, currentStartTime? }-Reschedule an existing booking instead of creating one
onError(error: WidgetError) => void-Error callback
onClose() => void-Popup close callback

API

open(config)

Opens the booking widget. In popup mode, creates a modal overlay. In inline mode, mounts into the target element.

close()

Closes the popup widget if open. No-op if no popup is active.

destroy(target)

Destroys an inline widget mounted on a target element. Removes the Shadow DOM and cleans up all resources.

autoInit()

Scans the DOM for elements with data-astrocal-* attributes and mounts widgets automatically. Called automatically when using the CDN script tag.

Theming

Customize the widget appearance with CSS custom properties:

open({
  eventTypeId: "YOUR-EVENT-TYPE-ID",
  theme: {
    primaryColor: "#6366f1",
    primaryHoverColor: "#4f46e5",
    borderRadius: "12px",
    fontFamily: "Inter, sans-serif",
  },
});
PropertyCSS Custom PropertyDescription
primaryColor--astrocal-primaryPrimary action color
primaryHoverColor--astrocal-primary-hoverPrimary hover color
headingColor--astrocal-headingHeading text color
backgroundColor--astrocal-bgWidget background
textColor--astrocal-textBody text color
borderColor--astrocal-borderBorder color
borderFocusColor--astrocal-border-focusFocused border color
borderRadius--astrocal-radiusBorder radius
fontFamily--astrocal-fontFont family

The widget uses Shadow DOM isolation, so your site's styles won't affect it and vice versa.

Reschedule Mode

The widget can move an existing booking instead of creating a new one. Pass the booking's ID and its cancel token (both are in the booking response and in the confirmation email):

open({
  eventTypeId: "YOUR-EVENT-TYPE-ID",
  reschedule: {
    bookingId: "BOOKING-ID",
    token: "CANCEL-TOKEN",
    currentStartTime: "2026-09-10T14:00:00.000Z", // optional, shows the old time
  },
  onBookingRescheduled: (booking) => console.log("moved to", booking.start_time),
});

In reschedule mode the widget skips the duration selector and the details form. Picking a slot shows a confirm step with the current and new times and an optional reason, then calls POST /v1/bookings/{id}/reschedule?token=.... The onBookingCreated callback never fires in this mode.

Astrocal's own hosted pages at www.astrocal.dev/bookings/{id}/cancel and www.astrocal.dev/bookings/{id}/reschedule use this mode. Those are the links in every confirmation and reminder email, so you only need reschedule mode if you host your own booker experience.

The widget header shows where the meeting happens, taken from the event type's conferencing settings: "Zoom", "Google Meet", "Microsoft Teams", "Video call" for a custom link, or "In person" with the address. After booking, the confirmation screen shows a join link when the meeting was created, or the address for in-person events. If a video link is still being created (for example, a paid booking whose Zoom meeting is created after payment), the confirmation tells the booker the link will be in their email.

The custom meeting URL of an event type is never exposed before booking. GET /v1/public/event-types/{id} omits custom_meeting_url; the link arrives on the booking response as meeting_url.

SSR / Server-Side Rendering

The package is safe to import in Node.js (Next.js, Nuxt, etc.). The autoInit() function is guarded and will not run on the server. open() and destroy() require a browser environment. close() is a silent no-op on the server.

import { open } from "@astrocal/widget";

// Only call in browser context
if (typeof window !== "undefined") {
  open({ eventTypeId: "..." });
}

Using React? The @astrocal/react package wraps this widget with proper lifecycle management. Mount, unmount, and prop updates are handled automatically.

Next Steps

On this page