UX patterns

The flows that
keep recurring.

A component is one part. A template is a whole starting file. A pattern is the flow in between: the handful of components that always travel together, the states the flow has to survive, and the one rule that must not break.

06 — Patterns

How to use one

Patterns are recipes, not files. Nothing here is copied wholesale, and nothing here asks the reader to pick a pattern — that is our job before the page exists.

  1. Find the pattern before writing the page. If one fits, its invariants are already decided and you do not get to re-decide them.
  2. useWhen and avoidWhen are the match test. Keyword overlap is a candidate, not a fit.
  3. Build every state listed, not just the happy one. A flow that has no empty, error and pending state is not finished, it is demoed.
  4. Invariants are not adjustable. The adaptable list is what you may change: wording, order, density, how many columns.
  5. Run the verification list at the bottom of this file before calling any pattern done.
06 — Patterns

13 patterns

Match on use when and avoid when, not on the title. Everything under invariants is already decided.

Tonally alternating page

tonal-rhythm-page
web
Use when

Any new marketing or product page longer than two sections.

Avoid when

A single-purpose surface with no scroll: the password gate, a 404, one deck slide.

Slots
heropositioningproofdetail bandsclose
States — build every one
initial paintreveal in progressreduced motionlight banddark band
Data it needs
section orderwhich bands are lightone closing action
Invariants — not adjustable
  • Dark and light bands alternate. Never two dark bands in a row where a light one would fit.
  • A light band is .section--light, or body.theme-light for a whole light page. Not a one-off background colour.
  • Anything placed inside a light band has to survive it: inline-coloured SVGs, dark cards and yellow text all break there.
  • Yellow is an accent on one phrase, never body text, on any band.
Adaptable
how many bandsband orderdensitywhich band carries the photography
How it usually breaks

Hardcoded-colour SVGs and dark cards survive the band switch and leave a dark hole in a light band. Scroll the whole page in both themes before shipping.

Rule

Alternate dark and light down every page. A continuous black page is the single most recognisable tell of generated design.

src/pages/index.astro · src/styles/index.css · .section--light

Form that lands somewhere a human will see it

form-to-slack
web
Use when

Any form, anywhere, without exception.

Avoid when

Never. A form with no notification path is not shippable.

Slots
fieldsconsentsubmitoutcome
States — build every one
emptyinvalid fieldsubmittingdelivereddelivery failedduplicate submission
Data it needs
field setdestination channelwebhook from env or Script Properties
Invariants — not adjustable
  • Every form posts to Slack #submissions. Standing rule, not a per-form decision.
  • Pages Functions route through functions/api/_slack.js. Apps Script reads the webhook from Script Properties.
  • No webhook URL is ever committed.
  • Delivery failure is shown to the user, not swallowed. A silent failure looks identical to success and costs a lead.
  • The user sees an on-screen confirmation, not just an email.
Adaptable
field seta sheet or Resend audience alongside Slackwording of the confirmation
How it usually breaks

The function returns 200 while the webhook 404s, so submissions vanish quietly. Assert on the Slack response, not the function's own exit.

Rule

Wire Slack before shipping the form, not after the first lost lead.

functions/api/_slack.js · functions/api/recommend.js

Double opt-in signup

double-opt-in
webemail
Use when

Collecting an email address for anything we will later send to.

Avoid when

A one-off enquiry that gets a human reply. Confirming twice there is friction with no benefit.

Slots
signup formpending noticeconfirmation emailconfirmed state
States — build every one
emptysubmitted and pendingconfirmation sentlink expiredalready confirmedsend failed
Data it needs
emailsigned confirmation tokenaudience to add to on confirm
Invariants — not adjustable
  • Nothing reaches a sendable audience before the confirmation link is clicked.
  • The confirmation token is signed and expires. An unsigned id in a URL is an open subscribe endpoint.
  • Re-submitting an address that is already confirmed is idempotent, not a duplicate record.
  • The pending state says plainly that an email is on its way, so nobody submits four times.
  • Email imagery uses absolute https URLs and inlined brand hexes. Mail clients have no custom properties.
Adaptable
which Resend segments to add tocopy of the confirmation emailexpiry window
How it usually breaks

A live Resend key in .dev.vars means wrangler pages dev sends real email during local testing. Blank it before testing.

Rule

Pending is a real state with real copy. Most double opt-in flows fail because nobody designed the middle.

functions/api/waitlist.js · functions/api/waitlist/confirm.js · functions/api/_waitlist-lib.js

Long intake that sizes the opportunity

scoping-intake
web
Use when

A prospect has to describe a workload before we can quote, and the answers imply a tier or shape.

Avoid when

A short contact form. Twenty questions in front of a stranger kills the conversion.

Slots
context questionsworkload questionsconstraintscontactoutcome
States — build every one
emptypartially completeinvalidsubmittingreceivedderived tier computed
Data it needs
question setderivation rules for tier and shapedestination sheet plus Slack
Invariants — not adjustable
  • The derived tier, seat count and sizing summary are internal. The submitter sees a thank-you, never their own score.
  • Derivation lives in one place, server side. A tier computed in the browser is a tier a prospect can edit.
  • Questions a sales call can answer faster do not belong in the form.
  • Paired institutional and individual variants stay in step — a question added to one is considered for the other.
Adaptable
question wording and orderhow many stepswhich answers feed the tier
How it usually breaks

Triage copy leaks into the confirmation screen and the prospect reads their own qualification band.

Rule

Collect generously, show nothing back. The triage is ours.

university-scoping/ · researcher-scoping/ · integrations/feedback-sheet/

Gated confidential surface

gated-surface
webdeck
Use when

A surface carries pricing, investor material, internal costing or a named customer.

Avoid when

Public marketing. A gate in front of a brochure just loses the reader.

Slots
lock screenerrorunlocked content
States — build every one
lockedwrong passwordunlockedsession expireddirect deep link while locked
Data it needs
password hashthe middleware route prefixwhich paths are inside the gate
Invariants — not adjustable
  • The gate is enforced in middleware. Content is not sent to the browser until the password is correct — a client-side gate ships the secret and hides it with CSS.
  • Only the hash is committed. Generate it with npm run deck:hash.
  • A deep link into a gated path hits the gate, not a half-rendered page.
  • URL and password travel through different channels.
  • The default for a new confidential surface is locked. Opening it is the decision that needs a reason.
Adaptable
gate copysession lengthwhether a deck or a document sits behind it
How it usually breaks

A new page added under a gated prefix but outside the middleware's path list is public and looks gated.

Rule

Pricing, investor material or a named customer means gated. Locked is the default.

functions/investors/_middleware.js · functions/decks/_middleware.js · src/styles/deck.css · .gate

One figure, many surfaces

figure-synced-surfaces
webdeckdocument
Use when

A number appears on more than one surface — deck, deal sheet, memo, FAQ, website, docs.

Avoid when

A figure that genuinely lives on one page and nowhere else.

Slots
source of trutheach consuming surfacethe drift check
States — build every one
in syncdriftedretired value still presentpublished copy stale
Data it needs
the canonical JSONwhich surfaces carry which factretired values with a why and a now
Invariants — not adjustable
  • The number changes in its source file first — investor/facts.json for deal figures, products.json for the offering. Never on a surface.
  • A superseded figure moves into retired with a why and a replacement. That is what stops it creeping back.
  • The drift check runs in the build. A figure that two surfaces disagree about fails CI rather than reaching a reader holding both.
  • A change on one surface means sweeping every surface that repeats the claim. Spot-checking and calling it clean is how contradictions ship.
  • Generated public copies are regenerated, never hand-edited.
Adaptable
how the figure is presented per surfacewhich surfaces carry itrounding and units per medium
How it usually breaks

The source JSON is updated and publish.sh is forgotten, so public/ still serves the old number. The check catches exactly this.

Rule

Source file first, surfaces second, drift check third. Never the other way round.

investor/facts.json · npm run investors:check · products.json · investor/publish.sh

Claim with its evidence attached

proof-before-claim
webdeckdocument
Use when

Asserting anything a sceptical reader would want backing for: capacity, demand, cost, timeline, traction.

Avoid when

Statements of intent and positioning, which are honestly ours and need no citation.

Slots
claimevidencethe fair objectionwhat we do not yet know
States — build every one
sourcedfirst-partyunsourced and therefore unshippablecontested
Data it needs
the figureits sourcethe date it was true
Invariants — not adjustable
  • Every external figure carries a source. An unsourced number is removed, not softened with 'approximately'.
  • Concede the fair objection on the page. A reader who finds the hole themselves stops believing the rest.
  • Retired claims stay retired. Specifically: the Naivasha $1bn project never happened and is never cited.
  • First-party evidence is labelled as ours, not dressed up as independent.
Adaptable
which evidence to lead withhow prominent the objection isstat or sentence
How it usually breaks

A plausible figure is carried over from a draft deck with no source and then quoted back to us.

Rule

No source, no claim. Name the objection before the reader does.

src/pages/infra-africa.astro · src/data/faq.js

Video inside a page

video-placement
web
Use when

A short first-party clip carries the argument better than another text band.

Avoid when

Decoration. A clip that explains nothing is 4 MB of nothing.

Slots
poster framevideotitle and descriptionthe band it sits in
States — build every one
poster onlyin view and autoplaying mutedpausedreduced motionmedia-only with no caption
Data it needs
mp4 pathposter pathtitle and description, or mediaOnly
Invariants — not adjustable
  • Always through VideoFeature, never a bare <video>.
  • Always a poster. preload is none, so without a poster the slot is a black box until it scrolls into view.
  • H.264 MP4, 720x1280 portrait, 600-800 kbps, 25 fps. The frame renders about 330px wide — 1080p is wasted bytes.
  • Video lives in public/. Astro does not transcode it, so it never goes in src/assets.
  • A clip that no page references gets deleted. It still ships.
Adaptable
left or right alignmentmediaOnly versus titledwhich band it sits in
How it usually breaks

A poster is generated with ffmpeg's webp encoder, which this build lacks, and ships as a 0-byte file. Extract a PNG frame and convert with sharp.

Rule

No poster, no ship. Check assets.json for the clip before shooting a new one.

src/components/VideoFeature.astro · public/assets/video/

Partner logo wall

partner-strip
webdeck
Use when

Showing the ecosystem around a layer of the stack.

Avoid when

Implying a partnership that is not signed, or crowding a wall with logos nobody recognises.

Slots
layer labellogo rowper-partner link
States — build every one
full rowmissing logo falls back to monogramunconfirmed partnersingle-logo row
Data it needs
partner list from src/data/partners.jslogo file named by sluglayer assignment
Invariants — not adjustable
  • Logos resolve by slug. The filename is the contract — rename it and the partner silently becomes a monogram tile.
  • Partner marks are not ours: never recoloured, never given the signal accent, never placed on ink where a dark mark disappears.
  • A partner marked unconfirmed does not go live with a fabricated quote or a borrowed claim.
  • The logo wall sits on a light band. Most partner marks are drawn for white.
Adaptable
row countordering within a layerwhether logos link out
How it usually breaks

The eager glob over the partner folder processes every file in it at build time, including team photos no slug ever requests.

Rule

Slug names the file. Never restyle someone else's mark.

src/data/partners.js · src/pages/partners.astro · src/assets/partners/partner_logos/

External deck

deck-narrative
deck
Use when

A standalone deck for investors, a customer, or a partner.

Avoid when

Something that wants to be a page. A deck that scrolls is a page with worse typography.

Slots
coverproblemstackproofmoatsofftakersteamaskclose
States — build every one
gatedunlockedkeyboard navigationprint to PDFmobile
Data it needs
deck definition in src/data/decks/figures from their source of truthpassword hash if confidential
Invariants — not adjustable
  • The stack in a deck mirrors the live stack. A deck that describes last quarter's offering contradicts the site.
  • Decks derived from products.json change through a reviewed pull request, never a silent regeneration.
  • Anything with pricing or a named customer is gated.
  • The PDF is generated, not hand-assembled, so slides and pack cannot diverge.
  • Figures come from facts.json, and the drift check covers the deck too.
Adaptable
slide orderwhich proof to lead withlengthaudience-specific framing
How it usually breaks

The deck is edited directly, products.json is not, and the next regeneration silently reverts the edit.

Rule

The deck follows the offering, not the other way round.

src/pages/decks/[slug].astro · src/data/decks/index.js · investor/make-deck-pdf.sh

HTML document that prints as A4

document-to-pdf
document
Use when

Something a reader will download, print, or attach — a proposal, memo, case study, costing.

Avoid when

Content that belongs on a page. A PDF is a dead end for anything that changes.

Slots
coversectionstablesrostercolophon
States — build every one
screenprintpage break mid-tableinternal-only variant
Data it needs
source HTMLfigures from their source of truthwhether an internal costing variant exists
Invariants — not adjustable
  • Print has no dark ramp. The document surface defines ink, charcoal, signal and paper only — a dark band does not survive a printer.
  • Tables declare their own break behaviour. A specification split across a page boundary is unreadable.
  • An internal costing variant is named so it cannot be sent by accident, and it never shares a filename stem with the shareable one.
  • The PDF is regenerated from the HTML. A hand-edited PDF is a second source of truth.
Adaptable
section orderhow many tablesgeneric versus named-customer variant
How it usually breaks

The offering and the costed variant are regenerated from different source states, so the numbers disagree between two files in the same folder.

Rule

Generate the PDF, never hand-edit it. Keep the internal variant obviously internal.

investor/make-pdf.sh · meru-lab/ · pausti/

Input-to-recommendation tool

sizing-estimator
web
Use when

A prospect can answer a few questions and get a genuinely useful answer without talking to us.

Avoid when

The honest answer is 'it depends'. A tool that always says 'contact sales' is a form with extra steps.

Slots
inputsderived recommendationassumptionsnext step
States — build every one
defaultsadjustedout of rangeno sensible recommendationrecommendation shown
Data it needs
the sizing maths and where it came fromhardware tiersstated assumptions
Invariants — not adjustable
  • The maths is named and sourced. A black-box recommendation is not credible to the engineer using it.
  • Assumptions are shown next to the answer, not buried in a footnote.
  • Out-of-range inputs say so rather than extrapolating into a number nobody should quote.
  • The tool does not become a lead gate. The answer appears first; the contact step is optional.
Adaptable
which inputs are exposedtier definitionshow the answer is presented
How it usually breaks

Hardware tiers drift from what we actually quote, and the tool recommends a configuration sales cannot sell.

Rule

Show the assumptions. An unexplained number from a vendor's own calculator convinces nobody.

src/pages/right-fit.astro

Referral link with attribution

attributed-referral
web
Use when

Someone outside Navon sends us a lead and expects credit for it.

Avoid when

Ordinary marketing traffic. Attribution on everything is tracking, not a programme.

Slots
referral linkattribution capturethe form it lands oncredited submissionreferrer's view
States — build every one
no referrerreferrer capturedattribution expiredself-referralcreditedagreement unsigned
Data it needs
referrer codeattribution windowsigned agreement state
Invariants — not adjustable
  • Attribution is captured server side at the edge, not read back from a query string at submit time.
  • A referrer cannot see other referrers' leads. The dashboard is scoped by the authenticated code.
  • Credit requires a signed agreement on file. An unsigned referrer collects attribution but not a payout claim.
  • The lead still reaches Slack #submissions like any other form.
Adaptable
attribution windowwhat the referrer dashboard showswhich forms participate
How it usually breaks

Attribution is read from the URL on the submitting page, so a lead that navigated anywhere in between loses its referrer.

Rule

Capture at the edge on arrival. By the time the form submits, the referrer is gone.

functions/affiliates/_middleware.js · functions/api/_affiliate-lib.js · functions/api/affiliate/
06 — Patterns

Before you call it done

The same list for every pattern. A flow that has only its success state built is not finished, it is demoed.

  1. Dark and light bands alternate, and nothing inside a .section--light band carries a hardcoded dark colour.
  2. Every state renders: loading, empty, error, success, and whatever the pattern adds.
  3. Keyboard only: reachable, visible focus, Escape closes anything that opened.
  4. Mobile at 375px with no horizontal scroll, and the nav drawer restores focus on close.
  5. Reduced motion honoured — reveals land in place rather than never arriving.
  6. Every image is the right pipeline for where it lives, and carries the alt text from assets.json.
  7. Every form reaches Slack #submissions, and the user sees confirmation on screen.
  8. Any figure that also appears on another surface came from its single source of truth, and npm run investors passes.
06 — Patterns

Never

Six things that have each gone wrong at least once.

  1. A continuous slab of black down a whole page — it reads as generated.
  2. A flow shipped with only its success state built.
  3. Two surfaces carrying the same number from two different places.
  4. A form that writes to a sheet and nowhere else.
  5. Client-side-only gating in front of anything genuinely confidential.
  6. A second source of truth introduced because the first one was inconvenient.
Navon Design System
Signal · v2.2.0 · 2026-10-02 · Naivasha, Kenya
00°42′S 36°26′E
← Back to site