How to Create an Event Registration Form
Conference, workshop, webinar, meetup - they all need registration. Ticket types, quantities, attendee details, dietary restrictions, session selection. This guide shows how to build a complete event registration form with automatic pricing and smart conditional logic.
Ticket Type Selection
Start with the basics - what kind of ticket does the attendee want? Most events have tiered pricing: general admission, VIP, student discounts.
form.addRow(row => {
row.addDropdown('ticketType', {
label: 'Ticket Type',
options: [
{ id: 'general', name: 'General Admission - $50' },
{ id: 'vip', name: 'VIP Access - $150' },
{ id: 'student', name: 'Student - $25' }
],
defaultValue: 'general',
isRequired: true
});
});Put the price right in the option name. No surprises. The attendee sees exactly what they're paying before they click anything else.
Adding Quantity
People often buy tickets for colleagues or friends. Add a quantity field next to the ticket type.
form.addRow(row => {
row.addDropdown('ticketType', {
label: 'Ticket Type',
options: [
{ id: 'general', name: 'General Admission - $50' },
{ id: 'vip', name: 'VIP Access - $150' }
],
defaultValue: 'general'
});
row.addInteger('quantity', {
label: 'Number of Tickets',
min: 1,
max: 10,
defaultValue: 1,
isRequired: true
});
}); Setting max: 10 prevents bulk purchases that might be scalpers. Adjust based on your event - some conferences limit to 5 per registration, others allow more.
Automatic Price Calculation
Show the total in real-time. When they change ticket type or quantity, the price updates instantly.
const ticketPrices: Record<string, number> = {
'general': 50,
'vip': 150,
'student': 25
};
form.addRow(row => {
row.addPriceDisplay('total', {
label: 'Total',
variant: 'highlight',
computedValue: () => {
const type = form.dropdown('ticketType')?.value() || 'general';
const qty = form.integer('quantity')?.value() || 1;
return ticketPrices[type] * qty;
}
});
}); The computedValue function runs whenever any input changes. No "calculate" button needed - the total is always current.
Pro tip
Put pricing information early in the form. People want to know the cost before filling out personal details. It reduces abandonment.
Collecting Attendee Information
Now the personal details. Name and email are essential. Company and job title help with networking features and badge printing.
const attendeeSection = form.addSubform('attendee', {
title: 'Attendee Information'
});
attendeeSection.addRow(row => {
row.addTextbox('name', {
label: 'Full Name',
placeholder: 'As it will appear on badge',
isRequired: true
});
row.addEmail('email', {
label: 'Email',
placeholder: 'For confirmation and updates',
isRequired: true
});
});
attendeeSection.addRow(row => {
row.addTextbox('company', {
label: 'Company/Organization',
placeholder: 'Optional'
});
row.addTextbox('jobTitle', {
label: 'Job Title',
placeholder: 'Optional'
});
});Use subforms to group related fields. The title "Attendee Information" makes it clear what this section is for.
Dietary Restrictions
If your event includes food, you need to know about dietary needs. A checkbox list works well - people can select multiple options.
attendeeSection.addRow(row => {
row.addCheckboxList('dietary', {
label: 'Dietary Restrictions',
options: [
{ id: 'vegetarian', name: 'Vegetarian' },
{ id: 'vegan', name: 'Vegan' },
{ id: 'gluten-free', name: 'Gluten-Free' },
{ id: 'kosher', name: 'Kosher' },
{ id: 'halal', name: 'Halal' },
{ id: 'none', name: 'None' }
],
orientation: 'horizontal'
});
});
attendeeSection.addRow(row => {
row.addTextbox('allergies', {
label: 'Food Allergies',
placeholder: 'Please list any food allergies',
isVisible: () => {
const dietary = form.checkboxList('dietary')?.value() || [];
return dietary.length > 0 && !dietary.includes('none');
}
});
});The allergies field only appears if they selected any dietary restriction (except "None"). No point showing it if they don't need it.
Conditional Sections for Ticket Types
VIP tickets often include extras that general admission doesn't. Show those options only to VIP buyers.
// VIP-only options
const vipSection = form.addSubform('vipOptions', {
title: 'VIP Benefits',
isVisible: () => form.dropdown('ticketType')?.value() === 'vip'
});
vipSection.addRow(row => {
row.addCheckbox('meetGreet', {
label: 'Include Meet & Greet Session',
defaultValue: true
});
row.addCheckbox('premiumSeating', {
label: 'Reserve Front Row Seating',
defaultValue: false
});
});The entire section appears or disappears based on ticket type. VIP buyers see their options. General admission buyers don't see fields that don't apply to them.
Session Selection
Multi-track conferences need session selection. Use max: 1 on checkbox lists when sessions run concurrently - they can only attend one at a time.
const sessionsSection = form.addSubform('sessions', {
title: 'Session Selection'
});
sessionsSection.addRow(row => {
row.addCheckboxList('morningSession', {
label: 'Morning Sessions (10:00 AM)',
options: [
{ id: 'keynote', name: 'Opening Keynote' },
{ id: 'workshop-a', name: 'Workshop A: Getting Started' },
{ id: 'workshop-b', name: 'Workshop B: Advanced Topics' }
],
max: 1 // Can only attend one at a time
});
});
sessionsSection.addRow(row => {
row.addCheckboxList('afternoonSession', {
label: 'Afternoon Sessions (2:00 PM)',
options: [
{ id: 'panel', name: 'Expert Panel Discussion' },
{ id: 'networking', name: 'Networking Session' },
{ id: 'demo', name: 'Product Demo' }
],
max: 1
});
});This prevents double-booking. If Workshop A and Workshop B run at the same time, selecting one automatically prevents selecting the other.
Need more form examples? Check out our template gallery.
Handling Multiple Attendees
When someone buys multiple tickets, you have options:
- Collect all attendee info upfront (longer form)
- Collect primary attendee, email the rest (shorter form)
- Collect names only, email for full details later
For most events, the second option works best. Keep the form short.
// Track total attendees based on quantity
form.addRow(row => {
row.addTextPanel('attendeeNote', {
computedValue: () => {
const qty = form.integer('quantity')?.value() || 1;
if (qty === 1) return 'Please enter your information below.';
return 'Please enter information for the primary attendee. ' +
'Additional attendee details will be collected via email.';
}
});
});Group Discounts
Encourage group registrations with automatic discounts. Show the discount so people know they're getting a deal.
const baseTicketPrices: Record<string, number> = {
'general': 50,
'vip': 150
};
form.addRow(row => {
row.addPriceDisplay('subtotal', {
label: 'Subtotal',
computedValue: () => {
const type = form.dropdown('ticketType')?.value() || 'general';
const qty = form.integer('quantity')?.value() || 1;
return baseTicketPrices[type] * qty;
}
});
});
form.addRow(row => {
row.addTextPanel('discountNote', {
computedValue: () => {
const qty = form.integer('quantity')?.value() || 1;
if (qty >= 5) return '๐ 10% group discount applied!';
if (qty >= 3) return '๐ 5% group discount applied!';
return '';
},
isVisible: () => (form.integer('quantity')?.value() || 1) >= 3
});
});
form.addRow(row => {
row.addPriceDisplay('total', {
label: 'Total',
variant: 'highlight',
computedValue: () => {
const type = form.dropdown('ticketType')?.value() || 'general';
const qty = form.integer('quantity')?.value() || 1;
let total = baseTicketPrices[type] * qty;
// Group discounts
if (qty >= 5) total *= 0.90; // 10% off
else if (qty >= 3) total *= 0.95; // 5% off
return total;
}
});
});The discount message appears at 3+ tickets, and the discount increases at 5+. People see the incentive to add more attendees.
Complete Example
Here's a working event registration form with ticket selection, attendee info, dietary restrictions, and pricing.
export function eventRegistration(form: FormTs) {
form.setTitle(() => 'Conference Registration');
const prices: Record<string, number> = {
'general': 99,
'vip': 249,
'student': 49
};
// Ticket selection
form.addRow(row => {
row.addDropdown('ticketType', {
label: 'Ticket Type',
options: [
{ id: 'general', name: 'General Admission - $99' },
{ id: 'vip', name: 'VIP Pass - $249' },
{ id: 'student', name: 'Student - $49 (ID required)' }
],
defaultValue: 'general',
isRequired: true
});
row.addInteger('quantity', {
label: 'Quantity',
min: 1,
max: 10,
defaultValue: 1
});
});
// Attendee info
const attendee = form.addSubform('attendee', {
title: 'Primary Attendee'
});
attendee.addRow(row => {
row.addTextbox('name', {
label: 'Full Name',
isRequired: true
});
row.addEmail('email', {
label: 'Email',
isRequired: true
});
});
attendee.addRow(row => {
row.addCheckboxList('dietary', {
label: 'Dietary Needs',
options: [
{ id: 'vegetarian', name: 'Vegetarian' },
{ id: 'vegan', name: 'Vegan' },
{ id: 'gluten-free', name: 'Gluten-Free' },
{ id: 'none', name: 'None' }
],
orientation: 'horizontal'
});
});
// Total
form.addRow(row => {
row.addPriceDisplay('total', {
label: 'Total',
variant: 'highlight',
computedValue: () => {
const type = form.dropdown('ticketType')?.value() || 'general';
const qty = form.integer('quantity')?.value() || 1;
return prices[type] * qty;
}
});
});
form.configureSubmitButton({ label: 'Complete Registration' });
}Copy this into the FormTs editor and customize for your event - change ticket names, prices, and add any additional fields you need.
Best Practices
Keep It Short
Every field you add reduces completion rate. Only ask for what you actually need. Job title? Only if you're using it for something. Dietary restrictions? Only if food is included.
Show Progress
For longer forms, use multi-page layouts with clear progress indicators. "Step 2 of 3" is less intimidating than a wall of fields.
Mobile First
Many people register on phones. Test your form on mobile. Single-column layouts work better on small screens.
Confirmation Matters
After submission, show a clear confirmation with:
- What they registered for
- Total amount
- What happens next (email confirmation, payment, etc.)
Common Questions
How do I handle payment?
FormTs collects the registration data. For payment, you'll typically redirect to a payment processor (Stripe, PayPal) after submission, or integrate using webhooks. The form calculates and displays the total - payment happens separately.
Can I limit total registrations?
Yes. Track registrations in your backend and use the form's visibility conditions to show a 'sold out' message when capacity is reached. You can also disable specific ticket types when they sell out.
How do I send confirmation emails?
Use FormTs webhooks to trigger emails through your email service (SendGrid, Mailchimp, etc.) or use the built-in email notifications for simpler needs. The webhook sends all form data to your endpoint.
Should I require an account to register?
Usually no. Account requirements add friction and reduce registrations. Collect email for communication - you can offer optional account creation after registration if needed.
How do I handle waitlists?
When an event sells out, change the form to collect waitlist signups instead. Same fields, different submit button text ('Join Waitlist'). Use a webhook or check submissions to manage the waitlist.