Build guide · site 346 · Wave 38, Industry Demos XXVII
A fictional six-chair, colour-led salon on Oxford Street, Leederville — cuts, global colour, balayage, half-head foils, toners and bond treatments. The page has one job: a booking request with the service, the length and the add-ons already priced.
The audience is the woman booking her first balayage after years of box dye, the regular who wants her six-weekly cut without a phone call, the bride's mother wanting a trial, and anyone whose blonde has gone brassy. Hair pricing is length-driven and clients rarely know which length they are, so the page names the three lengths in plain words (above the shoulder / to the bra strap / past the bra strap) and shows them drawn. It is deliberately distinct from the collection's barber demo — that is a men's chair menu; this is colour.
The direction is the salon at ten in the morning: plaster-blush ground, espresso ink, rosewood CTAs, brass linework for the mirror frame and trolley rails. The signature is the Colour Chair — a drawn salon chair in front of a brass-framed mirror with a client seated back-to-us so the hair is the subject. Length chips redraw the hair's fall; service chips redraw its colour and finish; the toggles draw their marks (a gloss band, a tint bowl on the trolley, two bottles on the shelf); a hairdryer on the trolley lifts the ends on a loop; and the price sits on the mirror like a lipstick note.
Every ratio below was computed by WCAG relative-luminance arithmetic from the shipped hex tokens in Pass 1 (the spec's figures were hypotheses). Rosewood #A64D57 measured 4.73:1 on the blush ground and 5.50:1 under white labels — both clear the 4.5 floor, so buttons keep the argued rosewood and darken to the pre-minted fill #8E3D47 (7.22:1) on hover and for selected chips; every rosewood-coloured word uses the text-ink #7E343E (7.38:1). Brass #B08D57 is 2.66:1 on the ground and is used only for lines and marks; brass-coloured copy uses #7A5C2E (5.32:1).
Shoulder to bra strap, painted
Fraunces 600 (with a 500 italic) for the wordmark, headings, the display line and the badge price — a soft, slightly wonky serif that reads like a good salon brand. The display moment sets the length huge above the drawn chair and re-words itself with the chips: Above the shoulder, cut · Shoulder to bra strap, painted · Past the bra strap, foiled.
Karla 400–700
Karla for everything read: body at 17px on mobile and 18px from 760px, chips, ledger, form. Weight carries hierarchy (700 legends and buttons, 600 chips, 500 fine print); tracking only on the uppercase eyebrows, the badge name line and the drawn service label.
<g class="layer"> so the first draw can stagger them in.hairD(layout, endY, sway, lift) builds a crown arc plus two cubic contours that flare toward the pinned end-Y for the length (wide: 292 / 392 / 462; the tall layout has its own set), with a curved bottom edge whose control points carry the sway. Four service groups (cut / global / balayage / foils) each hold a copy of the path with their own fill — espresso gradient, copper gradient, espresso plus a hand-painted overlay gradient with six clipped streaks, espresso plus seven foil packets — and cross-fade via a .cur class so a chip swap never snaps. A single clipPath shares the same d for the sheen, streaks, strands and gloss band.dt = min(max(now − last, 0)/1000, .05) drives sway (two sines) and tip lift scaled by a slow breeze envelope, three dashed breeze arcs from the dryer nozzle to the hair, the wet-comb sheen sweep on cut, a shimmer gliding down the balayage, travelling glints on the foils, the toner band breathing between 0.32 and 0.52 and the sconce glow. Hair opacity is always 1 — the never-invisible floor is structural, not tuned. Reduced motion renders one settled frame at t = 1.7 s; the loop pauses on document.hidden.hidden with the [hidden]{display:none!important} guard, never the IDL property), each fading in via a CSS animation on the group while the breathing opacity attribute lives on the child path — state and animation on separate elements.<style>, Google Fonts the only external request.State is one object — service, length, three toggles — and compute() is a pure function over it: base from the service × length matrix; toner +$45 only when the service is a colour service and the remembered toner choice is on; bond +$40; kit +$65; the long note when length is long on balayage or foils. render() writes the result to every surface in one pass: the display line, the badge (name, price, subtitle, note), the scene (current hair group, three marks, service label, length tag), the toner switch (disabled with the on-card reason on cut, aria-checked mirroring the effective state), the itemised ledger and the plain-text mirror list under the scene. Restore-vs-reset is by memory: state.toner is never overwritten by the cut chip, so switching cut → any colour service restores the prior choice exactly. Every handler bumps a scene token before touching state; the settle timer publishes window.__hairsalon.settled only for the latest token, so a rapid-fire run of clicks ends on one definite, settled state.
| Service | Short · above the shoulder | Medium · to the bra strap | Long · past the bra strap |
|---|---|---|---|
| Cut & style | $85 | $95 | $110 |
| Global colour & cut | $185 | $215 | $250 |
| Balayage & cut | $260 | $310 | $370 |
| Half-head foils & cut | $210 | $245 | $285 |
| Add-on / rule | Amount | Applies |
|---|---|---|
| Toner & gloss (one toner, any length) | +$45 flat | live on global, balayage and foils; DISABLED on cut & style with the reason "a toner sits over colour — pick a colour service to add one"; the switch remembers its choice and RESTORES on return to a colour service |
| Bond treatment | +$40 flat | live at every combination |
| Home-care kit | +$65 flat | live at every combination |
| Long × balayage or foils | note only | "past the bra strap we book an extra half hour — it's allowed for in the price" on badge, ledger and mirror; absent at short/medium and at long × cut / long × global |
| Keratin, extensions, bridal, colour correction | no number | "quoted at consultation" — grid, FAQ and fee note |
| Consultation, wash, blow-dry; free 15-min first consultation | included | declaration lines, never line items |
Totals are integer by construction and display to the whole dollar. The estimator's cheapest outputs equal the services grid floors: cut from $85, global from $185, balayage from $260, foils from $210.
| Scenario (full control vector set explicitly) | Spec | Measured | Also asserted |
|---|---|---|---|
| Rest: balayage · medium · toner ON · bond OFF · kit OFF | $355 | $355 | display "SHOULDER TO BRA STRAP, painted"; note absent on all three surfaces; balayage layer current |
| Cut & style · short · all OFF | $85 | $85 | toner switch disabled with reason; grid floor |
| …then chip to global colour (toner had been ON before cut) | $230 | $230 | toner RESTORED on; the OFF-memory path also asserted → $185 |
| Global · long · toner ON · bond ON | $335 | $335 | long note ABSENT (cut/global never note) |
| Half-head foils · medium · kit ON | $310 | $310 | bottles drawn on the shelf |
| Balayage · long · toner + bond + kit | $520 | $520 | note PRESENT on badge, ledger, mirror; all three marks drawn |
| …step to medium | $460 | $460 | note ABSENT on all three surfaces |
| Rest, toner OFF | $310 | $310 | gloss band hidden |
| Foils · long · all OFF / cut · long / global · short / foils · short / balayage · short | $285 / $110 / $185 / $210 / $260 | same | note present only on foils-long; floors match the grid |
The live page carries only WebPage + BreadcrumbList JSON-LD naming it a demonstration — fictional business data in a real search index would be dishonest markup. For the real Salt & Silk we would ship a HairSalon entity (the exact schema.org organization subtype) with opening hours, geo, price range, the service catalogue and a reviews feed:
{
"@context": "https://schema.org",
"@type": "HairSalon",
"@id": "https://saltandsilkhair.example/#salon",
"name": "Salt & Silk Hair",
"image": "https://saltandsilkhair.example/salon.jpg",
"url": "https://saltandsilkhair.example/",
"telephone": "+61 8 9000 0346",
"priceRange": "$85–$520",
"address": {
"@type": "PostalAddress",
"streetAddress": "Oxford Street",
"addressLocality": "Leederville",
"addressRegion": "WA",
"postalCode": "6007",
"addressCountry": "AU"
},
"geo": { "@type": "GeoCoordinates", "latitude": -31.936, "longitude": 115.842 },
"openingHoursSpecification": [
{ "@type": "OpeningHoursSpecification", "dayOfWeek": ["Tuesday","Wednesday","Friday"], "opens": "09:00", "closes": "17:30" },
{ "@type": "OpeningHoursSpecification", "dayOfWeek": "Thursday", "opens": "09:00", "closes": "20:00" },
{ "@type": "OpeningHoursSpecification", "dayOfWeek": "Saturday", "opens": "08:00", "closes": "16:00" }
],
"areaServed": ["Leederville","West Leederville","Mount Hawthorn","North Perth","Mount Lawley","Subiaco","Wembley"],
"hasOfferCatalog": {
"@type": "OfferCatalog",
"name": "Services",
"itemListElement": [
{ "@type": "Offer", "itemOffered": { "@type": "Service", "name": "Cut & style" }, "priceSpecification": { "@type": "PriceSpecification", "minPrice": 85, "priceCurrency": "AUD" } },
{ "@type": "Offer", "itemOffered": { "@type": "Service", "name": "Balayage & cut" }, "priceSpecification": { "@type": "PriceSpecification", "minPrice": 260, "priceCurrency": "AUD" } },
{ "@type": "Offer", "itemOffered": { "@type": "Service", "name": "Half-head foils & cut" }, "priceSpecification": { "@type": "PriceSpecification", "minPrice": 210, "priceCurrency": "AUD" } }
]
},
"aggregateRating": { "@type": "AggregateRating", "ratingValue": "4.9", "reviewCount": "184" },
"review": [
{
"@type": "Review",
"author": { "@type": "Person", "name": "K. — Mount Hawthorn" },
"reviewRating": { "@type": "Rating", "ratingValue": "5" },
"reviewBody": "First balayage after ten years of box dye — two visits, no line, nobody at work could tell."
}
]
}
Alongside the markup: a Google Business Profile with the same NAP and hours, per-service landing sections with the length-priced anchors, a reviews feed synced from Google, and location copy that names the inner-north and inner-west suburbs the salon actually draws from.
pattern attributes — phone validation is JS.This is a demonstration build by Sapience Analytics. Want one pointed at your customers?
Salt & Silk Hair is a fictional business — a hair salon industry demonstration designed & built by Sapience Analytics, part of the Generative Assets collection. Back to the site · Index.