Moving Company Quote Form
Someone's moving in three weeks. They're comparing quotes from five companies. Four of them say "call for estimate." Yours shows an instant ballpark price with date flexibility discounts. You get the booking while competitors are still scheduling in-home estimates.
Moving quotes are complex - the price depends on distance, inventory, packing services, and timing. A good quote form captures enough detail for an accurate estimate without making customers list every fork and spoon.
This guide covers building quote forms for residential moves: local and long-distance, with inventory collection, packing options, and flexible scheduling that rewards customers who give you scheduling freedom.
Move Type Selection
Start with the basics. Local moves and long-distance moves have completely different pricing models, so this question shapes the entire form.
form.addRow(row => {
row.addDropdown('moveType', {
label: 'Type of Move',
options: [
{ id: 'local', name: 'Local Move (same city)' },
{ id: 'long-distance', name: 'Long Distance (100+ miles)' },
{ id: 'interstate', name: 'Interstate Move' },
{ id: 'commercial', name: 'Commercial/Office Move' }
],
isRequired: true
});
});Local moves are typically hourly - the cost depends on crew size and time. Long-distance moves are weight and distance based. Commercial moves have different requirements entirely. Getting this right early simplifies everything that follows.
Origin and Destination Addresses
For long-distance moves, distance directly affects price. FormTs address fields can calculate distance automatically and show it to the customer.
// Origin address
form.addRow(row => {
row.addAddress('originAddress', {
label: 'Moving From',
placeholder: 'Enter pickup address',
isRequired: true,
restrictToCountries: ['us']
});
});
// Destination with distance calculation
form.addRow(row => {
row.addAddress('destinationAddress', {
label: 'Moving To',
placeholder: 'Enter destination address',
isRequired: true,
restrictToCountries: ['us'],
referenceAddress: () => form.address('originAddress')?.value() ?? undefined,
showDistance: true,
distanceUnit: 'miles'
});
}); The destination field references the origin address for distance calculation. showDistance: true displays the mileage so customers can verify it matches their expectations.
Pro tip
Restrict addresses to countries you actually serve. A customer in Canada filling out your US-only form wastes everyone's time. Use restrictToCountries to filter suggestions.
Distance-Based Pricing
For long-distance moves, show the distance charge separately. This transparency builds trust - customers understand why a 500-mile move costs more than 100 miles.
const baseRatePerMile = 3.50;
const minimumCharge = 500;
form.addRow(row => {
row.addPriceDisplay('distanceCharge', {
label: 'Distance Charge',
computedValue: () => {
const distance = form.address('destinationAddress')?.distance();
if (!distance) return null;
const charge = distance * baseRatePerMile;
return Math.max(charge, minimumCharge);
},
prefix: 'Estimated',
isVisible: () => {
const moveType = form.dropdown('moveType')?.value();
return moveType === 'long-distance' || moveType === 'interstate';
}
});
});A minimum charge ensures short long-distance moves are still profitable. The calculation only appears for move types where distance matters.
Property Details
Where someone is moving from and to affects the job difficulty. A third-floor walkup is harder than a ground-floor ranch house.
form.addRow(row => {
row.addDropdown('originType', {
label: 'Moving From',
options: [
{ id: 'apartment', name: 'Apartment' },
{ id: 'house', name: 'House' },
{ id: 'condo', name: 'Condo/Townhouse' },
{ id: 'storage', name: 'Storage Unit' },
{ id: 'office', name: 'Office/Commercial' }
],
isRequired: true
});
row.addDropdown('destinationType', {
label: 'Moving To',
options: [
{ id: 'apartment', name: 'Apartment' },
{ id: 'house', name: 'House' },
{ id: 'condo', name: 'Condo/Townhouse' },
{ id: 'storage', name: 'Storage Unit' },
{ id: 'office', name: 'Office/Commercial' }
],
isRequired: true
});
});Home Size and Access
Bedroom count is the industry standard proxy for move size. Combine it with floor level and elevator access to estimate labor requirements.
form.addRow(row => {
row.addDropdown('homeSize', {
label: 'Home Size',
options: [
{ id: 'studio', name: 'Studio' },
{ id: '1br', name: '1 Bedroom' },
{ id: '2br', name: '2 Bedrooms' },
{ id: '3br', name: '3 Bedrooms' },
{ id: '4br', name: '4 Bedrooms' },
{ id: '5plus', name: '5+ Bedrooms' }
],
isRequired: true
});
});
// Stairs and elevator affect pricing
form.addRow(row => {
row.addDropdown('originFloor', {
label: 'Floor (Origin)',
options: [
{ id: 'ground', name: 'Ground Floor' },
{ id: '2', name: '2nd Floor' },
{ id: '3', name: '3rd Floor' },
{ id: '4plus', name: '4th Floor or Higher' }
]
});
row.addDropdown('hasElevator', {
label: 'Elevator Available?',
options: [
{ id: 'yes', name: 'Yes' },
{ id: 'no', name: 'No' },
{ id: 'na', name: 'Ground Floor' }
]
});
});Fourth floor without an elevator? That's a significant surcharge. Customers understand why - they've walked up those stairs. Showing the surcharge upfront prevents surprises later.
Inventory Collection
A detailed inventory gives the best estimate, but no one wants to catalog every item they own. Strike a balance: list the big stuff, estimate the rest.
const inventorySection = form.addSubform('inventory', {
title: 'What Are You Moving?'
});
inventorySection.addRow(row => {
row.addCheckboxList('largeItems', {
label: 'Large Items',
options: [
{ id: 'sofa', name: 'Sofa/Couch' },
{ id: 'bed-king', name: 'King Bed' },
{ id: 'bed-queen', name: 'Queen Bed' },
{ id: 'dresser', name: 'Dresser' },
{ id: 'dining-table', name: 'Dining Table' },
{ id: 'desk', name: 'Desk' },
{ id: 'bookshelf', name: 'Bookshelf' },
{ id: 'piano', name: 'Piano' },
{ id: 'pool-table', name: 'Pool Table' }
]
});
});
inventorySection.addRow(row => {
row.addCheckboxList('appliances', {
label: 'Appliances',
options: [
{ id: 'fridge', name: 'Refrigerator' },
{ id: 'washer', name: 'Washer' },
{ id: 'dryer', name: 'Dryer' },
{ id: 'dishwasher', name: 'Dishwasher' },
{ id: 'tv-large', name: 'Large TV (55"+)' }
]
});
});Focus on items that significantly affect pricing: large furniture, heavy appliances, and things that need special handling. A checkbox list is faster than asking for counts of each item.
Special Items
Some items need special attention and additional charges. Pianos, antiques, and hot tubs require different equipment and expertise.
form.addRow(row => {
row.addCheckboxList('specialItems', {
label: 'Special Items (require extra care)',
options: [
{ id: 'antiques', name: 'Antiques' },
{ id: 'artwork', name: 'Artwork/Paintings' },
{ id: 'piano', name: 'Piano/Organ' },
{ id: 'safe', name: 'Safe/Gun Cabinet' },
{ id: 'hot-tub', name: 'Hot Tub' },
{ id: 'gym', name: 'Gym Equipment' }
]
});
});
form.addRow(row => {
row.addTextarea('specialInstructions', {
label: 'Special Instructions',
placeholder: 'Anything we should know? Fragile items, access issues, etc.',
rows: 3
});
});The open text field catches anything unusual - narrow staircases, gated communities with time restrictions, or that 800-pound safe in the basement.
See how other service businesses build quote forms.
Packing Services
Packing is where moving companies make or break their margins. Some customers want full service. Others just need help with fragile items. Many pack themselves.
const packingSection = form.addSubform('packing', {
title: 'Packing Services'
});
packingSection.addRow(row => {
row.addRadioButton('packingLevel', {
label: 'Packing Options',
options: [
{ id: 'full', name: 'Full Packing Service (we pack everything)' },
{ id: 'partial', name: 'Partial Packing (fragile items only)' },
{ id: 'self', name: 'Self-Pack (I\'ll pack myself)' }
],
defaultValue: 'self'
});
});
// Show supplies option if self-packing
packingSection.addRow(row => {
row.addCheckbox('needSupplies', {
label: 'I need packing supplies (boxes, tape, bubble wrap)',
isVisible: () => form.radioButton('packingLevel')?.value() === 'self'
});
});The supplies option appears only for self-packers. Someone paying for full packing service doesn't need to buy boxes separately.
Packing Pricing
Show packing costs clearly. Customers can see exactly what they're paying for and make informed decisions.
const packingPrices: Record<string, Record<string, number>> = {
'studio': { full: 200, partial: 75 },
'1br': { full: 350, partial: 125 },
'2br': { full: 500, partial: 200 },
'3br': { full: 750, partial: 300 },
'4br': { full: 1000, partial: 400 },
'5plus': { full: 1500, partial: 600 }
};
form.addRow(row => {
row.addPriceDisplay('packingCost', {
label: 'Packing Service',
computedValue: () => {
const packing = form.radioButton('packingLevel')?.value();
const size = form.dropdown('homeSize')?.value();
if (!packing || packing === 'self' || !size) return null;
return packingPrices[size]?.[packing] ?? null;
},
isVisible: () => {
const packing = form.radioButton('packingLevel')?.value();
return packing === 'full' || packing === 'partial';
}
});
});Move Date and Flexibility
This is where smart pricing comes in. Movers often have gaps in their schedule. Customers who can be flexible fill those gaps and should get a discount for it.
const dateSection = form.addSubform('scheduling', {
title: 'When Are You Moving?'
});
dateSection.addRow(row => {
row.addRadioButton('dateFlexibility', {
label: 'Date Flexibility',
options: [
{ id: 'exact', name: 'I have an exact date' },
{ id: 'flexible', name: 'I\'m flexible (±3 days)' },
{ id: 'window', name: 'Within a date range' }
],
defaultValue: 'exact'
});
});
dateSection.addRow(row => {
row.addDatepicker('moveDate', {
label: 'Move Date',
isRequired: true,
minDate: () => new Date().toISOString().split('T')[0],
isVisible: () => {
const flex = form.radioButton('dateFlexibility')?.value();
return flex === 'exact' || flex === 'flexible';
}
});
});
// Date range for window option
dateSection.addRow(row => {
row.addDatepicker('moveDateStart', {
label: 'Earliest Date',
minDate: () => new Date().toISOString().split('T')[0],
isVisible: () => form.radioButton('dateFlexibility')?.value() === 'window'
});
row.addDatepicker('moveDateEnd', {
label: 'Latest Date',
minDate: () => form.datepicker('moveDateStart')?.value() ?? undefined,
isVisible: () => form.radioButton('dateFlexibility')?.value() === 'window'
});
});Three options cover most situations: exact date for people who must move on a specific day, flexible for those who have some wiggle room, and a date window for maximum flexibility.
Flexibility Discounts
Show customers the benefit of flexibility immediately. A 10-15% discount is real money on a $2,000 move.
form.addRow(row => {
row.addTextPanel('flexDiscount', {
computedValue: () => {
const flex = form.radioButton('dateFlexibility')?.value();
if (flex === 'flexible') {
return '💡 Flexible dates can save 10-15% - we\'ll find the best available slot.';
}
if (flex === 'window') {
return '💡 Date ranges give us scheduling flexibility - expect 10-20% savings.';
}
return null;
},
isVisible: () => {
const flex = form.radioButton('dateFlexibility')?.value();
return flex === 'flexible' || flex === 'window';
}
});
});This encourages customers to consider flexibility they might not have thought about. Many people have "soft" move dates that could shift a few days for the right savings.
Pricing Logic
Now put it all together. Start with base pricing by home size, then layer in the variables.
const basePrices: Record<string, number> = {
'studio': 400,
'1br': 600,
'2br': 900,
'3br': 1200,
'4br': 1600,
'5plus': 2200
};
const floorSurcharge: Record<string, number> = {
'ground': 0,
'2': 75,
'3': 150,
'4plus': 250
};
form.addRow(row => {
row.addPriceDisplay('baseEstimate', {
label: 'Base Moving Cost',
variant: 'large',
prefix: 'Starting at',
computedValue: () => {
const size = form.dropdown('homeSize')?.value();
if (!size) return null;
let total = basePrices[size] ?? 0;
// Floor surcharge
const floor = form.dropdown('originFloor')?.value();
const hasElevator = form.dropdown('hasElevator')?.value();
if (floor && hasElevator === 'no') {
total += floorSurcharge[floor] ?? 0;
}
return total;
}
});
});Total Estimate
The grand total combines everything: base price, floor surcharges, packing, distance, and flexibility discounts.
form.addRow(row => {
row.addPriceDisplay('totalEstimate', {
label: 'Total Estimate',
variant: 'highlight',
prefix: 'Estimated Total',
computedValue: () => {
const size = form.dropdown('homeSize')?.value();
if (!size) return null;
let total = basePrices[size] ?? 0;
// Floor surcharge
const floor = form.dropdown('originFloor')?.value();
const hasElevator = form.dropdown('hasElevator')?.value();
if (floor && hasElevator === 'no') {
total += floorSurcharge[floor] ?? 0;
}
// Packing
const packing = form.radioButton('packingLevel')?.value();
if (packing && packing !== 'self') {
total += packingPrices[size]?.[packing] ?? 0;
}
// Distance (for long moves)
const distance = form.address('destinationAddress')?.distance();
const moveType = form.dropdown('moveType')?.value();
if (distance && (moveType === 'long-distance' || moveType === 'interstate')) {
total += Math.max(distance * 3.50, 500);
}
// Flexibility discount
const flex = form.radioButton('dateFlexibility')?.value();
if (flex === 'flexible') {
total *= 0.90; // 10% discount
} else if (flex === 'window') {
total *= 0.85; // 15% discount
}
return Math.round(total);
}
});
});Use "Estimated" as a prefix - the final price may vary based on actual inventory and conditions. But giving a realistic ballpark builds trust and filters out customers who can't afford your services.
Complete Example
Here's a streamlined moving quote form covering the essentials:
export function movingQuote(form: FormTs) {
form.setTitle(() => 'Moving Quote Calculator');
// Move type
form.addRow(row => {
row.addDropdown('moveType', {
label: 'Type of Move',
options: [
{ id: 'local', name: 'Local Move' },
{ id: 'long-distance', name: 'Long Distance' }
],
isRequired: true
});
});
// Addresses
form.addRow(row => {
row.addAddress('origin', {
label: 'Moving From',
isRequired: true
});
});
form.addRow(row => {
row.addAddress('destination', {
label: 'Moving To',
isRequired: true,
referenceAddress: () => form.address('origin')?.value() ?? undefined,
showDistance: true,
distanceUnit: 'miles'
});
});
// Home size
form.addRow(row => {
row.addDropdown('homeSize', {
label: 'Home Size',
options: [
{ id: '1br', name: '1 Bedroom' },
{ id: '2br', name: '2 Bedrooms' },
{ id: '3br', name: '3 Bedrooms' },
{ id: '4br', name: '4+ Bedrooms' }
],
isRequired: true
});
});
// Packing
form.addRow(row => {
row.addRadioButton('packing', {
label: 'Packing Service',
options: [
{ id: 'full', name: 'Full Packing' },
{ id: 'partial', name: 'Fragile Items Only' },
{ id: 'none', name: 'Self-Pack' }
],
defaultValue: 'none'
});
});
// Move date
form.addRow(row => {
row.addDatepicker('moveDate', {
label: 'Preferred Move Date',
isRequired: true
});
});
// Contact
const contact = form.addSubform('contact', { title: 'Contact Information' });
contact.addRow(row => {
row.addTextbox('name', { label: 'Name', isRequired: true });
row.addTextbox('phone', { label: 'Phone', isRequired: true });
});
contact.addRow(row => {
row.addEmail('email', { label: 'Email', isRequired: true });
});
form.configureSubmitButton({ label: 'Get My Quote' });
}This captures the core information for any residential move. Customize the pricing and add inventory details based on how detailed you want your initial quotes to be.
Follow-Up Strategy
An online quote is step one. For moves over a certain value, consider:
- Video walkthrough - Customer shows their home via phone for a more accurate quote
- In-home estimate - For large or complex moves, see it in person
- Binding quote - Lock in the price based on detailed inventory
- Instant booking - For smaller moves, let them book directly
The online form qualifies leads and sets expectations. Your follow-up process converts them into booked jobs.
Common Questions
How accurate should online moving quotes be?
Within 20% of the final price for most moves. Be clear it's an estimate - use 'starting at' language. Customers understand that actual weight/items may vary. If your quotes are consistently way off, adjust your formula or add more qualifying questions.
Should I show prices or just collect leads?
Show prices. Customers comparing movers will skip forms that don't give estimates. Showing prices also filters out customers who can't afford your services, saving everyone time. The movers who hide pricing lose to those who don't.
How do I handle items that need special handling?
Flag them early with a checkbox list (pianos, antiques, gun safes, hot tubs). Show a note that special items require individual pricing. You can't quote a piano move without knowing the piano type, stairs, and access - but you can indicate it's an additional charge.
What about commercial/office moves?
Separate form or a major branch in your form. Commercial moves have different timing (often nights/weekends), different inventory (desks, servers, files), and different pricing (often by cubic foot or workstation count). Don't mix residential and commercial logic.
Should I require the full inventory upfront?
No. Detailed inventory comes later after initial contact. For the quote form, bedroom count plus major items (appliances, special pieces) gives you enough for a ballpark. Requiring a full inventory upfront kills conversion rates.