A fully dynamic, settings-driven multi-step form built with Google Apps Script and React 18. Every field, step, and option is configured directly from a Google Sheet — no code changes, no redeployment.
Developed by Rameez Scripts · YouTube · WhatsApp
| Feature | Details |
|---|---|
| Settings-Driven | Add/remove fields and steps by editing the Settings sheet — zero code changes |
| Multi-Step Navigation | Step indicator with completed/active/pending states |
| Step-by-Step Validation | Required fields, email format, file presence checked per step |
| Review Before Submit | Dedicated review screen showing all answers before final submission |
| 11 Field Types | text, email, tel, number, date, textarea, select, radio, checkbox, image, file, dependent_select |
| Dependent Dropdowns | Cascading selects — child options filter based on the selected parent value |
| Image & File Upload | Files uploaded to Google Drive; image URLs use the fast lh3.google.com CDN |
| Duplicate Email Detection | Real-time check on blur — blocks Next if email already exists in Responses |
| Email Notifications | Confirmation email to submitter + admin alert email with full HTML summary |
| Custom Success Screen | Configure success title and message from the Config sheet |
| Responses in Sheets | Each submission appended as a row; headers auto-created from active fields |
| Mobile Responsive | Optimised for all screen sizes — 360px to desktop |
| Skeleton Loader | Smooth skeleton UI while form config loads |
├── Code.gs ← Apps Script backend (all server-side logic)
└── index.html ← Single-file React 18 frontend (HTML + CSS + JS)
Only two files. No build tools, no npm, no dependencies to install.
Open Google Sheets, create a new spreadsheet, and name it anything you like (the name is used as the Drive upload folder name automatically).
Go to Extensions → Apps Script in your spreadsheet.
- Paste
Code.gscontents into the defaultCode.gsfile. - Click + → HTML → name it
index→ pasteindex.htmlcontents.
In the Apps Script editor, select the setupDemoData function from the dropdown and click Run. This will:
- Create the Config sheet with default settings
- Create the Settings sheet with a 4-step demo form (including a dependent dropdown example)
- Create the Responses sheet with 5 sample responses
- Click Deploy → New Deployment
- Select type: Web App
- Execute as: Me
- Who has access: Anyone (or Anyone within your domain)
- Click Deploy → copy the web app URL
Your form is live. Open the URL to see it in action.
Note: Any time you change
Code.gs, you must create a New Deployment (not just save) to get the updated URL.
Controls app-level settings. Add rows in key / value format.
| Key | Description | Example |
|---|---|---|
admin_email |
Email address to receive admin alerts on every submission | admin@example.com |
success_title |
Heading shown on the success screen after submit | Thank You! |
success_message |
Body text shown on the success screen | We will be in touch shortly. |
Leave admin_email blank to disable admin notifications.
Each row defines one field. The form is fully rebuilt from this sheet on every load.
| Column | Name | Description |
|---|---|---|
| A | field_id |
Unique numeric ID for the field |
| B | step |
Step number (1, 2, 3…) |
| C | step_label |
Label shown in the step indicator |
| D | field_name |
Internal name used as the key in Responses (no spaces, use underscores) |
| E | field_label |
Label displayed to the user |
| F | field_type |
Field type — see Field Types |
| G | options |
Comma-separated options (select/radio/checkbox) or pipe-separated map (dependent_select) |
| H | required |
Yes or No |
| I | placeholder |
Placeholder text |
| J | order |
Sort order within the step (1, 2, 3…) |
| K | active |
Yes to show, No to hide |
| L | depends_on |
For dependent_select — the field_name of the parent field |
To add a new field: insert a new row with active = Yes. No redeployment needed — reload the web app.
To disable a field: set active = No. The field disappears from the form and is excluded from new responses.
Auto-generated on first submission. Each row is one form submission.
| Column | Description |
|---|---|
response_id |
Auto-incrementing integer ID |
submitted_at |
ISO 8601 timestamp (2026-04-11T09:22:14.000Z) |
| (field columns) | One column per active field, in the order they appear in Settings |
Headers are created automatically on first submit. If you add new fields later, new columns are appended to the right.
| Type | Description |
|---|---|
text |
Single-line text input |
email |
Email input with format validation + real-time duplicate check |
tel |
Phone number input |
number |
Numeric input |
date |
Date picker |
textarea |
Multi-line text area |
select |
Searchable dropdown (single select) |
radio |
Radio button group — single choice |
checkbox |
Checkbox group — multiple choices (stored as comma-separated) |
image |
Image upload → Google Drive → lh3.google.com CDN URL stored |
file |
Any file upload → Google Drive view link stored |
dependent_select |
Cascading dropdown — options change based on a parent field's value |
A dependent_select field shows different options depending on what was selected in a parent field.
Settings row example:
| field_name | field_type | options | depends_on |
|---|---|---|---|
industry_role |
dependent_select |
Technology:Developer,DevOps Engineer|Healthcare:Doctor,Nurse|Finance:Analyst,Accountant |
industry |
Options format:
ParentValue1:option1,option2,option3|ParentValue2:option4,option5|ParentValue3:option6
- Segments separated by
| - Each segment:
ParentValue:comma,separated,options - The child dropdown resets automatically when the parent value changes
- When no parent is selected, the child shows a "Select [parent field] first" placeholder
Two emails are sent automatically after every successful submission:
- Sent to the value entered in the
emailfield (if the form has one) - Subject:
Submission Confirmed — Ref #[ID] - Body: Navy Blue header + HTML table of all field answers
- Sent to the
admin_emailconfigured in the Config sheet - Subject:
New Form Submission — Ref #[ID] - Body: Same HTML table format with a reference ID callout
Both emails are sent via MailApp (uses the Google account that deployed the script). File/image field values are replaced with [Uploaded File] in the email body — the actual URL is stored in Responses.
Google Apps Script has a daily email quota (100 emails/day on free accounts, 1500/day on Workspace). Plan accordingly for high-volume forms.
When a user fills in an email field and moves focus away (on blur), the app silently calls the backend to check if that email already exists in the Responses sheet.
- Checking: spinner shown under the field
- Duplicate found: red warning — "This email is already registered" — the Next button is blocked
- Available: green checkmark shown
- On change: status resets immediately so the user can try a different email
The check is case-insensitive and trims whitespace. If the Responses sheet is empty or has no email column yet, it returns available.
Files are uploaded to Google Drive using the base64 → Blob pattern:
- User selects a file —
FileReaderconverts it to base64 in the browser - On submit, base64 is sent to
uploadFile()inCode.gs - The file is created in a Drive folder named after your spreadsheet (auto-created if it doesn't exist)
- The file is shared as Anyone with the link → View
- The Drive URL is stored in the Responses sheet:
- Images →
https://lh3.google.com/u/0/d/[fileId](fast CDN, renders in browser) - Other files →
https://drive.google.com/file/d/[fileId]/view
- Images →
File size limit: 10 MB per file (enforced client-side before upload).
In the Settings sheet, add rows with a new step number (e.g., 5) and a new step_label. The step indicator updates automatically.
Update the step numbers. Fields are sorted by step then order.
Set active = No. The field is excluded from the form and from new response columns. Existing data in Responses is not affected.
Edit the field_label column — labels update on next form load. Do not change field_name after data has been collected, as it maps to the Responses column header.
Update the order column values.
| Layer | Technology |
|---|---|
| Backend | Google Apps Script (V8 runtime) |
| Database | Google Sheets |
| File Storage | Google Drive |
| MailApp (Apps Script built-in) | |
| Frontend | React 18 (CDN + Babel) |
| Icons | Font Awesome 6.5.1 |
| Alerts | SweetAlert2 |
| Hosting | Google Apps Script Web App |
No server, no database setup, no hosting costs — everything runs on Google infrastructure.
When you deploy and first run the app, Google will ask you to authorise:
| Permission | Used For |
|---|---|
| Google Sheets | Read Settings/Config, write Responses |
| Google Drive | Create upload folder, upload files |
| Gmail (MailApp) | Send confirmation and admin emails |
Form shows a loading skeleton forever
- Check that
setupDemoDataran successfully and the Settings sheet exists. - Open Apps Script → Run
getFormConfigmanually and check the Execution Log for errors.
Files not uploading
- Make sure Drive permission was granted during authorisation.
- Check the 10 MB limit — larger files are blocked client-side.
Emails not sending
- Verify
admin_emailin the Config sheet has a valid email address. - Check the Gmail quota (100/day free, 1500/day Workspace).
- Open Apps Script → Execution Log — email errors are logged but don't fail the submission.
Changes to Code.gs not reflected
- You must create a New Deployment, not just save the file.
Duplicate check always shows "available"
- The Responses sheet must have a header row with a column named exactly matching the
field_nameof the email field (default:email).
Rameez Scripts — Custom Google Apps Script & PHP/MySQL Solutions
- YouTube: youtube.com/@rameezimdad
- WhatsApp: wa.me/923224083545 (Custom project inquiries)
This project is licensed under the MIT License — free to use, modify, and distribute.