Create a multi-step contact form that collects user information across separate screens with configurable default values through Builder.
Multi-step forms improve user experience by breaking form fields into focused sections.
This tutorial creates a contact form with three steps: basic information, message details, and review. Users with sufficient permissions can customize the component using the Visual Editor.
Before you begin, you need:
- An integrated Page or Section already connected to Builder.
- Basic knowledge of React and TypeScript.
- Familiarity with custom components.
- A React app running locally.
This tutorial results in a registered with the Builder registry that collects user information through a multi-step form and provides the flexibility of configuring default values through the Builder's Visual Editor:
Create a React component that manages form steps and integrates with Builder props.
Create the component file src/components/ContactForm.tsx
to handle user input, manages navigation between steps, and displays a review screen before submission:
// src/components/ContactForm.tsx
import React, { useState } from 'react';
import './styles/ContactForm.css';
interface ContactFormProps {
defaultName?: string;
defaultEmail?: string;
defaultMessage?: string;
}
export function ContactForm({
defaultName = '',
defaultEmail = '',
defaultMessage = ''
}: ContactFormProps) {
const [currentStep, setCurrentStep] = useState(0);
const [formData, setFormData] = useState({
name: defaultName,
email: defaultEmail,
message: defaultMessage
});
const [isSubmitted, setIsSubmitted] = useState(false);
const handleInputChange = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const handleNext = () => setCurrentStep(currentStep + 1);
const handlePrevious = () => setCurrentStep(currentStep - 1);
const handleSubmit = () => setIsSubmitted(true);
if (isSubmitted) {
return (
<div className="contact-form-success">
<h3>Thank you!</h3>
<p>Your message has been sent.</p>
</div>
);
}
return (
<div className="contact-form">
<h2>Contact Us</h2>
{/* Step 1: Basic Information */}
{currentStep === 0 && (
<div className="form-step">
<div className="form-field">
<label htmlFor="name">Name</label>
<input
id="name"
type="text"
value={formData.name}
onChange={(e) => handleInputChange('name', e.target.value)}
placeholder="Enter your name"
/>
</div>
<div className="form-field">
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleInputChange('email', e.target.value)}
placeholder="Enter your email"
/>
</div>
<button onClick={handleNext} className="btn-primary">
Next
</button>
</div>
)}
{/* Step 2: Message */}
{currentStep === 1 && (
<div className="form-step">
<div className="form-field">
<label htmlFor="message">Message</label>
<textarea
id="message"
value={formData.message}
onChange={(e) => handleInputChange('message', e.target.value)}
placeholder="Enter your message"
rows={4}
/>
</div>
<div className="form-buttons">
<button onClick={handlePrevious} className="btn-secondary">
Previous
</button>
<button onClick={handleNext} className="btn-primary">
Review
</button>
</div>
</div>
)}
{/* Step 3: Review */}
{currentStep === 2 && (
<div className="form-step">
<h3>Review your information</h3>
<div className="review-section">
<div className="review-item">
<strong>Name:</strong>
<span>{formData.name || 'Not provided'}</span>
</div>
<div className="review-item">
<strong>Email:</strong>
<span>{formData.email || 'Not provided'}</span>
</div>
<div className="review-item">
<strong>Message:</strong>
<span>{formData.message || 'Not provided'}</span>
</div>
</div>
<div className="form-buttons">
<button onClick={handlePrevious} className="btn-secondary">
Previous
</button>
<button onClick={handleSubmit} className="btn-primary">
Send Message
</button>
</div>
</div>
)}
<div className="step-indicator">
Step {currentStep + 1} of 3
</div>
</div>
);
}
Create the CSS file src/styles/ContactForm.css
to provide a clean, professional form layout with proper spacing, hover effects, and responsive design:
/* /styles/ContactForm.css */
.contact-form {
max-width: 500px;
margin: 0 auto;
padding: 24px;
border: 1px solid #e1e5e9;
border-radius: 8px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
}
.contact-form h2 {
margin-bottom: 24px;
text-align: center;
color: #1f2937;
}
.form-step {
min-height: 200px;
}
.form-field {
margin-bottom: 16px;
}
.form-field label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #374151;
}
.form-field input,
.form-field textarea {
width: 100%;
padding: 12px;
border: 1px solid #d1d5db;
border-radius: 4px;
font-size: 16px;
box-sizing: border-box;
}
.form-field input:focus,
.form-field textarea:focus {
outline: none;
border-color: #2563eb;
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2);
}
.form-buttons {
display: flex;
gap: 12px;
}
.btn-primary,
.btn-secondary {
padding: 12px 24px;
border: none;
border-radius: 4px;
font-weight: 500;
cursor: pointer;
font-size: 16px;
}
.btn-primary {
background-color: #2563eb;
color: white;
}
.btn-primary:hover {
background-color: #1d4ed8;
}
.btn-secondary {
background-color: #f3f4f6;
color: #374151;
}
.btn-secondary:hover {
background-color: #e5e7eb;
}
.step-indicator {
text-align: center;
margin-top: 16px;
font-size: 14px;
color: #6b7280;
}
.contact-form-success {
text-align: center;
padding: 32px;
max-width: 500px;
margin: 0 auto;
border: 1px solid #e1e5e9;
border-radius: 8px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
}
.contact-form-success h3 {
color: #059669;
margin-bottom: 8px;
}
.contact-form-success p {
color: #374151;
}
.review-section {
background-color: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 6px;
padding: 16px;
margin-bottom: 20px;
}
.review-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #e5e7eb;
}
.review-item:last-child {
margin-bottom: 0;
border-bottom: none;
}
.review-item strong {
color: #374151;
font-weight: 600;
min-width: 80px;
margin-right: 16px;
}
.review-item span {
color: #6b7280;
text-align: right;
word-break: break-word;
flex: 1;
}
This tutorial uses standard CSS to keep things clear and straightforward. You can use Tailwind CSS, CSS modules, or any styling method you prefer.
Create the registration file src/components/ContactFormInfo.ts
with the following configuration to make your component available in the Visual Editor.
// src/components/ContactFormInfo.ts
import { RegisteredComponent } from '@builder.io/sdk-react';
import { ContactForm } from './ContactForm';
export const contactFormInfo: RegisteredComponent = {
component: ContactForm,
name: 'ContactForm',
inputs: [
{
name: 'defaultName',
type: 'string',
friendlyName: 'Default Name',
defaultValue: 'John Doe'
},
{
name: 'defaultEmail',
type: 'string',
friendlyName: 'Default Email',
defaultValue: 'john@example.com'
},
{
name: 'defaultMessage',
type: 'longText',
friendlyName: 'Default Message',
defaultValue: 'Hello, I would like to get in touch...'
}
]
};
The contactFormInfo
object defines how Builder handles your component. The component
property references your React component, while name
sets the display name in Builder's component library. The inputs
array defines configuration options that appear in the Options tab.
Each input configuration creates a field where users can customize the component. The name
property becomes a prop passed to your React component, while friendlyName
provides a human-readable label in the Options tab. The type
property defines the input type, and defaultValue
sets the initial value when the component is added to Builder.
Register your component with a Builder page to make it available in the Visual Editor.
Create the page file src/pages/FormPage.tsx
:
// src/pages/FormPage.tsx
import React from 'react';
import { Content } from '@builder.io/sdk-react';
import { contactFormInfo } from '../components/ContactFormInfo';
// Replace this with your actual API key from Builder
const BUILDER_API_KEY = 'YOUR_API_KEY_HERE';
export default function FormPage() {
return (
<Content
model="page"
apiKey={BUILDER_API_KEY}
customComponents={[contactFormInfo]}
/>
);
}
The Builder
component connects your React app to Builder's Visual Editor. When Builder loads content, it replaces this component with the visual content created in Builder's editor, including any custom components you've registered through the customComponents
array.
Update your main
component to include the contact page by modifying src/App.tsx
:
// src/App.tsx
import React from 'react';
import FormPage from './pages/FormPage';
import './App.css';
function App() {
return (
<div className="App">
<FormPage />
</div>
);
}
export default App;
Before connecting to Builder, verify your component works in your local development environment.
- Make sure your development server is running with
npm run dev
. - Open your browser to
http://localhost:5173
. - Test the form by filling out fields, navigating between steps, and submitting.
Your form should work correctly in your local environment before connecting to Builder.
To see the multi-step form in action:
- Replace the placeholder API key in
src/pages/FormPage.tsx
with your Public API key. - Open Content entry to access the Visual Editor.
- Drag your ContactForm component from the Custom Components section to your page.
- Use the Options tab to configure default values when selecting the component.
- Click Publish to make your changes live with the URL defined.
Once your basic form is working, you can enhance the component configuration options to give content creators more control over appearance and behavior. Builder supports many input types beyond basic strings to create rich configuration interfaces.
Use enum types to provide predefined options that ensure consistency across your site:
{
name: 'formTheme',
type: 'string',
enum: [
{ label: 'Light', value: 'light' },
{ label: 'Dark', value: 'dark' }
],
friendlyName: 'Form Theme',
defaultValue: 'light'
}
This creates a dropdown menu in the Options tab where users can select between light and dark themes. The selected value gets passed to your component as the formTheme
prop, allowing you to conditionally apply CSS classes or styling based on the selection.
Boolean inputs provide on/off switches for optional features:
{
name: 'showProgressBar',
type: 'boolean',
defaultValue: true,
friendlyName: 'Show Progress Indicator'
}
This creates a checkbox that content creators can toggle to show or hide the progress indicator. Your component can then conditionally render the step indicator based on this prop.
Color inputs provide visual color pickers for brand consistency:
{
name: 'buttonColor',
type: 'color',
defaultValue: '#2563eb',
friendlyName: 'Button Color'
}
This creates a color picker that lets users customize the button color to match their brand. The selected color value gets passed to your component, where you can apply it as an inline style or CSS variable.
Enhance the configuration experience with additional options:
{
name: 'defaultName',
type: 'string',
friendlyName: 'Default Name',
defaultValue: 'John Doe',
helperText: 'The name that appears when the form loads',
required: true
}
The helperText
property provides descriptive text below the input to clarify its purpose, while required
marks the field as mandatory.
These examples demonstrate just a few of the available customization options. Builder provides many other input types for custom components, including file uploads, rich text editors, and reference fields.
You can also create dynamic configuration interfaces using ShowIf with registered components to show or hide inputs based on other field values.
You now have a working multi-step form that content creators can customize through Builder. Consider these next steps:
- Add form validation with error handling.
- Integrate with your API endpoints for form submission.
- Explore child blocks for embedding Builder content within your form.