Contact form

The contact form: name, email, organisation, role, a topic, the message and consent, with Turnstile; stores it through a Pages function in the D1 table contact_requests, where the owner reads it (nothing is emailed). The promise sits beside it.

Organism · beta · v1.0.0 · owner: Site system lead · contract 19 of 20 points (open: figma)

Do and don't

Do: The form

Rendered by contact-form.ts

Open example: Contact form · Do: The form Use the full-page example to follow page links.

As it ships.

Do: With its promise

Rendered by contact-form.ts

Open example: Contact form · Do: With its promise Use the full-page example to follow page links.

The reader knows who replies and what happens to the data.

Don't: Embedded elsewhere

Rendered by contact-form.ts

Open example: Contact form · Don't: Embedded elsewhere Use the full-page example to follow page links.

Own page.

Don't: Without its privacy note

Rendered by contact-form.ts

Open example: Contact form · Don't: Without its privacy note Use the full-page example to follow page links.

Say what happens to the data.

Rules

RuleWhyCheck
Contact form is computed by Loom's code from the visitor's own message, sent to POST /api/contact (functions/api/contact.ts) after a Turnstile check, not drawn by hand.A showcase is proof only if the product's own code produced what it shows.Automated: scripts/site/interactives/check.ts (recomputes the showcase data and fails on drift)
The form states what happens to the reader's data next to where they enter it.Trust: nothing is sent without the reader knowing.Automated: tests/site/contact-form.spec.ts

API

No props.

Contract details

The full contract: identity, anatomy, variants, states, tokens, layout, motion, accessibility, content, tests

Identity

AspectValue
Idcontact-form
LayerOrganisms
Categoryapp
OwnerSite system lead
Statusbeta
Version1.0.0
Sourceapps/showcase/site/system/organisms/contact-form.ts (private repository; the contract and the render function are this one module)
VersionDateChange
1.0.02026-10-26Contact form: computed by Loom's code from the visitor's own message, sent to POST /api/contact (functions/api/contact.ts) after a Turnstile check.

Use when

  • /contact/ and /ja/contact/. Why: No email address is published: the form is the way in.

Do not use when

  • Embedded in a marketing page. Why: Pages link to /contact/; the form keeps its own page and script.

Anatomy

PartWhat it is
introsection.contact-intro: h1, lede, privacy line
panelsection.panel.panel-form: required note, error summary, status, the form (fields, topic choices, message, honeypot, consent, spam check, submit), done message

Made of: showcase-frame, panel, form-field, button, heading, text.

Used in: task-body, page-apps.

Variants

VariantWhenWhy
fullOn its demo page and its section.The whole experience where the claim is made.

States

StateRendered or not applicable
defaultBuild-time contact fields, required/privacy notes and hidden confirmation. contact/contact.js owns validation and submission on its full task page.
hoverNative fields, choices, links and submit button use their own hover affordances.
focus-visibleTask controls show focus. Invalid submission focuses the linked error summary; successful submission focuses the confirmation heading.
activeThe reader chooses a topic/consent and submits; there is no timed playback or Pause control.
disabledThe real submit button is natively disabled while POST /api/contact is pending, then re-enabled after a failed send.
loadingTask-owned spam-check status and pending submit label/live status are separate from component mounting. The form remains build-time markup; it has no managed showcase initializer.
emptyBlank required fields remain editable; submit creates associated field errors and a focused summary. Optional company/role may remain empty.
errorInvalid fields use aria-invalid and linked descriptions. A failed request never shows success and retains the message for retry. Spam failures expose the existing retry control. Boot errors are logged; no host remount recovery is promised.
currentThe selected topic radio and consent checkbox expose their native checked state; no current-frame slider exists.
visitedNot applicable: No visited state on this root; any contained native links retain their own browser history.
expandedNot applicable: No disclosure state on this root; replacing a view is not aria-expanded.

Tokens

PartToken
panel.background--surface
field.border--line-strong
error.color--madder-ink
--contact-form-gap-grid-gapvar(--grid-gap)
--contact-form-padding-space-9var(--space-9)
--contact-form-padding-space-8var(--space-8)
--contact-form-font-type-h2var(--type-h2)
--contact-form-letter-spacing-ls-h2var(--ls-h2)
--contact-form-margin-top-space-7var(--space-7)
--contact-form-gap-space-5var(--space-5)
--contact-form-gap-space-9var(--space-9)
--contact-form-color-field-fg-2var(--field-fg-2)

Layout

AspectValue
breakpointsRequired review at 390, 768, 1024 and 1440 px; execution is not implied by this declaration; columns stack under 768 px and controls fill the row on phones.
wrappingText wraps; code panes scroll inside their own focusable region.
textExpansionJapanese strings come from the i18n tables and wrap; engine output stays English.

Spacing and rhythm

AspectValue
internalInside the Showcase frame's padding, controls and caption at its foot.
externalFull width of its section.
rhythmOne showcase per section, after the section's claim (D-SITE-008).

Motion

AspectValue
NoneThis task form has no intrinsic timed sequence or autoplay; native focus/hover rules and page-shell entrance policy remain separate.

Runtime and reuse

  1. Render-only element; mount any registered child enhancements separately. Preserve required Site CSS, fonts, responsive and locale rules from nativeElement.

Accessibility passport

AspectValue
roleNative form with labelled fields, topic fieldset, error summary, status and confirmation sections.
nameIts heading or caption
keyboardTab through fields/choices/links/buttons; Enter submits; the existing spam-check retry is keyboard operable.
focusValidation focuses the linked error summary; successful send focuses the confirmation heading.
screenReaderRequired labels, associated invalid-field messages, task statuses and confirmation; no playback announcements.
contrastInk on surface 14:1; state dyes only as marks with words.
targetControls are 44 px.

Content

SlotLengthVoice
label4 wordsPlain, second person

Good: “Your message”

Bad: “Submit inquiry”

Japanese: Japanese strings from contact/i18n/ja.json, filled at build time on /ja/contact/.

Icons and illustrations

None: this element uses no icon or illustration of its own.

Decisions and exceptions

Usage

8 uses across the built pages (measured by npm run site:system).

Tests

AspectValue
unittests/unit/site-system.test.ts: renders every do and don't example through the registry and checks the classes it owns
visualNot yet: there are no screenshot baselines; the Library page is checked for sideways scroll at 1440 px by tests/site/system.spec.ts
a11ytests/site/system.spec.ts: axe (0 serious or critical) on the element's Library page in light and dark
parityscripts/site/system/check.ts: Library examples are rendered by the same render function the site imports

Figma

not-yet: The Loom Site Figma library is the last stage of the plan; this element is not mapped yet.

Open in Storybook

Start with a conversation about your system.

We read your system, show you what we found, and agree with your team what to fix first.