Platforms
React, Next.js, Vue, Svelte and Astro
Embed the BookingsXP widget with the React, Vue 3, Svelte 5 and Astro components, including Next.js App Router usage, event callbacks and baseUrl.
The framework packages are thin wrappers around the same <bookingsxp-widget> element described in Embed attributes. Each one renders the element on the server, adds the loader script once on the client, and turns the widget's DOM events into props or emits. The props are the attributes in camelCase.
| Framework | Package | Import |
|---|---|---|
| React 18+ and Next.js | @bookingsxp/react | import { BookingsXPWidget, useBookingsXP } from "@bookingsxp/react" |
| Vue 3 and Nuxt | @bookingsxp/vue | import { BookingsXPWidget } from "@bookingsxp/vue" |
| Svelte 5 and SvelteKit | @bookingsxp/svelte | import BookingsXPWidget from "@bookingsxp/svelte" |
| Astro | @bookingsxp/astro | import BookingsXPWidget from "@bookingsxp/astro/BookingsXPWidget.astro" |
Release status (September 2026): the
@bookingsxp/*packages are being published to npm. Until they are, use the loader script and the<bookingsxp-widget>element directly — it works in React, Vue, Svelte, Astro and plain HTML, and every prop below is also an attribute.
The loader draws the widget inside the element's shadow root, so hydration never sees children it did not render and there are no hydration warnings.
Shared props#
| Prop | Type | Attribute |
|---|---|---|
widget | string | widget |
bookingUrl | string | booking-url |
mode | "inline" | "popup" | "floating" | mode |
buttonText | string | button-text |
template | "classic" | "compact" | "minimal" | "split" | "week" | "stepper" | "cards" | template |
theme | "light" | "dark" | "auto" | theme |
accent | string | accent |
radius | number | radius |
service | string | service |
staff | string | staff |
locale | string | locale |
prefill | { name?, email?, phone?, notes? } | name, email, phone, notes |
redirectUrl | string | redirect-url |
minHeight | number | min-height |
lazy | boolean | lazy |
hideHeader | boolean | hide-branding-header |
baseUrl | string | base-url |
baseUrl is only for self-hosting or local development. It sets where the loader script and the widget come from, for example http://localhost:3000. Leave it out in production; the default is https://bookingsxp.com.
React#
npm install @bookingsxp/reactimport { BookingsXPWidget } from "@bookingsxp/react";
export function BookingSection() {
return (
<BookingsXPWidget
widget="w_8fk2m1qz"
template="compact"
prefill={{ email: "alex@example.com" }}
onEvent={(e) => console.log(e.name)}
onBooked={(e) => console.log("Booked", e.booking?.id, e.slot?.start)}
/>
);
}BookingsXPWidget also accepts className, style and id. onEvent receives every event from this widget; onBooked receives booking_completed only.
A popup or floating button:
<BookingsXPWidget widget="w_8fk2m1qz" mode="popup" buttonText="Book a call" accent="#4f46e5" />
<BookingsXPWidget widget="w_8fk2m1qz" mode="floating" buttonText="Book now" accent="#4f46e5" />useBookingsXP#
The hook loads the script and subscribes to events from every widget on the page. Use it to open a popup from your own button, or to react to a booking anywhere in the tree.
import { useBookingsXP } from "@bookingsxp/react";
export function BookDemoButton() {
const { ready, open, lastEvent } = useBookingsXP({
event: "booking_completed",
onEvent: (e) => console.log("Booked", e.booking?.id),
});
return (
<>
<button type="button" disabled={!ready} onClick={() => open({ widget: "w_8fk2m1qz", service: "demo" })}>
Book a demo
</button>
{lastEvent ? <p>Thanks, your reference is {lastEvent.booking?.id}.</p> : null}
</>
);
}| Option | Default | |
|---|---|---|
event | "*" | One event name, or "*" for all. |
onEvent | none | Called for each matching event. |
baseUrl | https://bookingsxp.com | As above. |
It returns { api, ready, lastEvent, open }: api is window.BookingsXP once loaded, ready is true from then on, lastEvent is the most recent matching event, and open(options) opens a popup (it returns null until ready).
Next.js App Router#
The package is marked "use client", so you can render BookingsXPWidget from a Server Component as long as you pass no functions to it:
// app/book/page.tsx (a Server Component)
import { BookingsXPWidget } from "@bookingsxp/react";
export default function BookPage() {
return <BookingsXPWidget widget="w_8fk2m1qz" />;
}Event callbacks are functions, which cannot cross from a Server Component to a Client Component. Put them in a small client component:
// app/book/booking-widget.tsx
"use client";
import { BookingsXPWidget } from "@bookingsxp/react";
import { useRouter } from "next/navigation";
export function BookingWidget() {
const router = useRouter();
return (
<BookingsXPWidget
widget="w_8fk2m1qz"
onBooked={(e) => router.push(`/thank-you?ref=${encodeURIComponent(e.booking?.id ?? "")}`)}
/>
);
}The element renders during SSR and the script is added after hydration, so there is nothing to configure for streaming or static export. In the Pages Router the same component works unchanged.
Vue 3#
npm install @bookingsxp/vue<script setup lang="ts">
import { BookingsXPWidget, type WidgetEvent } from "@bookingsxp/vue";
function onBooked(e: WidgetEvent) {
console.log("Booked", e.booking?.id);
}
</script>
<template>
<BookingsXPWidget
widget="w_8fk2m1qz"
template="compact"
:prefill="{ email: 'alex@example.com' }"
@event="(e) => console.log(e.name)"
@booked="onBooked"
/>
</template>The component emits event for every widget event and booked for booking_completed. It is SSR-safe, so it works in Nuxt without ClientOnly. The package also exports loadEmbedScript if you want window.BookingsXP for open().
If you also write <bookingsxp-widget> directly in your own templates, Vue warns that it cannot resolve the component. Tell the compiler it is a custom element:
// vite.config.ts
import vue from "@vitejs/plugin-vue";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
vue({
template: { compilerOptions: { isCustomElement: (tag) => tag === "bookingsxp-widget" } },
}),
],
});Svelte 5#
npm install @bookingsxp/svelte<script lang="ts">
import BookingsXPWidget from "@bookingsxp/svelte";
import type { WidgetEvent } from "@bookingsxp/svelte";
let reference = $state<string | null>(null);
function onbooked(e: WidgetEvent) {
reference = e.booking?.id ?? null;
}
</script>
<BookingsXPWidget
widget="w_8fk2m1qz"
template="compact"
onevent={(e) => console.log(e.name)}
{onbooked}
/>
{#if reference}
<p>Thanks, your reference is {reference}.</p>
{/if}Callbacks are the props onevent and onbooked, in Svelte 5 style. The component also takes class, style and id, and renders on the server in SvelteKit. A named export BookingsXPWidget and loadEmbedScript are available too.
Astro#
npm install @bookingsxp/astro---
import BookingsXPWidget from "@bookingsxp/astro/BookingsXPWidget.astro";
---
<BookingsXPWidget widget="w_8fk2m1qz" />
<BookingsXPWidget widget="w_8fk2m1qz" mode="popup" buttonText="Book a call" accent="#0f766e" />
<BookingsXPWidget bookingUrl="https://outlook.office.com/book/Contoso@contoso.com/" template="classic" />The Astro component renders the element and the script tag on the server (the script once per page) and ships no framework JavaScript. It takes the shared props plus class, id and style.
Astro components have no client callbacks, so listen for DOM events in a script:
<script>
window.addEventListener("bookingsxp:booking_completed", (e) => {
const detail = (e as CustomEvent).detail;
console.log("Booked", detail.booking?.id);
});
</script>Or use Google Tag Manager on the dataLayer events the loader pushes. See GTM, GA4 and ad conversions.
Anything else#
The widget is one script and one HTML element, so it works in any framework, template language or site builder without a package: add the script tag from the Quickstart and render bookingsxp-widget like any other element. In Angular, add CUSTOM_ELEMENTS_SCHEMA to the component that uses it. For WordPress there is a plugin.
Edit or question? hello@bookingsxp.com