← Back to the Salt & Silk Hair demo Index

Build guide · site 346 · Wave 38, Industry Demos XXVII

How the Salt & Silk Hair demo was built

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.

Concept

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.

Palette — measured, not assumed

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).

#F6ECE7
Plaster blushPage ground — a blush plaster wall in morning light.ground for every ratio below
#2B211E
EspressoBody ink, headings, trust strip, demo band.13.50:1 on blush · white on it 15.68:1
#5A4A45
Soft inkSecondary copy, ledger notes, form fine print.7.23:1 on blush · 8.40:1 on white · 6.66:1 on the booking ground
#A64D57
RosewoodPrimary buttons, switch knobs, map ring, hairdryer.white on it 5.50:1 · on blush 4.73:1
#8E3D47
Rosewood fillButton hover, selected chips, Leederville star.white on it 7.22:1
#7E343E
Rosewood inkLinks, italic display words, the badge price, service label.7.38:1 on blush · 8.58:1 on white · 6.95:1 on the rose tint
#B08D57
BrassMirror frame, trolley rails, chip outlines, rules, sticky Call button ground.2.66:1 on blush — lines only · espresso on it 5.07:1
#7A5C2E
Brass inkEyebrows, badge name line, note lines, length tag.5.32:1 on blush · 6.18:1 on white · 5.76:1 on the card

Type

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.

Techniques

Signature implementation

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.

Pricing model (pinned anchors)

ServiceShort · above the shoulderMedium · to the bra strapLong · 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 / ruleAmountApplies
Toner & gloss (one toner, any length)+$45 flatlive 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 flatlive at every combination
Home-care kit+$65 flatlive at every combination
Long × balayage or foilsnote 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 correctionno number"quoted at consultation" — grid, FAQ and fee note
Consultation, wash, blow-dry; free 15-min first consultationincludeddeclaration 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.

Worked examples — measured from the running page against an independent oracle

Scenario (full control vector set explicitly)SpecMeasuredAlso asserted
Rest: balayage · medium · toner ON · bond OFF · kit OFF$355$355display "SHOULDER TO BRA STRAP, painted"; note absent on all three surfaces; balayage layer current
Cut & style · short · all OFF$85$85toner switch disabled with reason; grid floor
…then chip to global colour (toner had been ON before cut)$230$230toner RESTORED on; the OFF-memory path also asserted → $185
Global · long · toner ON · bond ON$335$335long note ABSENT (cut/global never note)
Half-head foils · medium · kit ON$310$310bottles drawn on the shelf
Balayage · long · toner + bond + kit$520$520note PRESENT on badge, ledger, mirror; all three marks drawn
…step to medium$460$460note ABSENT on all three surfaces
Rest, toner OFF$310$310gloss band hidden
Foils · long · all OFF / cut · long / global · short / foils · short / balayage · short$285 / $110 / $185 / $210 / $260samenote present only on foils-long; floors match the grid

Conversion decisions

Local SEO we'd implement for the real business

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.

What would change for the real client

Iteration log

Pass 1 — Craft

  • Measured every palette pair (table above). Rosewood cleared 4.5 both ways so buttons keep the base hue; ink variants used everywhere text is coloured. Brass demoted to lines and marks; brass-ink for the tag, eyebrows and note lines.
  • Screenshot review at 1200 found the price badge overhanging the mirror's right frame and its bottom edge grazing the hair; moved the head left (cx 545 → 525), widened the mirror (430 → 440), moved the badge inside the glass at 66.5% / 6% and shortened the badge subtitle to one-word add-ons (toner · bond · kit) so the worst case (foils, long, everything on, note) stays a compact card.
  • Short hair had been drawn past the shoulder — pinned end-Y to 292 so "above the shoulder" is true in the drawing. Tag and label fonts lifted (12/13 → 14/15) because a 900-unit scene renders at ~0.69 scale.
  • Map: the reach-ring caption ran through Osborne Park, the ring's right edge struck through the salon label, and the parking tag overflowed — moved the label above the star, Osborne Park to the coast, dropped the freeway hint (Chanel rule) and widened the tag.
  • Copy read: eyebrow shortened so it never wraps at 375; every price in the grid restated per length so the grid and the estimator can be checked against each other by eye.

Pass 2 — Depth

  • Enriched the hair: strands as clipped darker strokes on every service; balayage gets a painted overlay gradient plus six hand-painted streaks and a shimmer that glides toward the tips; foils get seven folded packets with travelling glints; cut gets a wider, brighter wet-comb sheen sweep (0.3–0.4) after the first one read as invisible in the screenshot.
  • Second-read details: the two sconces breathe out of phase; the hairdryer's breeze arcs dash toward the hair and their opacity follows the same envelope that scales the sway, so the hair visibly lifts when the breeze arcs brighten.
  • Length tag rebuilt as a pill: on long hair the old bare text sat over the chair leather and the mirror's bottom frame; the pill (blush card, brass hairline) with a leader from the hair edge is legible on any ground, and the tall layout swaps to a shorter phrase set so it never reaches the frame.
  • Micro-interaction: chip swaps cross-fade the four hair groups (0.45 s) instead of snapping; marks fade in; the mirror list and ledger re-render in the same pass so all four surfaces agree instantly.

Pass 3 — Hardening

  • 375px: masthead measured (wordmark right 182 vs call left 318), zero overflow offenders, sticky bar present, tall layout selected. Native-res 375 screenshots at rest and at balayage-long-all-on showed the badge covering the top of the hair when the note was on — added the mobile note template ("extra half hour booked — in the price"), reduced the mobile price size, moved the tall head down 8 units; badge/hair intersection now under 5% at rest and clear with the note.
  • 1000px mid-width check: badge right-anchored at 8% inside the mirror, no intersection with the hair (measured 0 px²).
  • Label-collision sweep at 2× on wide (rest, foils-long-all-on, cut-short, global-medium): tag, service label, badge and marks all clear; bond bowl moved left on the trolley so the brush no longer crosses the dryer.
  • Reduced motion: settled frame at t = 1.7 s, layers shown without stagger, hair opacity 1; probe settles in 20 ms. Visibility pause and resize re-layout (wide ↔ tall) verified. Zero console errors across load, every drive and the rapid-fire run (ten dispatched clicks in one evaluate → settled on global/long/toner ON/bond ON/kit ON = $400).
  • Form: hard reload → empty submit raises four field errors and the panel stays hidden; filled submit shows the panel. Keyboard arrows move the length chips. No pattern attributes — phone validation is JS.
  • Removed one ornament: the map's freeway hint line. Final copy read; harness 122/122 across index and guide — the guide probed for one h1, 375 overflow, table-scroll wrappers, colophon and the would-ship panel.

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.