Susegad UI
Register
Theme
Palette

Recipes

Booking

Book the whole house, or a room, in four steps built from every Wave 2 part: dates with the season ribbon, guests and room, your details with the phone confirmed by a code, and a signature to confirm and hold, with breakfast as a switch.

quiet warm playful

Open the live demo npx susegad add booking

Stands on: Checkbox, Combobox, Core, Core: components, Date range, Engine, Field (TextField, TextArea), Field note, Form, Kernels: booking, Loader, One-time code, Radio group, Select, Signature, Stamp, Stepper, Toast, Toggle, Tokens

The prompt

the prompt

Book a stay: four short steps, a price you can check by hand, and a hold that says it is a prototype.

The prompt

Build a booking form for a whole-house homestay entirely from Susegad UI parts. Put <sg-stepper> inside <sg-form> around a native form, with four steps as fieldsets: dates (<sg-date-range> with the season ribbon), guests and room (<sg-select> for guests, <sg-combobox> for the city they come from, and a required <sg-radio-group> for the room: the whole house, the Sotão, the Balcão or the Quintal), your details (<sg-field> for a name and a mobile number, and <sg-otp> for a six-digit code), and confirm and hold (a required <sg-check> for the house rules, an <sg-toggle> for breakfast, and <sg-signature> to sign). The rate card prices only the whole house, so for a single room show no figure and say the house will confirm its price. Without JavaScript every step must show and the form must validate and submit natively, with POST to a page that keeps nothing and says so, never with GET, which would put people's details in the address bar, with no control that needs scripts and a plain line saying what does. Beside the form, keep a quote card worked out entirely by the pure booking kernels: each band's nights on their own line, then rooms, GST at 18% on its own line, the total and the refundable deposit, in rupees with Indian digit grouping; print whether the rates include GST, and while anything on the card is provisional, say so. When the kernels refuse a stay, keep the person on the dates with the reason in their words. Confirm the phone with a code: since nothing leaves the page, show the code that would have been sent and say so, and keep the details step closed until the code typed matches. A valid submit is the hold: answer it at once with the stay, the guests and "Prototype: nothing is held and nothing is charged.", and let the form land a "Held" stamp in warm and playful. Keep a plain statement that the page holds nothing and charges nothing. Make the walk itself easy to follow: each step is a fieldset whose legend names it, Next checks only that step and focuses the first problem with the browser's own message, focus moves to the new step's legend with "Step 2 of 4" read after it, Back never loses what was typed, and a submit with a problem in an earlier step goes back to that step first. Read out only what changes on the quote card (the total, or the reason a stay is refused), in one polite line. Let the page's register choose every picture through the parts: in quiet, hairlines and words; in warm, the inked calendar, the survey-map walk and the block-print stamp; in playful, bolder ink, footprints along the road and a stamp that presses down. With reduced motion every part shows its finished state.

Words to code

When you sayTechniqueWhat happens
Make the walk itself easy to followthe four-step walkEach step is a <fieldset> with a <legend>. <sg-stepper> checks the step's own fields with their native validity, moves focus to the next legend described by "Step 2 of 4", and sends a submit with a problem back to its step. Nothing is removed from the form, so every step still submits.
<sg-stepper> inside <sg-form>compositionThe stepper shows one fieldset at a time and checks it with the form's own validation before Next; the form owns the submit, the summary of problems and the answer. Neither knows the other: they meet in native constraint validation.
every step must show without JavaScriptprogressive enhancementWith no scripts the stepper is four fieldsets in order and one submit button. The "Send a code" button is created by the script and the code field stays hidden, so nothing on the page needs a script it cannot run.
worked out entirely by the pure booking kernelsone source of figuresquote() prices each night by its band and weekday; quoteView only groups and formats them. The calendar reads the same kernels, so it and the card can never disagree.
with the reason in their wordsconstraint validation<sg-date-range> turns the kernels' reason into a guest's words and sets it as the departure input's custom validity, so Next stops on the dates and the browser says why.
for a single room show no figureno figure it does not havequoteView returns a room state with no lines and no total when the room is not the whole house, and the card says the house will confirm the price. The stay's rules (minimum nights, taken nights) still apply.
show the code that would have been sentthe demo says what it iscodeFor(seed, phone, attempt) makes a seeded six-digit code; the page prints it in a status line that begins "Prototype: nothing leaves this page."
keep the details step closed until the code typed matchescustom validityThe number carries "Send a code to confirm this number." until a code is sent; then the code field is required and carries "That code does not match…" until it does. Digits typed in Devanagari, Kannada or full width count, as they do in <sg-otp>.
answer it at onceno fake workThe prototype has nothing to wait for, so the hold resolves immediately. There is no timer pretending to be a server.

The Casa Exemplo booking flow, rebuilt from library parts: four short steps (dates, guests and room, your details, confirm and hold), a price you can check by hand, a phone confirmed by a code, and a signature that holds the dates. It is a prototype and says so: nothing leaves the page, nothing is held and nothing is charged.

StepPartsAsks for
The walk<sg-stepper> inside <sg-form>one step at a time with Back and Next; each step checked before the next; without JavaScript every step shows and the form submits
Dates<sg-date-range prices>arrival and departure, on a calendar that knows taken nights, turnover mornings and the seasons
Guests and room<sg-select>, <sg-combobox>, <sg-radio-group>guests (1 to 6), the city you are coming from (optional), the room: the whole house, the Sotão, the Balcão or the Quintal (the room names are still to be confirmed by the house)
Your details<sg-field>, <sg-otp>your name, a mobile number, and the 6-digit code "sent" to it
Confirm and hold<sg-check>, <sg-toggle>, <sg-signature>the house rules (required), breakfast (optional, arranged with the house and not in the price), your signature
Beside it allthe quote cardevery band's nights, rooms, GST on its own line, the total, the deposit

The answer to the hold is <sg-form>'s: "Held" stamped in warm and playful with "Prototype" under it, as the enquiry's "Not sent" is, and the same words in quiet. A real hold drops the second line (STRINGS.heldDetail).

Files

FileWhat it is
booking.core.jsPure: the quote as words and figures (nights grouped by band and weeknight or weekend, rooms, GST, total, deposit, provisional flags), the Held words, and the phone code (a seeded six-digit code, read in any script).
recipe.jsThe wiring: mountBooking(root, { rates, blocks, seed }), renderQuote(card, view, reasons) and the phone check.
recipe.cssThe layout, the quote card and the flow's own pieces.
held.htmlWhere the form lands without JavaScript. It keeps nothing and says so.
index.htmlThe page, which also works as the demo. Links the tokens relatively and reads ?register, ?theme and ?palette.
recipe.test.jsTests for the quote and the code, with figures worked by hand from the rate card.
recipe.check.mjsBrowser checks: the page with JavaScript off (quiet, reduced motion), then the walk with it.
form-check.jsonValid sample values for tools/form-check.mjs: a stay the rate card accepts, and the code the page shows. Not copied by the CLI.

Put it on a page

Copy the form and the quote card from index.html, then:

import { mountBooking } from './susegad/recipes/booking/recipe.js';
const tags = ['sg-form', 'sg-stepper', 'sg-date-range', 'sg-select', 'sg-combobox', 'sg-radio-group', 'sg-check', 'sg-field', 'sg-otp', 'sg-toggle', 'sg-signature'];
await Promise.all(tags.map(t => customElements.whenDefined(t)));
mountBooking(document.getElementById('booking'), {
  blocks: [{ arrival: '2026-11-13', departure: '2026-11-16', kind: 'booking' }], // nights already taken
});

For a real booking, replace the two prototype parts:

  • The code: mountBooking shows the code on the page because nothing is sent. Send it by SMS from your server instead, and check it there too.
  • The hold: mountBooking answers the form's sg-submit at once. Create the hold on your server, then resolve with { message, stamp: 'Held' }, or reject with what went wrong.

Where the figures come from

  • Every figure is the booking kernels' own: each band's nights on their own line, rooms, GST at 18% on its own line (or "none below ₹7,500 a night"), the total and the deposit, in lakh grouping.
  • The rate card's provisional flags reach the page: while any rate or the deposit is provisional, the card says so. The GST mode is printed, not implied.
  • A refused stay leads the card with "Not bookable as chosen" and the reasons, then its nights, with no total and no deposit, so no figure for an impossible stay can be screenshotted.
  • Without JavaScript the card says the house will send the price when they write back, and the minimum stays (3 nights, 4 over Christmas week) are stated under the dates.
  • A stay the kernels refuse is refused by the form's own validation, in a guest's words ("Stays over Christmas week are at least 4 nights."), and the walk does not go on.
  • The rate card prices the whole house only. Choose a single room and the card shows no figure for it: it says the house will confirm that room's price when they write back.
  • The phone step does not pass until the code typed matches the code "sent", and the page says plainly that nothing left it.
  • The prototype statement is always on the page, and the Held message repeats it.

Without JavaScript

Every step shows, in order: two date inputs with the window's min and max, a select, a text input with suggestions from a datalist, the room radios, the name and number, the house rules checkbox, the breakfast switch and a typed-name signature. The browser validates them and posts them to held.html, a static page that keeps nothing and says so. POST, never GET: a name, a mobile number and a signature must never end up in the address bar, the browser's history or a server's logs. In your project, point action at your own endpoint. There is no "Send a code" button and no code field, because nothing could send one; a noscript line says the number is confirmed when the house writes back, and another says the price and the hold need JavaScript.

Accessibility

  • The stepper moves focus to each step's legend and says "Step 2 of 4"; Next focuses the first problem with the browser's own message.
  • One polite line reads out the total for the stay, rather than the whole card, and only when the total changes: typing a name or a number never repeats it. A refused stay is read out once, by the calendar's own list of reasons; the card's line stays quiet about it. The code note is a polite status line.
  • The quote card is a labelled aside; its lines are a description list.
  • Every part brings its own keyboard and screen reader behaviour: the calendar grid, the combobox listbox, the radio group, the OTP boxes, the signature's typed path.

Verify

node --test packages/recipes/booking/recipe.test.js
node packages/recipes/booking/recipe.check.mjs
MSYS_NO_PATHCONV=1 node tools/form-check.mjs --url /packages/recipes/booking/index.html
MSYS_NO_PATHCONV=1 node tools/form-check.mjs --url /packages/recipes/booking/index.html --no-js
MSYS_NO_PATHCONV=1 node tools/matrix.mjs --url /packages/recipes/booking/index.html --jobs 1
MSYS_NO_PATHCONV=1 node tools/matrix.mjs --url /packages/recipes/booking/index.html --jobs 1 --no-js
MSYS_NO_PATHCONV=1 node tools/axe.mjs --url /packages/recipes/booking/index.html
MSYS_NO_PATHCONV=1 node tools/axe.mjs --url /packages/recipes/booking/index.html --no-js