@openings-link/react
Open-source React library for building booking interfaces with Openings.
Why Openings?
Most booking platforms treat a business as a flat list of time slots. That works for a solo freelancer — but falls apart the moment you have multiple locations or a team.
Openings is built around the way real service businesses actually work:
- Business → Schedules → Staff → Services. A barbershop with two studios and five barbers isn't five independent calendars — it's one business with structure. Openings models that structure natively.
- One widget, every configuration. The same
<BookingWidget>handles a solo operator, a multi-location chain, or a single staff member's personal booking page — controlled entirely by props. - Headless-first.
@openings-link/reactgives you hooks and state with zero UI opinions.@openings-link/react-uigives you a themed drop-in.
Installation
npm install @openings-link/react @openings-link/react-ui@openings-link/react — Headless hooks, state machine, and types. Zero dependencies, React 18+ peer dep only.
@openings-link/react-ui — Themed, drop-in booking components built on the headless core. Styled with CSS custom properties (no Tailwind, no CSS modules).
Using the Real API (CORS-safe)
This example app includes a same-origin proxy route at /api/proxy/* so browser requests do not hit https://api.openings.link directly.
When integrating in your own app, set apiBase to your proxy path:
<BookingWidget
business="your-business-handle"
apiBase="/api/proxy"
/>For full templates (Next.js, Remix, Express), seeproxy-templates.md.
Quick Start
Drop-in Widget
One component, one prop. Connect to a live Openings business by its handle.
import { BookingWidget } from "@openings-link/react-ui";
<BookingWidget
business="your-business-handle"
theme={{ accent: "#8B5CF6" }}
on={{
onBookingComplete: (result) => {
console.log("Booked!", result);
},
}}
/>Headless (Custom UI)
Use the hooks directly to build any UI. All booking state and actions are exposed through useBookingFlow().
import { OpeningsProvider, useBookingFlow } from "@openings-link/react";
<OpeningsProvider business="your-business-handle">
<MyBookingUI />
</OpeningsProvider>
function MyBookingUI() {
const {
step, // schedule | openings | review | verify | confirm | service-request
schedules, // available locations
services, // filtered by staff in member mode
members, // staff for selected schedule
selectedServices,// user's selected services
memberOpenings, // time slots by staff (or by location in staff mode)
selectSchedule,
selectService,
removeService,
selectSlot,
goToReview,
goBack,
canGoBack,
} = useBookingFlow();
}Booking Layers
Openings organizes booking around four layers. Understanding these is key to using the library — and what makes Openings powerful for multi-location, multi-staff businesses.
Business
The top-level entity. A business has a unique handle (slug) like "demo" and is the only required prop.
Schedules (Locations)
A business has one or more schedules. Each schedule is a bookable unit — usually a physical location, but it can also be an online schedule (no address). A barbershop with two studios has two schedules.
When a business has multiple schedules, the widget shows a location picker first. With only one schedule, it skips straight to booking.
Staff (Members)
Each schedule has staff members assigned to it. A staff member can work at multiple locations. When a customer picks a time slot, they're booking with a specific person at a specific place.
Staff booking mode: Pass memberId to show a specific staff member's availability across all their locations. Only their services are listed, and time slots are grouped by location.
Services
Services belong to the business, but each staff member offers different services. A colorist has different services than a barber. In location mode the dropdown shows all services available there. In staff mode it shows only that person's services.
How It All Fits Together
| Entry props | Customer sees |
|---|---|
| business only (multi) | Location → services → date/time → book |
| business only (single) | Services → date/time → book |
| business + memberId | Staff's services → date/time by location → book |
| business + scheduleId | Services → date/time → book (location locked) |
Booking Flow
The step machine enforces a linear flow with back navigation:
schedule — pick a location (skipped if one schedule or entry prop set).
openings — pick service, date, and time slot.
review — confirm selection.
verify — phone/email lookup. New customers enter name + verification code. Returning customers book directly.
confirm — booking confirmation.
service-request — consultation form with optional photo uploads. Triggered by services marked hasConsultation.
Props Reference
All props accepted by <BookingWidget />:
| Prop | Type | Description |
|---|---|---|
| business* | string | Required. The business handle (slug) to load. This is the only required prop. |
| apiBase | string | API base URL. Defaults to "https://api.openings.link". Override for self-hosted or staging environments. |
| scheduleId | string | Pre-select a specific schedule. Skips the location picker and jumps straight to service/time selection. |
| memberId | string | Pre-select a specific staff member. Shows only their services and groups time slots by location. |
| theme | BookingTheme | Customize the widget appearance — accent color, border radius, font, and light/dark mode. |
| labels | Partial<BookingLabels> | Override any user-facing string for i18n or custom copy. |
| apiClient | ApiClient | Inject a custom API client. Useful for testing with mock data or wrapping requests. |
| on | OpeningsCallbacks | Event callbacks for booking lifecycle events. |
| onConsultationRequest | (member, services) => void | Called when a user clicks "Send a Request" on a consultation service. If omitted, the built-in service request form is shown. |
| className | string | CSS class name applied to the root container element. |
BookingTheme
{
accent?: string; // Primary color (default: "#000000")
radius?: number; // Border radius in px (default: 8)
fontFamily?: string; // Font family (default: system)
mode?: "light" | "dark" | "auto";
}OpeningsCallbacks
{
onBookingComplete?: (result) => void;
onServiceRequestComplete?: (result) => void;
onStepChange?: (from, to) => void;
onError?: (error) => void;
onScheduleSelect?: (schedule) => void;
onServiceSelect?: (service) => void;
onSlotSelect?: (slot) => void;
onVerificationSent?: (method) => void;
onVerificationComplete?: (customerId) => void;
onConsultationRequest?: (request) => void;
}See live demos → Examples