@roofmaxx/form
v0.103.0
Published
This document covers both **how to install and configure** the form on any website and **how the form works internally** in full technical detail.
Readme
Roof Maxx ReactJs Form
This document covers both how to install and configure the form on any website and how the form works internally in full technical detail.
The form is a multistep lead-capture widget built with ReactJS + Tailwind CSS, integrated with three external services:
- HubSpot Forms API — lead submission and CRM
- Roof Maxx Connect
map_statusAPI — real-time dealer lookup by location - Google Maps Places API — address autocomplete with city/state/ZIP extraction
- Zippopotam.us API — ZIP code existence validation
Table of Contents
- Installation
- Usage & Configuration
- UTM Priority Logic
- Examples of Use
- How the Form Works — Step by Step
- Conditional Field Logic
- State & Data Persistence
- Dealer Finder Logic
- HubSpot Submission
- Redirect Logic
- Google Maps Integration
- Component Architecture
- Styling & Theming
- Build & Development
- Third-Party Services Reference
Installation
-> STEP 1 — Add the script tag to your page
Latest Files
Copy and paste the codes below preferably in the <head> of your page:
<script type="module" src="https://unpkg.com/@roofmaxx/form/dist/form.js"></script>If you are facing issues with the form try inserting the JS script (not the CSS) in the footer of your page OR add the defer value (defer means the script executes after the document has been parsed)
<script defer type="module" src="https://unpkg.com/@roofmaxx/form/dist/form.js"></script><link rel="stylesheet" href="https://unpkg.com/@roofmaxx/form/dist/form.css" />Specific Version Files
To pin a specific version, specify it in both the script and link tags:
<script type="module" src="https://unpkg.com/@roofmaxx/[email protected]/dist/form.js"></script><link rel="stylesheet" href="https://unpkg.com/@roofmaxx/[email protected]/dist/form.css" />-> STEP 2 — Register your domain
Please send an email to [email protected] with your base domain where the form will be installed. For example if you have the form installed at www.domain.com/test or www.test.domain.com please send us domain.com.
Usage & Configuration
Place the following HTML at the location on your page where you want the form to appear:
<div id="roofmaxx-form"
data-utm-source=""
data-utm-campaign=""
data-utm-medium=""
data-utm-content=""
data-utm-term=""
data-page-variant=""
data-zip-code=""
data-dealer-id=""
data-source-vendor=""
data-deal-type=""
data-self-generated=""
data-lead-type=""
data-microsite-type=""
data-thank-you-page=""
data-sorry-page=""
data-video-form=""
></div>All configuration is passed via data-* attributes on the wrapper element. No JavaScript configuration is required.
Data Attributes Reference
| Attribute | Purpose | Default |
|-----------|---------|---------|
| data-dealer-id | Force a specific dealer ID (skips the API location lookup) | — |
| data-source-vendor | Name of the marketing vendor (e.g. "XYZ") | — |
| data-thank-you-page | Full URL to redirect to after a successful submission | https://roofmaxx.com/thank-you |
| data-sorry-page | Full URL to redirect to when no dealer is nearby (out-of-area). If empty, the thank-you page is used in all cases | — |
| data-video-form | Set to "false" to hide the Wistia intro video. Video is shown by default. | "true" |
| data-zip-code | Pre-fill the ZIP code field in Step 2 | — |
| data-deal-type | Deal type code forwarded to the dealer API and HubSpot (e.g. "RMCL-F", "MICRO") | — |
| data-lead-type | Lead type code forwarded to HubSpot | "C" |
| data-program-type | Program type code forwarded to HubSpot | — |
| data-self-generated | Self-generated lead flag (e.g. "SG" for dealer sites) | — |
| data-microsite-type | Microsite type forwarded to HubSpot | "Non-Microsite" |
| data-page-variant | Page variant code forwarded to HubSpot | — |
| data-utm-source | UTM source (see UTM priority logic below) | — |
| data-utm-campaign | UTM campaign | — |
| data-utm-medium | UTM medium | — |
| data-utm-content | UTM content | — |
| data-utm-term | UTM term | — |
UTM Priority Logic
The form applies the following priority order when collecting UTM parameters:
- Both URL and data-attributes present → data-attributes win
- Only URL → URL UTMs are used
- Only data-attributes → data-attributes are used
- Neither → empty values are sent
Examples of Use
Internal Use — Roof Maxx Corporate
For roofmaxx.com (Website)
In all cases (dealer found or not) the user is redirected to roofmaxx.com/thank-you.
<div id="roofmaxx-form"
data-utm-source=""
data-utm-campaign=""
data-utm-medium=""
data-utm-content=""
data-utm-term=""
data-page-variant=""
data-zip-code=""
data-dealer-id=""
data-source-vendor=""
data-deal-type=""
data-self-generated=""
data-lead-type=""
data-microsite-type=""
data-thank-you-page=""
data-sorry-page=""
data-video-form=""
></div>For roofmaxx.com/find-a-dealer/state OR roofmaxx.com/find-a-dealer/state/microsite
In all cases the user is redirected to roofmaxx.com/find-a-dealer/thank-you.
For the find-a-dealer / state-location page:
<div
id="roofmaxx-form"
data-utm-source=""
data-utm-campaign=""
data-utm-medium=""
data-utm-content=""
data-utm-term=""
data-page-variant=""
data-zip-code=""
data-dealer-id=""
data-source-vendor=""
data-deal-type="RMCL-F"
data-self-generated=""
data-lead-type=""
data-microsite-type=""
data-thank-you-page=""
data-sorry-page=""
data-video-form=""
></div>
<script>
/* Add dealer/company ID to HS form */
if (typeof roofmaxx_company_id !== "undefined" && roofmaxx_company_id) {
$("#roofmaxx-form").attr("data-dealer-id", roofmaxx_company_id);
}
</script>For Microsites:
<div
id="roofmaxx-form"
data-utm-source=""
data-utm-campaign=""
data-utm-medium=""
data-utm-content=""
data-utm-term=""
data-page-variant=""
data-zip-code=""
data-dealer-id=""
data-source-vendor=""
data-deal-type="MICRO"
data-self-generated=""
data-lead-type=""
data-microsite-type=""
data-thank-you-page=""
data-sorry-page=""
data-video-form=""
></div>
<script>
/* Add dealer/company ID to HS form */
if (typeof roofmaxx_company_id !== "undefined" && roofmaxx_company_id) {
$("#roofmaxx-form").attr("data-dealer-id", roofmaxx_company_id);
}
</script>Internal Use — HubSpot Landing Pages
1. Corporate landing page — same redirect regardless of dealer:
<div id="roofmaxx-form"
data-utm-source=""
data-utm-campaign=""
data-utm-medium=""
data-utm-content=""
data-utm-term=""
data-page-variant=""
data-zip-code=""
data-dealer-id=""
data-source-vendor=""
data-deal-type=""
data-self-generated=""
data-lead-type=""
data-microsite-type=""
data-thank-you-page="https://exampleHubspot.com/thank-you"
data-sorry-page=""
data-video-form=""
></div>2. Corporate landing page — different pages for dealer found vs. not found:
<div id="roofmaxx-form"
data-utm-source=""
data-utm-campaign=""
data-utm-medium=""
data-utm-content=""
data-utm-term=""
data-page-variant=""
data-zip-code=""
data-dealer-id=""
data-source-vendor=""
data-deal-type=""
data-self-generated=""
data-lead-type=""
data-microsite-type=""
data-thank-you-page="https://exampleHubspotYES.com/thank-you"
data-sorry-page="https://exampleHubspotNO.com/thank-you"
data-video-form=""
></div>3. Vendor (XYZ) landing page — same redirect regardless of dealer:
<div id="roofmaxx-form"
data-utm-source=""
data-utm-campaign=""
data-utm-medium=""
data-utm-content=""
data-utm-term=""
data-page-variant=""
data-zip-code=""
data-dealer-id=""
data-source-vendor="XYZ"
data-deal-type=""
data-self-generated=""
data-lead-type=""
data-microsite-type=""
data-thank-you-page="https://exampleHubspot.com/thank-you"
data-sorry-page=""
data-video-form=""
></div>4. Vendor (XYZ) landing page — different pages for dealer found vs. not found:
<div id="roofmaxx-form"
data-utm-source=""
data-utm-campaign=""
data-utm-medium=""
data-utm-content=""
data-utm-term=""
data-page-variant=""
data-zip-code=""
data-dealer-id=""
data-source-vendor="XYZ"
data-deal-type=""
data-self-generated=""
data-lead-type=""
data-microsite-type=""
data-thank-you-page="https://exampleHubspotYES.com/thank-you"
data-sorry-page="https://exampleHubspotNO.com/thank-you"
data-video-form=""
></div>Dealer Use
Placing the form on a dealer's own website or landing page with a custom thank-you page.
It is recommended to always supply a custom
data-thank-you-page. Without one the user is redirected to roofmaxx.com/thank-you and conversion tracking on the dealer's own site will not work correctly.
- In case you use UTM parameters and you don't want to use the UTMs in the URL, insert them as data-attributes. These hardcoded UTM parameters are only sent when the form is submitted.
- Dealer ID example:
2452308196
<div id="roofmaxx-form"
data-utm-source=""
data-utm-campaign=""
data-utm-medium=""
data-utm-content=""
data-utm-term=""
data-page-variant=""
data-zip-code=""
data-dealer-id="2452308196"
data-source-vendor=""
data-deal-type=""
data-self-generated="SG"
data-lead-type=""
data-microsite-type=""
data-thank-you-page="https://dealers.com/thank-you"
data-sorry-page=""
data-video-form=""
></div>How the Form Works — Step by Step
The form is a React application that mounts inside #roofmaxx-form and guides the user through three steps before submitting a lead to HubSpot.
Step 1 — Roof Information
Collects details about the property and roof condition.
Question 1 — Property Type (always shown, required)
| Option | Internal Value |
|--------|---------------|
| Single-Family Home | SingleFamilyHome |
| Condominium / Apartment | MultiFamily |
| Commercial | Commercial |
Question 2 — Manage Multiple Properties? (shown only when MultiFamily or Commercial is selected)
| Option | Internal Value |
|--------|---------------|
| Yes | Yes |
| No | No |
Question 3 — Property Unit Count (shown only when Question 2 is Yes)
| Option | Internal Value |
|--------|---------------|
| 1–10 units | 1To10 |
| 11–50 units | 11To50 |
| 51+ units | 51Plus |
| Not Sure | Unknown |
Question 4 — Asphalt Shingle Roof? (always shown, required)
| Option | Internal Value |
|--------|---------------|
| Yes | Yes |
| No | No |
| Not Sure | NotSure |
Question 5 — Roof Age (always shown, required)
| Option | Internal Value |
|--------|---------------|
| 0–4 years | ZeroToFour |
| 5–15 years | FiveToFifteen |
| 15+ years | FifteenPlus |
| Not Sure | Unknown |
On submit the answers are saved to localStorage and the form advances to Step 2.
Step 2 — Location
Collects the property address to power the dealer lookup.
| Field | Type | Notes | |-------|------|-------| | Street Address | Text (autocomplete) | Google Maps Places API — selecting a suggestion auto-fills city, state, and ZIP | | ZIP Code | Text | Exactly 5 numeric digits, required. Non-numeric characters are blocked as you type. Validated against Zippopotam.us on blur. | | City | Text | Auto-filled from address selection, or entered manually | | State | Dropdown | All 50 US states + DC |
ZIP code validation:
- Non-numeric characters are stripped in real time as the user types.
- On blur (when the user leaves the ZIP field),
validateZipExists()calls the Zippopotam.us API to confirm the ZIP is a real US postal code. An inline error is shown immediately if not. - On submit, a format guard (
/^\d{5}$/) infindDealer()acts as a final fallback. - If Zippopotam.us is unreachable, the check is skipped silently so valid submissions are never blocked.
Error messages:
- Wrong format (non-numeric or not 5 digits):
"Please enter a valid 5-digit ZIP code" - Valid format but ZIP does not exist:
"ZIP code not found. Please enter a valid US ZIP code"
On submit:
- If
data-dealer-idwas provided, the location lookup is skipped. - Otherwise,
findDealer(location, dealTypeCode)is called against the Roof Maxx Connect API. - The returned dealer and
LocationStatusare saved tolocalStoragefor use on the thank-you page. - The dealer's ID from the lookup is not submitted to HubSpot — only a
dealer_idset explicitly viadata-dealer-idor?dealer_id=is sent. This prevents pre-assigning a dealer before downstream routing completes. - The form advances to Step 3.
Step 3 — Contact Information
Collects the lead's personal details.
| Field | Type | Validation |
|-------|------|------------|
| First Name | Text | Required |
| Last Name | Text | Required |
| Email | Email | Required, regex validated |
| Phone | Masked text | Required, format (###) ###-####, area code must start with 2–9 |
| SMS Opt-In | Checkbox | Optional |
On submit:
- Combines all data from
localStorage(roof info, location, dealer) with the contact fields. - Builds the HubSpot payload (see HubSpot Submission).
- Posts the lead to the HubSpot API.
- Redirects the user (see Redirect Logic).
- Clears all form-related
localStoragekeys (resetForm).
Conditional Field Logic
All conditional display logic is driven by react-hook-form's watch() in roof-information-step.tsx:
propertyType === MultiFamily OR Commercial
→ show "Manage Multiple Properties?"
manageProperty === Yes
→ show "Property Unit Count?"Hidden fields are also excluded from validation, so the user is never blocked by a field they cannot see.
State & Data Persistence
Form state is stored in localStorage so that progress survives a page refresh.
| Key | Contents |
|-----|---------|
| roofInformation | All Step 1 answers (JSON) |
| location | Address, ZIP, city, state from Step 2 (JSON) |
| dealer | Dealer object returned by the API (JSON) |
| status | LocationStatus string from the API |
| step | Current step index: 0, 1, or 2 |
On successful final submission all keys are cleared via resetForm().
Dealer Finder Logic
packages/utils/src/dealers.ts exports two functions:
validateZipExists(zipCode) — called on blur in Step 2. Hits the Zippopotam.us API to confirm the ZIP is a real US postal code. Returns true if valid or if the service is unreachable, false if the ZIP does not exist.
findDealer(location, dealType) — called on Step 2 submit. Contains a format guard that throws INVALID_ZIP for non-5-digit values, then calls the Roof Maxx Connect API:
GET https://roofmaxxconnect.com/api/v1/map-status
postal_code, city, state, address, deal_type, api_tokenThe API responds with a LocationStatus and a Dealer object.
LocationStatus Values
| Status | Meaning |
|--------|---------|
| TERRITORY | Active dealer in the user's area |
| CA1 | Close enough — dealer covers this area |
| CA2 | On the boundary — dealer may cover this area |
| HOLD | Area on hold, no active dealer |
| ERROR | API error or no response |
| CORPCOMMERCIAL | Corporate commercial account territory |
isDealerNearby() returns true only for TERRITORY and CA1. This result drives the redirect decision.
Dealer Slug Attribution
form.tsx also checks the current URL for the pattern /find-a-dealer/{state}/{city}. When found, the slug is forwarded to HubSpot as an attribution signal.
HubSpot Submission
packages/utils/src/hubspot.ts submits to:
POST https://api.hsforms.com/submissions/v3/integration/submit/6044336/{formId}- Portal ID:
6044336 - Form ID:
3e57be1b-f9df-4263-9cf4-8c11d67427fd
Fields Sent to HubSpot
| HubSpot Field | Source |
|---------------|--------|
| email, firstname, lastname | Step 3 |
| phone, sms_opt_in_checkbox | Step 3 |
| address, zip, city, state | Step 2 |
| property_type, roof_age, shingle_roof | Step 1 |
| manage_property, property_unit_count | Step 1 (conditional) |
| dealer_id | Only when explicitly set via data-dealer-id attribute or ?dealer_id= URL param — not from the automatic dealer lookup |
| detected_meeting_link | "true" / "false" — whether the looked-up dealer has a meeting link |
| had_explicit_dealer_id | true if dealer_id was explicitly provided, false if it came from the lookup |
| utm_source, utm_campaign, utm_medium, utm_content, utm_term | Data attributes / URL |
| steps_completed, page_variant | Internal form metadata |
| lead_type_code, program_type_code | Data attributes |
| microsite_type, self_generated | Data attributes |
| deal_type_code, source_vendor | Data attributes |
The submission also includes:
- The HubSpot tracking cookie (
hubspotutk) for contact deduplication - The client's IP address (fetched asynchronously from
api64.ipify.org) - The current page URI and title as context
Redirect Logic
After a successful HubSpot submission:
- A URL is built by appending all collected form fields as query parameters to
data-thank-you-page. - If
data-sorry-pageis set ANDisDealerNearby()returnsfalse→ redirect to the sorry page. - Otherwise → redirect to the thank-you page.
Thank-you page URL parameters
Key parameters passed to the thank-you page for routing decisions:
| Parameter | Value | Notes |
|-----------|-------|-------|
| dealer_found | "true" / "false" | Whether a dealer is available — either explicitly assigned or found via lookup |
| dealer_status | LocationStatus string | Raw status from the dealer API |
| dealer_brand_name | string | Dealer's brand name (if a dealer was found) |
| dealer_id | string | Only present when explicitly set via data-dealer-id or ?dealer_id= |
All http:// URLs are automatically upgraded to https:// before redirecting.
Google Maps Integration
Step 2 wraps the location fields with @react-google-maps/api's LoadScript component.
The StreetInput component:
- Uses the Google Places
AutocompleteServiceto fetch address predictions as the user types (debounced 300 ms). - Renders a dropdown of predictions with keyboard navigation (arrow keys, Enter, Escape).
- On selection, calls
PlacesService.getDetails()to extract structured address components (street number, route, city, state, ZIP). - Auto-fills the ZIP, city, and state fields from the selected result.
Component Architecture
The project is a Turborepo monorepo with three shared packages consumed by the form app.
Monorepo Structure
roofmaxx/
├── apps/form/ # The embeddable React form application
└── packages/
├── components/ # Reusable UI components
├── types/ # Shared TypeScript types and enums
└── utils/ # Service functions (HubSpot, dealer API, URL helpers)Key UI Components (packages/components)
| Component | Description |
|-----------|-------------|
| Input | Text input with error state styling |
| StreetInput | Google Places autocomplete input |
| PhoneInput | Masked phone input — format (###) ###-####, first digit 2–9 |
| Select | Styled select dropdown |
| Spinner | Animated 4-dot loading indicator |
| IconRadioGroup / IconRadioItem | Icon-based radio button groups used for Step 1 questions |
| StepCard | Animated step container with 500 ms slide transition |
| StepSelector | Numbered progress indicator — allows jumping back to completed steps |
| FormGroup, FormLabel, FormError | Form layout primitives |
TypeScript Types (packages/types)
| Type | Description |
|------|-------------|
| Step enum | RoofInformation (0), Location (1), ContactInformation (2) |
| RoofInformation | Step 1 answer shape |
| Location | Step 2 address shape |
| Contact | Step 3 contact shape |
| Dealer | Dealer object returned by the API |
| LocationStatus | Enum of possible dealer proximity statuses |
| HubSpotFields | Generic field mapping for HubSpot submission |
Styling & Theming
Tailwind CSS is used with a tw- class prefix to avoid collisions with the host page's styles. The important: true option is set so form styles always win over host-page styles.
Brand Color Palette
| Token | Hex | Usage |
|-------|-----|-------|
| roofmaxx-primary | #003057 | Dark blue — primary buttons and headers |
| roofmaxx-secondary | #0088BB | Light blue — focus states and accents |
| roofmaxx-accent | #CC6C27 | Orange — warning and info states |
| roofmaxx-accent2 | #D68A52 | Light orange |
| roofmaxx-accent3 | #66B8D6 | Cyan |
Animations
| Animation | Duration | Used by |
|-----------|----------|---------|
| Step slide in/out | 500 ms | StepCard (Headless UI Transition) |
| Icon hover scale | 300 ms | IconRadioItem |
| Spinner (popout/translate/popin) | Custom keyframes | Spinner component |
Build & Development
The project uses Turborepo for task orchestration and Parcel 2 as the bundler.
Install dependencies (run from the repository root — do not run npm install in subdirectories):
npm installStart dev server (http://localhost:3000):
npm run devProduction build (outputs to apps/form/dist/):
npm run buildThe compiled output is a global-format IIFE bundle (dist/form.js + dist/form.css). No module system is required on the host page.
Third-Party Services Reference
| Service | Purpose | Key / Endpoint |
|---------|---------|----------------|
| HubSpot Forms API | Lead submission | Portal 6044336 · Form 3e57be1b-f9df-4263-9cf4-8c11d67427fd |
| Google Maps Places API | Address autocomplete | Key: AIzaSyA6XzSKqUYfuG97HDpQmrDZXBHVHM8vbYs |
| Roof Maxx Connect API | Dealer lookup | https://roofmaxxconnect.com/api/v1/map-status |
| Zippopotam.us | ZIP code existence validation | https://api.zippopotam.us/us/{zipcode} — free, no key required |
| ipify | Client IP address | https://api64.ipify.org?format=json |
| Wistia | Intro video | Video ID: zfdfuxftgy |
To rotate any key or endpoint, update the relevant source file:
| Key | File |
|-----|------|
| HubSpot Portal & Form ID | packages/utils/src/hubspot.ts |
| Google Maps API Key | apps/form/src/steps/location-step.tsx |
| Dealer API endpoint & token | packages/utils/src/dealers.ts |
| Wistia Video ID | apps/form/src/components/video-form.tsx |
