@usgb/forms
v1.0.1
Published
Shared USGB lead-form package for PWA and WordPress
Keywords
Readme
@usgb/forms
Shared lead and newsletter forms for the PWA (React) and WordPress (Web Component) hosts.
You pick a predefined formId. The package owns fields, validation, consent copy, tracking keys, and layout. You own the submit endpoint, legal-document URLs, heading/subheading/CTA copy, and thank-you behavior.
What you get
| Artifact | Path / export | Who uses it |
| ------------- | --------------------------------------------------------------- | ----------------------------------- |
| React package | @usgb/forms → dist/index.js | PWA / any React host |
| Stylesheet | @usgb/forms/usgb-forms.css | Required for React hosts |
| Web Component | dist/usgb-forms.js (also versioned usgb-forms.<version>.js) | WordPress and other non-React hosts |
| Catalog | @usgb/forms/manifest → forms-manifest.json | CMS pickers, docs, tooling |
The Web Component script injects the same CSS once on load. React hosts must import the stylesheet themselves.
Mental model
| You pass | Package already decided |
| -------------------------------- | ------------------------------------------------------------------------------------- |
| formId | Fields, required/optional rules, consent bundle, tracking set, layout family (kind) |
| adapter.submit | Where the payload goes (never an attribute or hardcoded URL) |
| legalUrls / *-url | Hrefs for package-owned consent copy |
| heading, subheading, ctaLabel | Marketing copy around the form |
| successMessage or successUrl | Thank-you UX after accept |
Hosts must not invent field lists, consent wording, or tracking keys. New forms are new catalog entries in this package, not host-built schemas.
Use listFormDefinitions() (or the manifest) when a CMS needs a picker of available formIds. Deeper catalog rules: src/definitions/context.md.
Available forms
| formId | Kind | Fields | Consent | Variants |
| -------------------- | ------------ | --------------------------------------------------------- | ------------------------------ | ------------------------------------------------------- |
| main-investors-kit | lead | First name, last name, email (required); phone (optional) | TCPA checkbox (lead-tcpa v1) | dense, large, sidebar, plus optional two-column |
| newsletter | newsletter | Email only | None | None — omit variants / variant |
Appearances for both: on-light | on-dark. Appearance only recolors the form for readability on a light or dark host background — it never sets a background, padding, or radius. The host container owns those.
kind is the layout family (lead grid vs inline newsletter row). The root exposes data-kind for CSS. Several catalog forms can share one kind (e.g. another lead kit that reuses the lead layout).
Install
Registry package name, scope, and CDN base URL are TBD — examples below use the working package name @usgb/forms and a placeholder CDN host. Replace them when publishing is finalized.
npm (React / PWA)
yarn add @usgb/forms
# or
npm install @usgb/formsThen import the component and stylesheet:
import { UsgbForm } from '@usgb/forms'
import '@usgb/forms/usgb-forms.css'Peer dependency: react and react-dom ^18 or ^19.
CDN (Web Component / WordPress)
Load the built IIFE (styles inject themselves):
<script src="https://cdn.example.com/usgb-forms/usgb-forms.1.0.0.js"></script>Exact CDN hostname, path, and versioning scheme are TBD. Prefer a versioned filename (usgb-forms.<version>.js) so hosts can pin releases.
Until the package is published, you can still develop against a local checkout (yarn add file:../usgb-forms or yarn link).
React (PWA)
import { UsgbForm, createStubAdapter, PWA_LEGAL_URLS } from '@usgb/forms'
import '@usgb/forms/usgb-forms.css'
;<UsgbForm
formId="main-investors-kit"
appearance="on-dark"
variants={['large', 'two-column']}
heading="Get My Free Guide"
subheading="Enter your details to receive the Main Investors Kit."
ctaLabel="GET MY FREE GUIDE"
source="cms-home"
campaign="spring-kit"
legalUrls={PWA_LEGAL_URLS}
successMessage="Thanks — we will send your kit shortly."
adapter={yourHostAdapter}
onAccepted={(payload) => {
/* analytics bridge */
}}
onFailed={(payload, message) => {
/* error bridge */
}}
/>Newsletter (no consent, no variants):
<UsgbForm
formId="newsletter"
appearance="on-light"
heading="Subscribe"
ctaLabel="SUBSCRIBE"
adapter={yourHostAdapter}
/>React props
| Prop | Required | Role |
| ------------------------- | -------------------------- | --------------------------------------------------------------------------------------------------- |
| formId | yes | Catalog key (main-investors-kit, newsletter, …) |
| adapter | yes | HostAdapter with host, getTrackingHints(), submit() |
| appearance | no | on-light (default) or on-dark |
| variants | no | Lead only. Default ['dense']. One of dense | large | sidebar, optionally add two-column |
| heading / subheading | no | Title and supporting copy |
| ctaLabel | no | Submit button label. Falls back to the catalog ctaLabel for that formId |
| source / campaign | no | Placement / campaign context in the payload |
| legalUrls | when consent requires them | Host CMS paths for legal links in consent copy |
| successMessage | no | Inline thank-you when successUrl is omitted |
| successUrl | no | Redirect after accept (analytics / onAccepted are invoked first; see Success behavior) |
| analytics | no | (event) => void for view / validation / submit / success / failure |
| className | no | Extra class on .usgb-form |
| onAccepted / onFailed | no | Callbacks with the submission payload |
Web Component (WordPress)
Load one script. Styles are injected automatically.
<script src="https://cdn.example.com/usgb-forms/usgb-forms.1.0.0.js"></script>
<usgb-form
```
form-id="main-investors-kit"
appearance="on-dark"
variant="large two-column"
heading="Get My Free Guide"
subheading="Enter your details to receive the Main Investors Kit."
cta="GET MY FREE GUIDE"
source="cms-home"
client-agreement-url="/client-agreement/"
privacy-policy-url="/privacy-policy/"
success-message="Thanks — we will send your kit shortly."
></usgb-form>Submit is not an attribute. Markup alone uses a stub adapter. Assign a real HostAdapter in script:
<script>
const el = document.querySelector('usgb-form')
el.adapter = {
host: 'wordpress',
getTrackingHints() {
// Cookies, UTMs, click ids — keys must match the package tracking registry
return {}
},
async submit(payload) {
const res = await fetch('/your-form-submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
if (!res.ok) return { status: 'failed', message: res.statusText }
return { status: 'accepted' }
}
}
el.addEventListener('usgb-form:success', (e) => {
/* e.detail is SubmissionPayload */
})
el.addEventListener('usgb-form:failure', (e) => {
/* e.detail = { payload, message } */
})
</script>Attribute map
| Attribute | React prop |
| ----------------------------------------------- | ------------------------------------------------------------- |
| form-id | formId |
| appearance | appearance |
| variant | variants (space-separated) |
| heading / subheading | heading / subheading |
| cta | ctaLabel |
| source / campaign | source / campaign |
| success-message / success-url | successMessage / successUrl |
| client-agreement-url, privacy-policy-url, … | legalUrls.* |
| host | Used only for the stub adapter before you assign el.adapter |
Host adapter
interface HostAdapter {
host: 'pwa' | 'wordpress'
getTrackingHints(): Partial<Record<TrackingKey, string | undefined>>
submit(payload: SubmissionPayload): Promise<SubmitResult>
}submitmust return{ status: 'accepted' }or{ status: 'failed', message?: string }.- The package never embeds Workato, Magento, or WordPress endpoints.
createStubAdapter('pwa' | 'wordpress')accepts every payload (playground / markup-only WC).- Tracking keys and destination names live in
forms-manifest.json. Hosts may supply values for registered keys; they must not invent new ones.
Payload shape (summary)
Successful submits send a nested envelope: formId, definitionVersion, submissionContractVersion, idempotencyKey, data (field values), optional consent, and context (host, source, campaign, page, tracking). Exact field and tracking sets come from the form’s catalog entry.
Consent and legal URLs
Consent bundles are versioned and package-owned (checkboxes and/or clickwrap notices). Hosts only supply document hrefs.
main-investors-kituseslead-tcpav1 and requiresclientAgreementandprivacyPolicy.- PWA Magento defaults:
PWA_LEGAL_URLS→/content/client-agreement,/content/privacy-policy. - WordPress must pass every URL required by the selected consent. A consent form with a missing required URL does not render.
- Checkbox values land in
consent.values(and mirrored indata). Notices have no boolean field; the payload still records consent definition id/version. - Distinct legal wording requires a new consent id/version in this package — not freeform copy from the host.
LEAD_CLICKWRAP_V1 (“By clicking the button…”) is in the catalog and needs five legal URLs (privacyPolicy, userAgreement, marketLossPolicy, electronicDisclaimer, termsOfSale). It is not attached to a formId until that recipe is chosen.
Success behavior
- Adapter returns
accepted. analytics({ type: 'success' })andonAccepted/usgb-form:successrun synchronously. The package does notawaitthem.- If
successUrl/success-urlis set, the page callswindow.location.assignimmediately after those callbacks return. - Otherwise the form shows
successMessage/success-message, or a package default thank-you line.
Redirect vs analytics
Invocation order is fixed (hooks first, then navigate). Delivery is not. location.assign starts unload in the same turn, so a typical GTM / gtag / dataLayer.push / image pixel started in analytics or onAccepted can be cancelled before the beacon leaves — especially on Safari and slow networks. Same for async work inside a usgb-form:success listener: the CustomEvent is dispatched synchronously, but anything it schedules after return can die with the document.
This package does not wait on a returned Promise, add a flush timeout, or call sendBeacon on the host’s behalf. A hung analytics callback must not trap the user on the submit screen.
What is reliable today:
- Inline thank-you (
successMessageonly) — no navigation, so tags usually complete. - Same-document hash redirects (
#thank-you) — no unload. - Host
analyticsthat usesnavigator.sendBeaconorfetch(..., { keepalive: true }). - Conversion measured on the thank-you page itself, not on this
successevent.
submit_attempt is safer than success because it fires before adapter.submit and has the round-trip to flush. Prefer the thank-you page (or a beacon) when successUrl is a full navigation.
Styling
Light DOM with stable usgb- class names (not CSS-in-JS). Isolation comes from the prefix and from styling .usgb-field-control / .usgb-btn instead of bare input / button. Host theme rules can still win; load host CSS after this package’s stylesheet and override under .usgb-form.
| Host | How styles load |
| ------------- | ---------------------------------------------- |
| React | import '@usgb/forms/usgb-forms.css' |
| Web Component | Injected as <style id="usgb-forms-css"> once |
Prefer custom properties on .usgb-form for color, type, and spacing:
.usgb-form {
--usgb-button-background: #123456;
--usgb-field-border: #cccccc;
}
.usgb-form[data-appearance='on-dark'] {
--usgb-button-background: #f4e08e;
}Use class selectors when a token does not exist (.usgb-form .usgb-btn { border-radius: 0; }).
Stable hooks: .usgb-form (data-appearance, data-variant, data-kind), .usgb-form-heading, .usgb-form-subheading, .usgb-form-grid, .usgb-form-inline, .usgb-field-control, .usgb-field-error, .usgb-consent-*, .usgb-btn, .usgb-form-status-*.
Semantic tokens (--usgb-field-*, --usgb-button-*, --usgb-newsletter-underline, …) are the first place to retheme. Palette vars (--usgb-blue-1, …) are a PWA snapshot. Error and checkbox glyphs are inline SVGs that use currentColor — override --usgb-alert-danger or --usgb-checkbox-mark to recolor them.
Appearance never paints a card. Put the form on a dark host surface and pass appearance="on-dark" (or on-light on a light surface):
<div style="background: #001f3d; padding: 2rem; border-radius: 0.75rem">
<usgb-form form-id="main-investors-kit" appearance="on-dark" …></usgb-form>
</div>Visual styles are a ported snapshot of the PWA form kit. When PWA form styles change, update this package and bump the version.
Develop (package maintainers)
yarn install
yarn dev # Vite playground (tries :5174)
yarn build
yarn typecheck
yarn lint
yarn formatPrettier and ESLint match magento-frontend (4-space indent, single quotes, no semicolons, print width 80).
Git
Local repo only. Connect a GitHub remote later when ready to publish.
