Generative Assets · Industry Demos XXV · site 339
How the Clearwater Speech Pathology demo was built
A fictional Como speech pathology clinic — paediatric and adult, in clinic, at home and school, and by telehealth across Perth's southern suburbs. The page has one job: an initial-appointment booking with the term plan already priced, and because the audience is NDIS-adjacent, the accessibility floor was written into the spec rather than left to judgement.
Concept
Parents of a four-year-old nobody outside the family can follow, plan-managed teens without a therapist, adults finding words again after a stroke, and teachers saying "get that checked". They ring around three clinics and book the one that tells them what a term costs before they've dialled. So the signature is a Term Board — a drawn ten-week strip that fills with speech-bubble sessions as you plan, with the price badge pinned to the week-zero clipboard — and every control on it pre-fills the booking form.
Palette
Argued from a bright clinic room: warm paper on the wall, deep-indigo ink, the violet of a speech-bubble sticker, and a sun sticker for marks. Contrast figures below are measured from the shipped tokens by WCAG arithmetic (spec-time hypotheses in brackets).
The ink/line split holds throughout: sun is for ticks, tails, rules and the outer glow of the focus ring; anything set in type at sun hue is sun-ink. Violet is deep enough that no two-token remedy was needed for white-on-violet buttons; the deeper #5A3C97 is a hover state, not a contrast fix.
Type
Outfit 700 for display and buttons — a rounded geometric that reads to a child and a grandparent at the same size, with a wordmark that sits comfortably in a 62px masthead. Nunito for body at 17px on mobile and 18px from 760px (the a11y floor is ≥17px). The typographic moment is the term line set huge in uppercase Outfit over the board — EIGHT SESSIONS, ONE TERM — with the count re-set in words by the planner (Two … Ten). Scale: 0.86 / 0.95 / 1.04 / 1.35 / 2.3 / 2.75 rem, with the badge price at 1.75rem so it never fights the term line.
Techniques
- SVG, built in JS: the whole board is generated per state — clipboard, ten cells, bubbles, car glyphs, legend — into one
<svg>. Two viewBoxes: 900×520 wide (clipboard left, 5×2 grid) and 360×532 narrow (clipboard strip on top), switched by aResizeObserveron the stage below 560px. - Hand-drawn wobble: cell and clipboard outlines are rounded rects with a deterministic ±0.8-unit jitter per corner seeded by week number, so the board reads drawn without any raster.
- Bubble pop-in: each token is an outer
<g transform>for position with an inner<g class="pop">that scales 0→1 in CSS (cubic-bezier(.34,1.56,.64,1), transform-box fill-box) — position attribute and CSS animation on separate elements (W16/W17). Dual speed: first draw staggers 62ms/token capped at 1.3s; every later redraw uses 20ms/token capped at 480ms with a shorter transition. Every handler bumps a token BEFORE mutating state, and each queued timeout checks the token, so a rapid-fire chip drive can't finish a stale entrance on the new board. - Sound-wave squiggle: nine-point polyline inside every bubble, rebuilt each frame from
sin(i·1.35 + t·3.1 + phase)with a sine envelope; opacity is0.775 + 0.225·sin(t·1.6 + phase), so the floor is exactly 0.55 and the board is visibly alive at any capture instant. rAF delta ismin(max(now − last, 0)/1000, .05), paused ondocument.hidden, static at t=0.7 under reduced motion. - Price badge on the artwork: an HTML badge absolutely positioned on the clipboard's clip (rotated −2°), so it wraps declaration text properly; a narrow variant drops the long subtitle and sits on the right of the clipboard strip. Both its box and the board's intersect the first 750px at 1200 wide.
- Plain-text mirror: an ordered list under the board restates every cell (week 0 to 10, off weeks, declaration, total) and the SVG's
aria-labelis regenerated per state — the board is decorative for a screen reader, the list is the content. - Keyboard: chips are
role=radioin roving-tabindex groups (arrows/Home/End move and select, Space/Enter select), toggles arerole=switch, the stepper is two labelled buttons; the focus ring is a 3px violet outline with a 7px sun glow. The locked visit switch isaria-disabled(still focusable, still explains itself) rather thandisabled. - No libraries. Google Fonts only. Service-area map is a hand-placed SVG of the coast, Swan and Canning rivers, a Como star and a hatched visit zone.
Pricing model (pinned anchors)
Private fee schedule; every fee sits under the current NDIS price limit for speech pathology, stated on the page. Totals are integer by construction (the weeks stepper is 4–10 in twos, so fortnightly sessions are always whole).
| Line | Anchor | Rule |
|---|---|---|
| Initial assessment & written report (60 min) | $290 | Toggle, live at every combination, ON at rest |
| Therapy 45 min, in clinic | $145 / session | Sessions = weeks (weekly) or weeks ÷ 2 (fortnightly) |
| Therapy 30 min, in clinic | $100 / session | Cheapest priced output: 2 sessions fortnightly over 4 weeks, no assessment = $200 → grid floor "from $100" |
| Telehealth 45 min | $135 / session | Locks the visit toggle OFF with reason "no travel — it's a video call"; switching back to an in-clinic service RESTORES the prior visit choice |
| Home / school visit | +$40 per visited appointment | Multiplies every session AND the assessment when it's on; ledger prints "N visits — $X" |
| Funding: Private | — | No declaration |
| Funding: NDIS (plan- or self-managed) | no repricing | Badge appends "claimable against your plan — no gap"; ledger and mirror print "NDIS: billed under the price limit for each item" |
| Funding: Medicare CDM referral | no repricing, never a number | Badge appends "less your Medicare rebate on up to five sessions a calendar year"; ledger prints "Medicare rebate: at the MBS schedule, processed on the day — up to 5 sessions per calendar year with a GP referral" |
| Groups & school programs | no number | "Priced per program — tell us the group size" (designed price-less card, not on the board) |
Worked examples — measured from the running page against an independent oracle
| Scenario | Arithmetic | Spec | Measured |
|---|---|---|---|
| Rest: 45 min · weekly · 8 weeks · assessment ON · visit OFF · private | 8 × 145 + 290 | $1,450 | $1,450 |
| Telehealth · fortnightly · 10 weeks (5 sessions) · assessment OFF | 5 × 135 | $675, visit disabled | $675, visit locked |
| 30 min · weekly · 6 weeks · assessment ON | 600 + 290 | $890 | $890 |
| 45 min · weekly · 4 weeks · assessment ON · visit ON | 580 + 290 + 5 × 40 | $1,070, "5 visits — $200" | $1,070, ledger reads "5 visits — $200" |
| Rest + NDIS chip | unchanged | $1,450 + declaration on badge and ledger | $1,450, both surfaces |
| Rest + Medicare chip | unchanged | $1,450 + declaration on badge and ledger | $1,450, both surfaces |
| Cheapest: 30 min · fortnightly · 4 weeks · assessment OFF | 2 × 100 | $200 | $200 |
| Lock/restore: 45 min · 8 wk · visit ON → telehealth → 30 min | 1,810 → 8 × 135 + 290 → 8 × 100 + 290 + 9 × 40 | restore prior visit | $1,810 → $1,370 → $1,450, visit restored ON |
Zero spec corrections: every worked figure was pure anchor arithmetic and matched the oracle first time. The one measured deviation from the spec is a contrast hypothesis, not a price — sun-ink measures 5.38:1 rather than the estimated ≈6:1 and still clears AA.
Toggle matrix
| Control | Therapy 45 min | Therapy 30 min | Telehealth 45 min |
|---|---|---|---|
| Assessment & report | live, +$290 | live, +$290 | live, +$290 (no visit fee on it — no travel) |
| Home / school visit | live, +$40 × (sessions + assessment) | live, +$40 × (sessions + assessment) | DISABLED — "no travel — it's a video call"; prior choice remembered and restored on the way back |
| Weekly / fortnightly | sessions = weeks / weeks ÷ 2 | same | same |
| Funding chips | declaration only | declaration only | declaration only |
Conversion decisions
Above the fold at 375
Wordmark, phone chip and (from 640px) a Book button in the masthead; eyebrow, h1 naming trade and area, "Book an initial appointment" + "Call" buttons, and three trust ticks all land inside 740px on a 375 screen. The paragraph is re-ordered below the CTAs on mobile so the promise beats the prose.
Board before form
The Term Board sits in the hero at desktop widths (board and badge inside the first 750px) because the price is the argument; the controls follow immediately below, and "Book this plan" hands the whole scenario — service, frequency, weeks, funding, total — into the form's textarea and funding select.
Form fields
Name, phone (v-flag-safe pattern), suburb, who it's for (child / teen / adult — a clinic triages by that first), funding select, preferred times, and the pre-filled plan textarea. Only the four the receptionist actually needs to ring back are required. Confirmation is a designed panel hidden until a valid submit; the callback promise near the form is static copy and not the confirmation.
Sticky bar
Sun "Call now" + violet "Book" fixed to the bottom under 760px; the footer carries extra bottom padding so nothing hides behind it. Speech-bubble tails on the primary buttons tie the CTAs to the board's tokens.
Local SEO we'd implement for the real business
The live page emits only WebPage / WebSite / BreadcrumbList schema naming it a demonstration — fictional business data in a real index is dishonest markup. For a real clinic we would ship the block below. The top-level type is MedicalClinic, an actual schema.org organization subtype (LocalBusiness → MedicalBusiness → MedicalClinic); Speech-style specialty tokens are MedicalSpecialty enumerations, not organization types, and belong in medicalSpecialty, never in @type.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "MedicalClinic",
"name": "Clearwater Speech Pathology",
"url": "https://www.example-clinic.com.au/",
"telephone": "+61 8 9000 0339",
"image": "https://www.example-clinic.com.au/clinic-room.jpg",
"priceRange": "$100–$290",
"medicalSpecialty": "SpeechPathology",
"address": {
"@type": "PostalAddress",
"streetAddress": "[street address]",
"addressLocality": "Como",
"addressRegion": "WA",
"postalCode": "6152",
"addressCountry": "AU"
},
"geo": { "@type": "GeoCoordinates", "latitude": -31.99, "longitude": 115.86 },
"areaServed": ["Como", "South Perth", "Manning", "Applecross", "Victoria Park", "Bentley", "Booragoon", "Melville", "Rossmoyne", "Bull Creek", "Willetton", "Cannington"],
"openingHoursSpecification": [
{ "@type": "OpeningHoursSpecification", "dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday"], "opens": "08:00", "closes": "18:00" },
{ "@type": "OpeningHoursSpecification", "dayOfWeek": "Saturday", "opens": "08:30", "closes": "13:00" }
],
"availableService": [
{ "@type": "MedicalTherapy", "name": "Initial speech pathology assessment and report" },
{ "@type": "MedicalTherapy", "name": "Speech and language therapy sessions" },
{ "@type": "MedicalTherapy", "name": "Telehealth speech pathology" }
],
"aggregateRating": { "@type": "AggregateRating", "ratingValue": "4.9", "reviewCount": "63" },
"review": [
{
"@type": "Review",
"author": { "@type": "Person", "name": "[verified client]" },
"reviewRating": { "@type": "Rating", "ratingValue": "5" },
"reviewBody": "[pulled from the practice's verified reviews feed]"
}
]
}
</script>
Plus: a Google Business Profile with the Como pin and telehealth service area, suburb-level FAQ answers marked up as FAQPage, and an NDIS/Medicare page each targeting the query the parent actually types ("speech pathologist NDIS Como").
What would change for the real client
- Booking straight into practice software: the "Request a callback" form becomes a live booking widget from Cliniko or Halaxy (both expose embeddable online-booking and appointment-type APIs), with the Term Board's plan attached to the appointment note.
- NDIS claiming: Splose for line-item invoicing to the price guide and plan-manager submission — the "billed under the price limit for each item" declaration on the board becomes the actual invoice template.
- Accounts: Xero synced from the practice system for reconciled invoices and Medicare bulk-bill/rebate tracking.
- Reviews feed from Google verified reviews replacing the demo copy; real photos of the clinic room and clinicians (with consent) where the drawn board and map stand in here; the real ABN, provider numbers, WWC and SPA membership numbers replacing the placeholder patterns.
- Fee schedule driven from the practice's item list so the board reprices when the NDIS price limit is updated each July.
Iteration log
Pass 1 — Craft
- The first composition put the board under a full-width hero; at 1200×750 only its top 60px made the fold. Rebuilt the hero as an 0.88/1.12 grid — copy left, term line + board right — so the board spans ~200–560px and the badge sits at ~250px in the thumbnail.
- Price badge collided with the clipboard's "Week 0 / Initial assessment" labels (and worse with the Medicare declaration). Moved the badge to the clip at 3%, tightened its padding and price size, dropped the redundant left caption, and started the clipboard copy at +210 units (two form lines, tick, "goals set for the term" all inside the sheet) so even the four-line Medicare badge and its tail clear "Week 0". Narrow layout: clipboard strip raised to 150 units, badge to the right of it, tick to the strip's bottom-right corner, right caption removed.
- Narrow cells were 176 units tall for 65 wide — pale and empty; cut to 122 and the bubbles read at 375.
- Measured contrast for every token pair (see palette); sun-ink recorded at 5.38:1 rather than the spec's ≈6.
- Masthead: phone number now stays visible down to 360px (wordmark right edge 153 vs call-chip left 209 at 375); hero eyebrow shortened so it holds one line at desktop; NDIS provider number wrapped at the hyphen in the footer — nowrap span.
- Harness (128 asserts): rest $1,450 on badge, ledger, probe and mirror; nine oracle scenarios each set the full control vector; lock/restore chain $1,810 → $1,370 → $1,450; keyboard drive of chips (arrows/Home/Space/Enter), stepper, switches, FAQ; rapid-fire 30-click drive twice; empty then filled submit via
button[type=submit]; reduced-motion settled frame; 375 masthead, fold, sticky bar, overflow; zero console errors.
Pass 2 — Depth
- Badge "bump": a 380ms scale pulse on every price change (class toggled from the readout, removed on
animationend, off under reduced motion) so the number visibly reacts to a chip tap without redrawing. - Second-read detail: the term line's count word is underlined with a sun stroke that redraws its width to the word; and the off-week cells carry a faint dashed "home practice" tick — the plan isn't empty on those weeks, it moves home.
- Refined the pop ease from a hard overshoot to
cubic-bezier(.34,1.56,.64,1)at 500ms/340ms (entrance/snappy) so ten tokens landing in sequence read as a strip filling rather than a flicker. - Telehealth bubbles gained the drawn screen bezel + stand and a shorter, quieter squiggle so the three services are told apart at a glance without a legend.
Pass 3 — Hardening
- Label-collision sweep at native resolution on 1200 and 375 across private / NDIS / Medicare × visit ON × telehealth: badge over clipboard copy, tail over "Week 1", caption under badge — all cleared after the Pass 1 geometry; re-shot to confirm.
- Chanel rule: removed the dashed freeway hint from the service-area map — it read as a scratch through the Como star.
- 375: scrollWidth 375, no offenders outside the SVGs, body 17px, tap targets ≥46px, sticky bar clear of the form's submit; guide tables in
.table-scroll. - Reduced motion: no pops, no squiggle loop, hero rise disabled, smooth-scroll off; the settled frame is identical to the 7s state.
- rAF: dt clamped [0, 50ms], loop parks on
visibilitychangeand restarts on return; ResizeObserver only redraws on a layout crossing, not every resize tick. - Final copy read; confirmation copy stays in the "thanks / received / we'd call" family; declaration strings checked character-for-character against the spec on badge, ledger and mirror.
This is a demonstration build by Sapience Analytics. Want one pointed at your customers?