Waitlist desk

A React block (a whole screen) in Beamline's Events & bookings app.

A front desk's waitlist beside the day's book: who is waiting (filters as chips, the queue's count apart from the day's), the day's bookings and the openings your app offers, and the chosen opening with every request for that date drawn on the gap's own time scale, so what fits, what overhangs and by how much, a group short of places, a stylist asked for or a window that starts late reads before a word. Slide the chosen booking along the gap to set its start, then Book (details your app still needs, pending, the receipt; or taken meanwhile) or Not for this client (the request stays queued). New requests in a sheet: one request however many dates, each with its time window. Two more parts travel with it: WaitlistJoin, the client's own Day & time step with Join the waitlist under the times (nothing booked by joining), and WaitlistRouting, the waitlist's messages (who hears that a client was added, by how they were added; openings to the salon only).

Use it for

  • Filling cancellations from a waitlist at a salon, spa, clinic, studio or any business booked by the slot: the desk sees which waiting requests fit an opening, and by how much the others miss, before calling anyone.
  • Taking waitlist requests where one client can come on several dates, each with a time window, with a guest count for a group and a preferred stylist or practitioner.
  • WaitlistJoin: an online booking step where a client can ask to be told when a time opens, beside the times that are free. WaitlistRouting: the settings page for the waitlist's messages.

Not for

Anatomy

  • header: the product's mark, the location (also the queue's), the book's day with previous and next, New request
  • queue: Waitlist with its result count (or Count unavailable), filters as chips (requested date, stylist, fits an opening on the book's day) with Add filter and Clear all, and request cards longest waiting first: the client, how long they have waited, services and their total length, guests for a group, each date with its time window joined by or, the stylist asked for, Added online, and Fits <stylist> <time> when an opening on the book's day fits
  • book: a lane per stylist across the day's hours with the hours labelled once on a ruler; bookings as quiet ink tiles (client and service, folding to the client, the first name, then initials, by the tile's width; a tile too short for the whole name names it in a tooltip on hover and focus); each opening as a dashed well with its times and how many requests fit; the book's own count of requests for its day and location; where the chosen request would land, as a dashed ghost that slides with its start
  • opening lane: the opening's stylist, times, length and places; the scale with the gap across most of the lane and, after its end, an overflow zone (Past it) as long as the longest overrun, between a quarter and a third of the gap; requests grouped Fit, Close but, Not offered; each row the client, services and the reason in words (ink; success for a fit) beside a bar of the services' length placed at the earliest start the client's window allows, the gap as a dashed well behind it, the part of the gap the client did not ask for shaded, the part past the gap's end in the danger tone (+N min past the zone), and the client's window drawn under it as the request sheet's rail, labelled where it starts or pointing off the lane
  • decision (a fitting row opened): the bar becomes a handle (drag along the gap or ←/→, Home, End; snapped to the step), the start as a select of the allowed starts beside it, Book <time> and Not for this client; a phone number field when your app asks for details; a receipt with the reference after a booking; taken meanwhile with Choose another opening
  • request sheet: client (new requests), location and guests, services with their total length, stylist (anyone or one), dates joined by or, each with a date, a time window on the day's hours (two thumbs, from opening to closing for any time) and its two times as selects, Add another date, Remove date; a note; Add to waitlist or Save request, with the problems counted and the refusal's reason beside it
  • phone: Waitlist and Day as two views; the day as a list of its openings (stylist, times, length, places, how many fit); an opening's lane full width with Back to the openings
  • WaitlistJoin: the booking's steps, the service and stylist chosen, a strip of days (a full day marked Full), the day's times by morning and afternoon, and under them Join the waitlist (primary when the day is full); the request: each date with its time window (two thumbs), another day added with one tap, name and phone, Join the waitlist, and the receipt that lists the dates and says nothing is booked
  • WaitlistRouting: Send waitlist messages; the two events on a spine, each route a card with its switch (email or text the client; email the salon at an address checked where it is typed, for every request or only clients who added themselves) and its last delivery as your app reports it; that clients aren't told about openings; Who hears it for a request added at the desk or online; Save messages

States

  • loading (skeletons of cards and lanes)
  • filtered results (chips, the result count; the book's count unchanged)
  • no matches (Clear filters)
  • no future requests (Nobody is waiting)
  • unavailable count (resultCount or dayCount null: Count unavailable, never 0)
  • candidate opening (a dashed well with how many fit; the lane lists every request for that date)
  • a live cancellation (a booking leaves the book while its gap opens; the lane lines up against the new opening the app chooses)
  • a fitting request opened (the handle, the start, Book and Not for this client)
  • dismissed candidate with the request retained (Not offered, Still on the waitlist, Undo; the request is not offered that opening again)
  • slot no longer available (taken meanwhile: the well greys as Taken, the row keeps the request with Choose another opening)
  • additional client information required (a phone number field before Book goes through)
  • booking pending (Book working, the row kept)
  • confirmed receipt (the reference; the gap becomes the booking on the book; the request leaves the queue)
  • group capacity unavailable (Needs 2 chairs, 1 free)
  • new draft, multiple alternatives (dates joined by or, each with its window)
  • missing service (named on the field; Save counts what to fix)
  • permission denied (canManage false: the sheet says so in place of the form)
  • submission failed with draft retained (the reason beside Save; every field as it was)
  • no openings on the day, no day to show
  • client step: times free but unsuitable (Join the waitlist under them), no times (the waitlist leads), waitlist off (no action; try another day), times unknown, request draft (Back keeps every choice), acknowledged, submission refused with the request kept
  • messages: feature off, configured routes, a route limited to self-added requests, an invalid salon address, saved, no delivery reported

Keyboard

  • Tab through the header, the filters, the cards, the book's openings and the lane
  • a card: Enter opens its request in the sheet; closing returns focus to the card with the filters kept
  • an opening: Enter chooses it (again to put it away)
  • a fitting row: Enter opens it; the bar is a slider: ←/→ one step, Home and End the first and last start; the start select is its precise twin
  • while dragging the bar, Escape puts the start back
  • the sheet: Remove date moves focus to the neighbouring date; Escape closes
  • the client's day strip: Tab to a day, Enter chooses it; Back returns focus to Join the waitlist
  • messages: every route a switch; Who hears it a segmented control

Motion

A live change on the day plays once: a cancelled booking thins and fades where it stood while the gap your app opens in its place draws in, then says how many fit; a new opening lines its requests up, each bar growing from the gap's start to its length on the motion tokens, a row after another by the item stagger; within one opening a row that changes group slides there (FLIP); the chosen booking and its ghost slide to a new start in the fast duration (held, they follow the hand in steps); a new booking and the receipt fade in. Nothing moves at rest; reduced motion places at once.

Props

PropTypeDefaultDescription
locations / staff / services / clients (required)WaitlistLocation[] / WaitlistStaff[] / WaitlistService[] / WaitlistClient[]—The business's locations, stylists (with the locations they work at), services (with their minutes; null when not known, so the desk never says it fits) and the clients a new request can be for.
requests / defaultRequests / onRequestsChangeWaitlistRequest[]—Waiting requests. Each has one client, a guest count, services, a stylist or null, and one or more dates with a time window (minutes from midnight, null ends for opening and closing). A booked request leaves the list; Not for this client adds the opening to its dismissed.
day / date / defaultDate / onDateChange (required)WaitlistDay | null / string—The book: bookings and openings for the query's location on `date` ("2026-11-06"). Fetch it when the date or location changes; null while it loads.
dayCount / resultCountnumber | null—dayCount: Requests for the book's day and location, from your server; leave it out and the desk counts what it has; null says "count unavailable". It never follows the queue's filters. resultCount: Matching requests from your server when the queue is paged there; leave it out to count the list; null says "count unavailable".
query / defaultQuery / onQueryChangeWaitlistQuery—{ locationId, date, staffId, onlyOpenings }. The location is the book's too.
opening / defaultOpening / onOpeningChangestring | null—The opening lined up under the book.
editing / defaultEditing / onEditingChangestring | null—The request open in the sheet: an id, "new", or null.
onBook(input: WaitlistBookInput) => Promise<WaitlistBookResult>—Book a request into an opening; your answer decides what happens (see WaitlistBookResult). A rejection says why in the row and keeps everything as it was.
onDismiss({ openingId, requestId }) => Promise | void—The opening is not for this client: the request stays queued, with the opening in its `dismissed`.
onSaveRequest(draft: WaitlistDraft) => Promise<{ id }>—Save a new or edited request; resolve with its id. A rejection keeps the whole draft in the sheet with the reason.
canManageboolean—false: the person may look but not add or edit requests (the sheet says so in place of the form). Default true.
step / hoursnumber / [number, number]—step: Minutes between allowed starts. Default 15. hours: The book's hours when nothing is booked outside them, minutes from midnight. Default 9:00 to 19:00.
loading / error / onRetry / product / nowboolean / Error | null / () => void / string / number—Loading and a load failure with Try again; your product's name; now (ms) for how long each client has waited.
locations (required)WaitlistLocation[]—
staff (required)WaitlistStaff[]—
services (required)WaitlistService[]—
clients (required)WaitlistClient[]—The clients a new request can be for (the client field searches them).
requestsWaitlistRequest[]—Waiting requests: controlled with `onRequestsChange`, or `defaultRequests`. Booked requests leave this list.
defaultRequestsWaitlistRequest[]—
onRequestsChange((requests: WaitlistRequest[]) => void)—
day (required)WaitlistDay | null—The book's day for the query's location and `date`; null while it loads (with `loading`) or when there is none.
datestring—The book's date ("2026-11-06"), controlled with `onDateChange`, or `defaultDate`; fetch `day` for it.
defaultDatestring—
onDateChange((date: string) => void)—
dayCountnumber | null—Requests for the book's day and location, from your server; leave it out and the desk counts what it has; null says "count unavailable". It never follows the queue's filters.
resultCountnumber | null—Matching requests from your server when the queue is paged there; leave it out to count the list; null says "count unavailable".
queryWaitlistQuery—
defaultQueryWaitlistQuery—
onQueryChange((query: WaitlistQuery) => void)—
openingstring | null—The opening lined up under the book (its id), controlled with `onOpeningChange`, or `defaultOpening`.
defaultOpeningstring | null—
onOpeningChange((openingId: string | null) => void)—
candidatestring | null—The request opened in the opening's lane (its id), controlled with `onCandidateChange`, or `defaultCandidate`.
defaultCandidatestring | null—
onCandidateChange((requestId: string | null) => void)—
editingstring | null—The request open in the sheet: an id, "new", or null.
defaultEditingstring | null—
onEditingChange((editing: string | null) => void)—
stepnumber—Minutes between allowed starts. Default 15.
hours[number, number]—The book's hours when nothing is booked outside them, minutes from midnight. Default 9:00 to 19:00.
loadingboolean—
errorError | null—
onRetry(() => void)—
productstring—Your product's name, shown as its mark.
nownumber—Now, for "waiting 6 days" (ms). Default the time of the first render.
classNamestring—Classes on the documented visual root; no override by default.
refRef<HTMLDivElement>—Receives the documented native target or imperative handle; omitted by default.
WaitlistJoin: service / stylist / days / date / waitlist / onPickTime / onJoin{ name, minutes } / string | null / JoinDay[] / string / boolean / (date, start) => void / (request: JoinRequest) => Promise—The chosen service and stylist, the days with their free starts (null while unknown), the shown day, whether clients may join, the time picked (your flow confirms it), and the join (a rejection keeps the request with its reason). No appointment is made by joining.
WaitlistRouting: value / defaultValue / onValueChange / onSave / deliveries / sampleClientWaitlistRoutes / (routes) => Promise / WaitlistDeliveries / string—The routes of both events, Save (a rejection keeps the edits), the last delivery your app saw per route (null: none reported, never inferred from a route being on), and the client the preview names.

Built from

Dependencies

lucide-react

Import

import { WaitlistDesk, WaitlistJoin, WaitlistRouting } from "@beamline/waitlist-desk";

Get it

Waitlist desk is part of Beamline UI. Install the parts you need in your React app as compiled packages or editable source. Get Beamline · Connect your agent

More in Events & bookings

  • Team calendar: A team's schedule: the people and shared calendars to show (each its colour), a month to jump by, who is out this week, and the week, day or month of…
  • Day agenda: One person's days with notes, tasks and calendar events side by side, each kept as what it is. A task's scheduled day, deadline, reminder and repeat are…
  • Crew booking: Booking crew for a project: each person with a strip of the project's days, days in the same state joined into one run said once (booked solid, available…
  • Resource booking: Booking something shared (a room, a van, a camera kit). Choose what, then either a free hour on a day: a grid of starts the app says are free (a taken…
  • Resource rules: Each person's scheduling rules beside their day, the day board leading: role, available (an unavailable day filled in the danger tint and said, a job…
  • Desk booking: Desks in an office for the people who book them and the admin who sets how far ahead. Your days over the next weeks: booked with a desk, free to book…
  • Linked appointments: A drop-off and a pickup kept as one linked pair on the week's calendar (the shared event calendar, day view on a phone), with the car's stay drawn…
  • Visitor desk: An office front desk's visitors. Invite a meeting's guests from the calendar: colleagues are shown but never invited, guests without an email are said…
  • Issued ticket: One issued event ticket at the door, led by the ticket itself: what it admits to (event and venue), when, the section, seat and who it admits, and its…
  • Itinerary: A trip's days where every event says who sees it, in words: everyone, only Ana and Ben, or hidden from Leo. The same rules give each traveller their own…
  • Trip budget: A trip's money against its plan, in the trip's own currency. The budget (set or changed in place; a refused save keeps what was typed), what's spent and…
  • Proposal payments: A travel proposal's money from the agent's side to the client's. Pricing: the price rule (markup on supplier cost, a fixed price, or net), what the…
  • Project periods: A project's time periods (build, show, teardown) on one time scale, the thing being planned: each period a bar across its days with the crew and gear…

Part of Beamline UI: polished, editable React components and complete screens for custom applications. Build your own style from any look; tokens, fonts, radii, motion; not only colours.