Carport & Metal Building Calculator

Give your carport and metal building website a configurator that prices the way the trade quotes. Customers pick the building (steel carport, RV cover, enclosed steel garage, wood-framed garage or pole barn), set the width, length and leg height in feet, choose a regular, boxed-eave or vertical roof, the roof, side and trim colors, 14- or 12-gauge frame and certification for wind and snow, then roll-up, sectional or sliding doors by size and count, a walk-in door, windows, insulation, wainscot, closed sides and gable ends. The price is per square foot with the surcharges dealers use, delivery and installation included within your radius and per mile beyond, a concrete slab or gravel pad priced when you do it, permit handling, a deposit or a pay-in-full discount and a minimum order. Every rate is a constant at the top of the code, so you match it to your price list in minutes.

Home Services

Try the Calculator

Carport & Metal Building Quote
Pick the building, set the size in feet, choose the roof, colors and doors. Delivery and installation are included within 75 miles.
🏠 What Are We Building?
 
Delivery and installation are included: our crew delivers and installs within 75 miles at no charge. You prepare a level site (a slab, a gravel pad or level ground within 3 inches), or we do it and the site step prices it.
📐 Size, Roof & Colors
18ft
18 ft
1060
21ft
21 ft
1080
6ft
6 ft
616
18 x 21 ft, 378 sq ft - steel comes in 5-ft bays: built as 21 ft, 4 bays
 
🛡️ Structure
 
 
 
✨ Extras
🏗️ Site, Delivery & Payment
 
30mi
30 mi
0300
💰 Your Quote
$ 2,835
+
$ 0
+
$ 0

$ 2,835
plus sales tax
From the order to the keys: $284 deposit at the order, $2,551 due when the crew is done. Where your county requires a permit, you pull it. You have the site level within 3 inches before the crew arrives. Delivery and installation about 4 weeks from the order; we call first to confirm the color, the site and the access for the truck.
📋 Your Contact Details
 
 
 
 
 
 
🧾 Summary
$ 2,835
delivered and installed, plus sales tax
Site preparation and permit as chosen on the site step. The order is confirmed by phone.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
export function carportMetalBuildingCalculator(form: FormTs) {
// Price per square foot by building, delivered and installed (doors and windows on top)
const buildingRates: Record<string, number> = {
'carport': 7.5, // steel carport and RV cover: per sq ft of roof, 14-gauge frame, 29-gauge panels
'rv': 7.5,
'garage': 18, // enclosed steel garage: frame, roof and walls with base trim
'wood': 58, // wood-framed garage: 2x4 walls, trusses, lap siding, shingles, on the customer's slab
'barn': 30 // pole barn: posts in the ground, girts and purlins, vertical panels
};
 
const buildingNames: Record<string, string> = {
'carport': 'Steel carport',
'rv': 'RV cover',
'garage': 'Enclosed steel garage',
'wood': 'Wood-framed garage',
'barn': 'Pole barn'
};
 
// The height the rates are quoted at; every foot above carries a surcharge
const standardHeights: Record<string, number> = { 'carport': 6, 'rv': 12, 'garage': 8, 'wood': 8, 'barn': 10 };
 
// Typical order each building starts from (width x length x height in feet)
const typicalSizes: Record<string, number[]> = {
'carport': [18, 21, 6],
'rv': [12, 36, 12],
'garage': [24, 26, 9],
'wood': [24, 24, 9],
'barn': [30, 40, 12]
};
 
// Percent surcharges on the structure
const surcharges = {
boxedRoof: 12, // A-frame roof, horizontal panels, boxed eave
verticalRoof: 25, // panels down the slope, ridge cap, hat channels
legFoot: 5, // each foot of leg or wall above the standard height
gauge12: 12, // 2.5 in 12-gauge tubing instead of 14-gauge
certified: 8 // engineer-stamped drawings, extra bracing and anchors
};
const certifiedWindMph = 140;
const certifiedSnowPsf = 30;
 
const structureExtras = {
closedSidePerFt: 9, // panels from the eave to the ground, per linear foot of side
gableEnd: 350, // one end closed above the legs; the form counts two
insulationPerSqFt: 1.4, // double-bubble foil under the roof, and on the walls of an enclosed building
wainscotPerFt: 6 // 36 in band in the roof color along the bottom of the walls, per foot of perimeter
};
 
const doorPrices = {
rollupSmall: 750, // up to 10 x 8 ft
rollupLarge: 1250, // 10 x 10 ft and larger
sectional: 1600, // single insulated sectional door
sectionalDouble: 2700, // 16 x 7, 16 x 8, 18 x 8 ft
sliding: 1100, // sliding barn door on a track
opener: 550, // belt-drive opener, per sectional door
walkDoor: 450, // 36 x 80 in steel walk-in door
window: 325 // 30 x 36 in single-hung with a screen
};
 
const doorSizes: Record<string, string[]> = {
'rollup': ['8x7', '9x7', '9x8', '10x8', '10x10', '12x12', '12x14'],
'sectional': ['8x7', '9x7', '9x8', '10x8', '16x7', '16x8', '18x8'],
'sliding': ['10x10', '12x12', '14x14', '16x14']
};
 
const doorTypeNames: Record<string, string> = {
'rollup': 'Roll-up door',
'sectional': 'Sectional overhead door',
'sliding': 'Sliding barn door'
};
 
const sitePrices = {
slabPerSqFt: 8.5, // 4 in reinforced slab with a turned-down footer
gravelPerSqFt: 3.5, // compacted crushed-stone pad about 4 in deep
anchors: 180, // auger anchors for a steel building on gravel or bare ground
permit: 350, // drawings, application and inspections; county fee at cost
mileRate: 3.5, // per mile beyond the free delivery radius
freeMiles: 75
};
 
const padNames: Record<string, string> = {
'existing': 'My slab is ready',
'slab': 'Pour a concrete slab',
'gravel': 'Spread a gravel pad',
'ground': 'Set it on the ground with anchors'
};
 
const colorNames: Record<string, string> = {
'white': 'White', 'pewter': 'Pewter gray', 'charcoal': 'Charcoal', 'black': 'Black', 'red': 'Barn red', 'burgundy': 'Burgundy',
'green': 'Evergreen', 'tan': 'Tan', 'clay': 'Clay', 'brown': 'Brown', 'blue': 'Gallery blue'
};
 
const depositPercent = 10;
const prepayPercent = 3;
const minimumOrder = 1495;
// Promotion applied last on the whole order; 0 hides the banner
const promoPercent = 0;
const promoText = 'Free walk-in door with any garage';
const leadWeeks = 4;
 
// Steel comes in 5-ft bays: a 21 ft building has five bows
const FOOT_BAY = 5;
 
const money = (amount: number): string => {
const whole = String(Math.round(Math.abs(amount)));
let out = '';
for (let i = 0; i < whole.length; i++) {
out += whole.charAt(i);
const remaining = whole.length - i - 1;
if (remaining > 0 && remaining % 3 === 0) out += ',';
}
return (amount < 0 ? '-$' : '$') + out;
};
const perSqFt = (rate: number) => '$' + rate.toFixed(2) + '/sq ft';
const sizeText = (size: string) => size.replace('x', ' x ') + ' ft';
const colorOptions = (): ControlOption[] => Object.keys(colorNames).map(id => ({ id, name: colorNames[id] || id }));
 
form.addRow(row => {
row.addTextPanel('header', {
computedValue: () => 'Carport & Metal Building Quote',
customStyles: { 'font-size': '1.5rem', 'font-weight': '600', 'color': '#1e293b' }
});
});
 
form.addRow(row => {
row.addTextPanel('intro', {
computedValue: () => 'Pick the building, set the size in feet, choose the roof, colors and doors. Delivery and installation are included within ' + sitePrices.freeMiles + ' miles.',
customStyles: { 'font-size': '0.95rem', 'color': '#475569' }
});
});
 
form.addSpacer({ height: 20 });
 
// Pages are created up front so the building tile can preset the sliders of the next step
const steps = form.addPages('steps', { heightMode: 'current-page' });
const buildingPage = steps.addPage('building');
const configPage = steps.addPage('configure');
const sitePage = steps.addPage('site');
const quotePage = steps.addPage('quote');
 
// ---- Step 1: the building ------------------------------------------------------
const buildingSection = buildingPage.addSubform('building', { title: '🏠 What Are We Building?', isCollapsible: false });
 
// The sections of the next step, filled in below; the tile presets their fields once they exist
let configSection: SubformTs | undefined;
let doorsSection: SubformTs | undefined;
 
// The tile sets the size and doors a typical order of that building has
const applyBuilding = (id: string) => {
const size = typicalSizes[id];
if (!size) return;
configSection?.slider('width')?.setValue(size[0]);
configSection?.slider('length')?.setValue(size[1]);
configSection?.slider('height')?.setValue(size[2]);
configSection?.radioButton('roof')?.setValue(id === 'carport' ? 'regular' : id === 'garage' ? 'boxed' : 'vertical');
doorsSection?.integer('doors')?.setValue(id === 'garage' ? 2 : id === 'wood' || id === 'barn' ? 1 : 0);
doorsSection?.radioButton('doorType')?.setValue(id === 'wood' ? 'sectional' : id === 'barn' ? 'sliding' : 'rollup');
doorsSection?.dropdown('doorSize')?.setValue(id === 'wood' ? '16x7' : id === 'barn' ? '12x12' : '9x7');
};
 
buildingSection.addRow(row => {
row.addRadioButton('kind', {
label: 'Building type (each starts from a typical size; you set every dimension on the next step)',
options: [
{ id: 'carport', name: 'Steel carport - legs and a roof, open or closed sides (' + perSqFt(buildingRates['carport'] || 0) + ')' },
{ id: 'rv', name: 'RV cover - tall legs, a vertical roof for the length (' + perSqFt(buildingRates['rv'] || 0) + ')' },
{ id: 'garage', name: 'Enclosed steel garage - walls, roll-up doors, lockable (' + perSqFt(buildingRates['garage'] || 0) + ')' },
{ id: 'wood', name: 'Wood-framed garage - siding and shingles to match the house (' + perSqFt(buildingRates['wood'] || 0) + ')' },
{ id: 'barn', name: 'Pole barn - post-frame, the biggest for the money (' + perSqFt(buildingRates['barn'] || 0) + ')' }
],
defaultValue: 'carport',
orientation: 'vertical',
isRequired: true,
onValueChange: value => {
if (typeof value === 'string') applyBuilding(value);
}
});
});
 
buildingSection.addRow(row => {
row.addTextPanel('included', {
computedValue: () => 'Delivery and installation are included: our crew delivers and installs within ' + sitePrices.freeMiles + ' miles at no charge. You prepare a level site (a slab, a gravel pad or level ground within 3 inches), or we do it and the site step prices it.',
customStyles: { 'font-size': '0.9rem', 'color': '#0369a1', 'background': '#e0f2fe', 'padding': '10px', 'border-radius': '6px' }
});
});
 
const kind = form.computedValue(() => {
const raw = buildingSection.radioButton('kind')?.value() || 'carport';
return buildingRates[raw] !== undefined ? raw : 'carport';
});
const isCarport = form.computedValue(() => kind() === 'carport' || kind() === 'rv');
const isSteel = form.computedValue(() => kind() !== 'wood' && kind() !== 'barn');
const hasWalls = form.computedValue(() => !isCarport());
const hasRoofChoice = form.computedValue(() => isSteel());
const standardHeight = form.computedValue(() => standardHeights[kind()] || 6);
 
// ---- Step 2: size, roof, colors, structure ------------------------------------
configSection = configPage.addSubform('size', { title: '📐 Size, Roof & Colors', isCollapsible: false });
 
configSection.addRow(row => {
row.addSlider('width', {
label: 'Width (one car needs 12 ft, two cars 18 to 24 ft)',
min: 10,
max: 60,
step: 1,
unit: 'ft',
defaultValue: 18
}, '1fr');
row.addSlider('length', {
label: 'Length (a car needs 20 ft, a truck 26 ft)',
min: 10,
max: 80,
step: 1,
unit: 'ft',
defaultValue: 21
}, '1fr');
row.addSlider('height', {
label: () => (isCarport() ? 'Leg height' : 'Wall height') + ' to the eave (standard ' + standardHeight() + ' ft, each foot above +' + surcharges.legFoot + '%)',
min: 6,
max: 16,
step: 1,
unit: 'ft',
defaultValue: 6
}, '1fr');
});
 
const widthFt = form.computedValue(() => configSection.slider('width')?.value() || 18);
const lengthFt = form.computedValue(() => configSection.slider('length')?.value() || 21);
const heightFt = form.computedValue(() => configSection.slider('height')?.value() || 6);
// Steel is built to the nearest 5-ft bay; the order is written for the built length
const lengthBuilt = form.computedValue(() => isSteel() ? Math.max(11, Math.round((lengthFt() - 1) / FOOT_BAY) * FOOT_BAY + 1) : lengthFt());
const bays = form.computedValue(() => Math.round((lengthBuilt() - 1) / FOOT_BAY));
const area = form.computedValue(() => widthFt() * lengthBuilt());
 
configSection.addRow(row => {
row.addTextPanel('sizeInfo', {
computedValue: () => widthFt() + ' x ' + lengthBuilt() + ' ft, ' + area() + ' sq ft'
+ (isSteel() ? ' - steel comes in 5-ft bays: built as ' + lengthBuilt() + ' ft, ' + bays() + ' bays' : ''),
customStyles: { 'font-size': '0.9rem', 'color': '#059669', 'font-weight': '500' }
});
});
 
configSection.addRow(row => {
row.addRadioButton('roof', {
label: 'Roof style',
options: [
{ id: 'regular', name: 'Regular - rounded eaves, horizontal panels (included)' },
{ id: 'boxed', name: 'Boxed eave - A-frame look, horizontal panels (+' + surcharges.boxedRoof + '%)' },
{ id: 'vertical', name: 'Vertical - panels down the slope, sheds snow (+' + surcharges.verticalRoof + '%)' }
],
defaultValue: 'regular',
orientation: 'vertical',
isVisible: () => hasRoofChoice()
}, '1fr');
row.addTextPanel('roofNote', {
computedValue: () => kind() === 'wood' ? 'A wood garage always has architectural shingles.' : 'A pole barn has a vertical panel roof.',
customStyles: { 'font-size': '0.9rem', 'color': '#64748b' },
isVisible: () => !hasRoofChoice()
}, '1fr');
});
 
const roof = form.computedValue(() => kind() === 'wood' ? 'shingle' : kind() === 'barn' ? 'vertical' : (configSection.radioButton('roof')?.value() || 'regular'));
const roofName = form.computedValue(() => roof() === 'shingle' ? 'Architectural shingles' : roof() === 'boxed' ? 'Boxed eave' : roof() === 'vertical' ? 'Vertical' : 'Regular');
 
configSection.addRow(row => {
row.addDropdown('roofColor', {
label: () => roof() === 'shingle' ? 'Shingle color' : 'Roof color',
options: colorOptions(),
defaultValue: 'charcoal'
}, '1fr');
row.addDropdown('wallColor', {
label: () => kind() === 'wood' ? 'Siding color' : isCarport() ? 'Side and trim base color' : 'Side color',
options: colorOptions(),
defaultValue: 'pewter'
}, '1fr');
row.addDropdown('trimColor', {
label: 'Trim',
options: [
{ id: 'white', name: 'White' },
{ id: 'black', name: 'Black' },
{ id: 'match', name: 'Match the sides' }
],
defaultValue: 'white'
}, '1fr');
});
 
const roofColor = form.computedValue(() => configSection.dropdown('roofColor')?.value() || 'charcoal');
const wallColor = form.computedValue(() => configSection.dropdown('wallColor')?.value() || 'pewter');
const trimColor = form.computedValue(() => configSection.dropdown('trimColor')?.value() || 'white');
 
const structureSection = configPage.addSubform('structure', {
title: '🛡️ Structure',
isCollapsible: false,
isVisible: () => isSteel()
});
 
structureSection.addRow(row => {
row.addRadioButton('gauge', {
label: 'Frame (12-gauge tubing is thicker and carries a longer frame warranty)',
options: [
{ id: '14', name: '14 gauge (standard)' },
{ id: '12', name: '12 gauge (+' + surcharges.gauge12 + '%)' }
],
defaultValue: '14',
orientation: 'vertical'
}, '1fr');
row.addRadioButton('certification', {
label: 'Certification (most counties ask for it with the permit)',
options: [
{ id: 'standard', name: 'Standard' },
{ id: 'certified', name: 'Certified: engineer-stamped drawings for ' + certifiedWindMph + ' mph wind and ' + certifiedSnowPsf + ' psf snow (+' + surcharges.certified + '%)' }
],
defaultValue: 'standard',
orientation: 'vertical'
}, '1fr');
});
 
structureSection.addRow(row => {
row.addRadioButton('sides', {
label: 'Sides (closed sides keep the rain and the sun off what stands under the roof)',
options: [
{ id: 'open', name: 'Open' },
{ id: 'closed', name: 'Closed with panels (+' + money(structureExtras.closedSidePerFt) + '/ft, both sides)' }
],
defaultValue: 'open',
orientation: 'vertical',
isVisible: () => isCarport()
}, '1fr');
row.addCheckbox('gableEnds', {
label: 'Gable ends - both ends closed above the legs (+' + money(2 * structureExtras.gableEnd) + ')',
defaultValue: false,
isVisible: () => isCarport() && roof() !== 'regular'
}, '1fr');
});
 
const gauge12 = form.computedValue(() => isSteel() && structureSection.radioButton('gauge')?.value() === '12');
const certified = form.computedValue(() => isSteel() && structureSection.radioButton('certification')?.value() === 'certified');
const closedSides = form.computedValue(() => isCarport() && structureSection.radioButton('sides')?.value() === 'closed');
const gableEnds = form.computedValue(() => isCarport() && roof() !== 'regular' && structureSection.checkbox('gableEnds')?.value() === true);
 
// ---- Doors, windows and extras ---------------------------------------------------
doorsSection = configPage.addSubform('doors', {
title: '🚪 Doors & Windows',
isCollapsible: false,
isVisible: () => hasWalls()
});
 
doorsSection.addRow(row => {
row.addRadioButton('doorType', {
label: 'Garage doors',
options: [
{ id: 'rollup', name: 'Roll-up door - rolls into a drum over the opening (from ' + money(doorPrices.rollupSmall) + ')' },
{ id: 'sectional', name: 'Sectional overhead - insulated panels on tracks, takes an opener (from ' + money(doorPrices.sectional) + ')' },
{ id: 'sliding', name: 'Sliding barn door - on a track over the wall, no headroom needed (' + money(doorPrices.sliding) + ')' }
],
defaultValue: 'rollup',
orientation: 'vertical'
}, '1fr');
row.addInteger('doors', {
label: 'How many garage doors (0 = storage building with a walk-in door only)',
min: 0,
max: 2,
defaultValue: 0
}, '1fr');
});
 
const doorCount = form.computedValue(() => hasWalls() ? (doorsSection.integer('doors')?.value() || 0) : 0);
const doorType = form.computedValue(() => {
const raw = doorsSection.radioButton('doorType')?.value() || 'rollup';
return doorSizes[raw] ? raw : 'rollup';
});
 
// Which price a door of this type at this size takes
const doorPrice = (type: string, size: string): number => {
const w = Number(size.split('x')[0] || 0);
const h = Number(size.split('x')[1] || 0);
if (type === 'sliding') return doorPrices.sliding;
if (type === 'sectional') return w >= 14 ? doorPrices.sectionalDouble : doorPrices.sectional;
return w >= 10 && h >= 10 ? doorPrices.rollupLarge : doorPrices.rollupSmall;
};
 
doorsSection.addRow(row => {
row.addDropdown('doorSize', {
label: 'Door size, width x height',
options: () => (doorSizes[doorType()] || []).map(size => ({ id: size, name: sizeText(size) + ' - ' + money(doorPrice(doorType(), size)) })),
defaultValue: '9x7',
isVisible: () => doorCount() > 0
}, '1fr');
row.addCheckbox('opener', {
label: () => 'Door opener - belt drive, two remotes and safety sensors (+' + money(doorPrices.opener) + ' per door)',
defaultValue: false,
isVisible: () => doorCount() > 0 && doorType() === 'sectional'
}, '1fr');
});
 
// The chosen size if the door type comes in it, else the first size the type is made in
const doorSize = form.computedValue(() => {
const sizes = doorSizes[doorType()] || ['9x7'];
const raw = doorsSection.dropdown('doorSize')?.value() || '';
return sizes.includes(raw) ? raw : (sizes[0] || '9x7');
});
const doorW = form.computedValue(() => Number(doorSize().split('x')[0] || 0));
const doorH = form.computedValue(() => Number(doorSize().split('x')[1] || 0));
const opener = form.computedValue(() => doorCount() > 0 && doorType() === 'sectional' && doorsSection.checkbox('opener')?.value() === true);
 
doorsSection.addRow(row => {
row.addTextPanel('doorHint', {
computedValue: () => {
if (doorCount() > 0 && doorCount() * doorW() + 2 * doorCount() > widthFt()) return 'Too wide for the front: pick a narrower door or a wider building.';
if (doorCount() > 0 && heightFt() < doorH() + 1) return 'A ' + doorH() + ' ft door needs walls at least ' + (doorH() + 1) + ' ft tall.';
return 'Doors are framed into the front wall and installed.';
},
customStyles: { 'font-size': '0.9rem', 'color': '#64748b' }
});
});
 
doorsSection.addRow(row => {
row.addCheckbox('walkDoor', {
label: 'Walk-in door - 36 x 80 in steel door with a lockset (+' + money(doorPrices.walkDoor) + ')',
defaultValue: false
}, '1fr');
row.addInteger('windows', {
label: 'Windows - 30 x 36 in single-hung with a screen (' + money(doorPrices.window) + ' each)',
min: 0,
max: 4,
defaultValue: 0
}, '1fr');
});
 
const walkDoor = form.computedValue(() => hasWalls() && doorsSection.checkbox('walkDoor')?.value() === true);
const windows = form.computedValue(() => hasWalls() ? (doorsSection.integer('windows')?.value() || 0) : 0);
 
const extrasSection = configPage.addSubform('extras', { title: '✨ Extras', isCollapsible: false });
 
extrasSection.addRow(row => {
row.addCheckbox('insulation', {
label: () => (isCarport() ? 'Insulation - foil bubble under the roof, stops the drip' : 'Insulation - foil bubble under the roof and on the walls') + ' (' + perSqFt(structureExtras.insulationPerSqFt) + ')',
defaultValue: false
}, '1fr');
row.addCheckbox('wainscot', {
label: 'Wainscot - a 36 in band in the roof color along the bottom of the walls (+' + money(structureExtras.wainscotPerFt) + '/ft)',
defaultValue: false,
isVisible: () => kind() === 'garage' || kind() === 'barn'
}, '1fr');
});
 
const insulation = form.computedValue(() => extrasSection.checkbox('insulation')?.value() === true);
const wainscot = form.computedValue(() => (kind() === 'garage' || kind() === 'barn') && extrasSection.checkbox('wainscot')?.value() === true);
 
// ---- Step 3: the site ------------------------------------------------------------
const siteSection = sitePage.addSubform('site', { title: '🏗️ Site, Delivery & Payment', isCollapsible: false });
 
siteSection.addRow(row => {
row.addRadioButton('pad', {
label: 'What does the building stand on?',
options: () => {
const options: ControlOption[] = [
{ id: 'existing', name: 'My slab is ready - level concrete, at least the size of the building' },
{ id: 'slab', name: 'Pour a concrete slab - 4 in reinforced with a turned-down footer (+' + perSqFt(sitePrices.slabPerSqFt) + ')' }
];
if (kind() !== 'wood') options.push({ id: 'gravel', name: 'Spread a gravel pad - compacted crushed stone, leveled (+' + perSqFt(sitePrices.gravelPerSqFt) + (isSteel() ? ' plus ' + money(sitePrices.anchors) + ' anchors' : '') + ')' });
if (isSteel()) options.push({ id: 'ground', name: 'Set it on the ground - level grass or dirt, auger anchors (+' + money(sitePrices.anchors) + ')' });
return options;
},
defaultValue: 'existing',
orientation: 'vertical',
isRequired: true
});
});
 
// The chosen pad if the building can stand on it, else the customer's slab
const pad = form.computedValue(() => {
const raw = siteSection.radioButton('pad')?.value() || 'existing';
if (raw === 'gravel' && kind() === 'wood') return 'existing';
if (raw === 'ground' && !isSteel()) return 'existing';
return padNames[raw] ? raw : 'existing';
});
 
siteSection.addRow(row => {
row.addSlider('distance', {
label: 'Distance from our yard (free within ' + sitePrices.freeMiles + ' miles, then $' + sitePrices.mileRate.toFixed(2) + '/mile)',
min: 0,
max: 300,
step: 5,
unit: 'mi',
defaultValue: 30
}, '1fr');
row.addDropdown('timing', {
label: 'When do you need it?',
options: [
{ id: 'asap', name: 'As soon as possible' },
{ id: 'quarter', name: 'Within 3 months' },
{ id: 'year', name: 'This year' },
{ id: 'comparing', name: 'Just comparing' }
],
defaultValue: 'quarter'
}, '1fr');
});
 
siteSection.addRow(row => {
row.addCheckbox('permit', {
label: 'Pull the permit for me - drawings, application and inspections; the county fee at cost (+' + money(sitePrices.permit) + ')',
defaultValue: false
}, '1fr');
row.addCheckbox('prepay', {
label: 'Pay in full at the order instead of a ' + depositPercent + '% deposit (' + prepayPercent + '% off)',
defaultValue: false
}, '1fr');
});
 
const distance = form.computedValue(() => siteSection.slider('distance')?.value() || 0);
const permit = form.computedValue(() => siteSection.checkbox('permit')?.value() === true);
const prepay = form.computedValue(() => siteSection.checkbox('prepay')?.value() === true);
const timingNames: Record<string, string> = { asap: 'As soon as possible', quarter: 'Within 3 months', year: 'This year', comparing: 'Just comparing' };
const timing = form.computedValue(() => siteSection.dropdown('timing')?.value() || 'quarter');
 
// ---- Price logic -----------------------------------------------------------------
const legSteps = form.computedValue(() => Math.max(0, Math.ceil(heightFt() - standardHeight())));
const roofPercent = form.computedValue(() => {
if (!hasRoofChoice()) return 0;
return roof() === 'boxed' ? surcharges.boxedRoof : roof() === 'vertical' ? surcharges.verticalRoof : 0;
});
 
// Structure: the area times the rate, then the roof, the legs, the gauge and the certification
const structure = form.computedValue(() => {
const base = area() * (buildingRates[kind()] || 0);
const forRoof = 1 + roofPercent() / 100;
const forLegs = 1 + legSteps() * surcharges.legFoot / 100;
const forGauge = gauge12() ? 1 + surcharges.gauge12 / 100 : 1;
const forCert = certified() ? 1 + surcharges.certified / 100 : 1;
return base * forRoof * forLegs * forGauge * forCert;
});
 
const sidesCost = form.computedValue(() => (closedSides() ? 2 * lengthBuilt() * structureExtras.closedSidePerFt : 0) + (gableEnds() ? 2 * structureExtras.gableEnd : 0));
 
const roofArea = form.computedValue(() => area() * 1.05);
const wallArea = form.computedValue(() => 2 * (widthFt() + lengthBuilt()) * heightFt());
const insulatedArea = form.computedValue(() => hasWalls() ? roofArea() + wallArea() : roofArea());
const insulationCost = form.computedValue(() => insulation() ? insulatedArea() * structureExtras.insulationPerSqFt : 0);
const perimeter = form.computedValue(() => 2 * (widthFt() + lengthBuilt()));
const wainscotCost = form.computedValue(() => wainscot() ? perimeter() * structureExtras.wainscotPerFt : 0);
 
const doorsCost = form.computedValue(() => {
if (!hasWalls()) return 0;
let sum = doorCount() * doorPrice(doorType(), doorSize());
if (opener()) sum += doorCount() * doorPrices.opener;
if (walkDoor()) sum += doorPrices.walkDoor;
sum += windows() * doorPrices.window;
return sum;
});
 
const padCost = form.computedValue(() => {
if (pad() === 'slab') return area() * sitePrices.slabPerSqFt;
if (pad() === 'gravel') return area() * sitePrices.gravelPerSqFt + (isSteel() ? sitePrices.anchors : 0);
if (pad() === 'ground') return isSteel() ? sitePrices.anchors : 0;
return 0;
});
 
const permitCost = form.computedValue(() => permit() ? sitePrices.permit : 0);
const delivery = form.computedValue(() => Math.max(0, distance() - sitePrices.freeMiles) * sitePrices.mileRate);
 
const subtotal = form.computedValue(() => structure() + sidesCost() + insulationCost() + wainscotCost() + doorsCost() + padCost() + permitCost() + delivery());
const prepayDiscount = form.computedValue(() => prepay() ? subtotal() * prepayPercent / 100 : 0);
const beforePromo = form.computedValue(() => subtotal() - prepayDiscount());
const promo = form.computedValue(() => beforePromo() * promoPercent / 100);
// The minimum holds last: a promotion never takes an order below what the steel costs
const total = form.computedValue(() => Math.max(Math.round(beforePromo() - promo()), minimumOrder));
const deposit = form.computedValue(() => prepay() ? total() : Math.round(total() * depositPercent / 100));
 
const describeSize = (): string => widthFt() + ' x ' + lengthBuilt() + ' ft, ' + heightFt() + ' ft ' + (isCarport() ? 'legs' : 'walls');
const describeColors = (): string => 'roof ' + (colorNames[roofColor()] || '').toLowerCase() + ', sides ' + (colorNames[wallColor()] || '').toLowerCase() + ', trim ' + (trimColor() === 'match' ? 'to match the sides' : trimColor());
const describeFrame = (): string => isSteel()
? (gauge12() ? '12-gauge' : '14-gauge') + ' frame, ' + (certified() ? 'certified ' + certifiedWindMph + ' mph / ' + certifiedSnowPsf + ' psf' : 'standard, not certified')
: kind() === 'wood' ? 'stick-built 2x4 walls, trusses' : 'pressure-treated posts, girts and purlins';
const describeDoors = (): string => {
if (!hasWalls()) return 'none';
if (doorCount() === 0) return 'no garage door';
return doorCount() + ' x ' + (doorTypeNames[doorType()] || '').toLowerCase() + ' ' + sizeText(doorSize()) + (opener() ? ', with opener' : '');
};
const describeExtras = (): string => {
const parts: string[] = [];
if (closedSides()) parts.push('closed sides');
if (gableEnds()) parts.push('gable ends');
if (walkDoor()) parts.push('walk-in door');
if (windows() > 0) parts.push(windows() + ' window' + (windows() > 1 ? 's' : ''));
if (insulation()) parts.push('insulation');
if (wainscot()) parts.push('wainscot');
return parts.join(', ') || 'none';
};
 
// ---- Step 4: the quote and contact details ----------------------------------------
quotePage.addRow(row => {
row.addTextPanel('promoBanner', {
computedValue: () => promoText + ': the ' + promoPercent + '% discount is already in the price below.',
customStyles: { 'font-size': '0.95rem', 'font-weight': '600', 'color': '#166534', 'background': '#dcfce7', 'padding': '10px', 'border-radius': '6px' },
isVisible: () => promoPercent > 0
});
});
 
const summarySection = quotePage.addSubform('summary', { title: '💰 Your Quote', isCollapsible: false });
 
summarySection.addRow(row => {
row.addPriceDisplay('structureCost', {
label: () => (buildingNames[kind()] || '') + ', ' + area() + ' sq ft (' + describeSize() + ', ' + roofName().toLowerCase() + ')',
computedValue: () => Math.round(structure()),
decimals: 0,
variant: 'default'
}, '1fr');
row.addPriceDisplay('sidesCost', {
label: 'Closed sides & gable ends',
computedValue: () => Math.round(sidesCost()),
decimals: 0,
variant: 'default',
prefix: '+',
isVisible: () => sidesCost() > 0
}, '1fr');
});
 
summarySection.addRow(row => {
row.addPriceDisplay('doorsCost', {
label: () => 'Doors & windows (' + describeDoors() + ')',
computedValue: () => Math.round(doorsCost()),
decimals: 0,
variant: 'default',
prefix: '+',
isVisible: () => hasWalls()
}, '1fr');
row.addPriceDisplay('extrasCost', {
label: () => 'Insulation & wainscot' + (insulation() ? ' (' + Math.round(insulatedArea()) + ' sq ft insulated)' : ''),
computedValue: () => Math.round(insulationCost() + wainscotCost()),
decimals: 0,
variant: 'default',
prefix: '+',
isVisible: () => insulationCost() + wainscotCost() > 0
}, '1fr');
});
 
summarySection.addRow(row => {
row.addPriceDisplay('siteWorkCost', {
label: () => 'Site work (' + (padNames[pad()] || '').toLowerCase() + (permit() ? ', permit handled by us' : ', permit by the owner') + ')',
computedValue: () => Math.round(padCost() + permitCost()),
decimals: 0,
variant: 'default',
prefix: '+'
}, '1fr');
row.addPriceDisplay('deliveryCost', {
label: () => 'Delivery & installation (' + distance() + ' mi from our yard' + (delivery() === 0 ? ', included' : '') + ')',
computedValue: () => Math.round(delivery()),
decimals: 0,
variant: 'default',
prefix: '+'
}, '1fr');
});
 
summarySection.addRow(row => {
row.addPriceDisplay('prepayDiscount', {
label: 'Paid in full at the order (' + prepayPercent + '% off)',
computedValue: () => -Math.round(prepayDiscount()),
decimals: 0,
variant: 'success',
isVisible: () => prepayDiscount() > 0
}, '1fr');
row.addPriceDisplay('promoDiscount', {
label: () => promoText + ' (-' + promoPercent + '%)',
computedValue: () => -Math.round(promo()),
decimals: 0,
variant: 'success',
isVisible: () => promo() > 0
}, '1fr');
});
 
summarySection.addSpacer({ showLine: true, lineStyle: 'solid', lineColor: '#e2e8f0' });
 
summarySection.addRow(row => {
row.addPriceDisplay('total', {
label: () => (buildingNames[kind()] || '') + ', delivered and installed',
computedValue: () => total(),
decimals: 0,
variant: 'large',
suffix: 'plus sales tax'
});
});
 
summarySection.addRow(row => {
row.addTextPanel('schedule', {
computedValue: () => 'From the order to the keys: '
+ (prepay() ? money(deposit()) + ' paid in full at the order' : money(deposit()) + ' deposit at the order, ' + money(total() - deposit()) + ' due when the crew is done')
+ '. ' + (permit() ? 'We pull the permit' + (certified() ? ' with the engineer-stamped drawings' : '') + '.' : 'Where your county requires a permit, you pull it' + (certified() ? '; we supply the engineer-stamped drawings' : '') + '.')
+ ' ' + (pad() === 'existing' ? 'You have the site level within 3 inches before the crew arrives.' : 'Our crew does the site work a few days before the building.')
+ ' Delivery and installation about ' + leadWeeks + ' weeks from the order; we call first to confirm the color, the site and the access for the truck.',
customStyles: { 'font-size': '0.9rem', 'color': '#0369a1', 'background': '#e0f2fe', 'padding': '10px', 'border-radius': '6px' }
});
});
 
summarySection.addRow(row => {
row.addTextPanel('minimumNote', {
computedValue: () => 'Orders below ' + money(minimumOrder) + ' are priced at the minimum, because a delivery has to pay for itself.',
customStyles: { 'font-size': '0.85rem', 'color': '#64748b', 'font-style': 'italic' },
isVisible: () => Math.round(beforePromo() - promo()) < minimumOrder
});
});
 
const contactSection = quotePage.addSubform('contact', { title: '📋 Your Contact Details', isCollapsible: false });
 
contactSection.addRow(row => {
row.addTextbox('name', {
label: 'Full name',
placeholder: 'John Smith',
isRequired: true
}, '1fr');
row.addTextbox('phone', {
label: 'Phone',
placeholder: '(336) 555-0100',
isRequired: true
}, '1fr');
});
 
contactSection.addRow(row => {
row.addEmail('email', {
label: 'Email (where we send the PDF quote)',
placeholder: 'john@example.com',
isRequired: true
}, '1fr');
row.addTextbox('address', {
label: 'Installation address',
placeholder: '1204 Pine Ridge Rd',
isRequired: true
}, '1fr');
});
 
contactSection.addRow(row => {
row.addTextbox('cityState', {
label: 'City, state',
placeholder: 'Mount Airy, NC',
isRequired: true
}, '1fr');
row.addTextbox('zip', {
label: 'ZIP code',
placeholder: '27030',
isRequired: true
}, '1fr');
});
 
contactSection.addRow(row => {
row.addTextarea('notes', {
label: 'Anything else we should know?',
placeholder: 'E.g. the site is behind the house, the gate is 10 ft wide, the HOA wants the same color as the roof of the house',
rows: 3,
isRequired: false
});
});
 
// Sticky total, visible on every step
const finalSection = form.addSubform('final', {
title: '🧾 Summary',
isCollapsible: false,
sticky: 'bottom'
});
 
finalSection.addRow(row => {
row.addPriceDisplay('totalPrice', {
label: () => (buildingNames[kind()] || '') + ', ' + describeSize(),
computedValue: () => total(),
decimals: 0,
variant: 'large',
suffix: 'delivered and installed, plus sales tax'
});
});
 
finalSection.addRow(row => {
row.addTextPanel('disclaimer', {
computedValue: () => 'Site preparation and permit as chosen on the site step. The order is confirmed by phone.',
customStyles: { 'font-size': '0.85rem', 'color': '#94a3b8', 'font-style': 'italic' }
});
});
 
const fullAddress = () => [contactSection.textbox('address')?.value(), contactSection.textbox('cityState')?.value(), contactSection.textbox('zip')?.value()].filter(Boolean).join(', ');
 
// ---- PDF quote ---------------------------------------------------------------------
form.configurePdf('quote', pdf => {
pdf.configure({
filename: 'building-quote',
pageSize: 'Letter',
allowUserDownload: true,
downloadButtonLabel: 'Download the quote (PDF)',
header: {
title: 'Quote: ' + (buildingNames[kind()] || '').toLowerCase(),
subtitle: describeSize() + ' - ' + roofName().toLowerCase()
},
footer: {
text: 'Delivery and installation included. Site preparation, permit and sales tax as noted.',
showPageNumbers: true
}
});
 
pdf.addSection('Customer', section => {
section.addRow(row => row
.addField('Name', contactSection.textbox('name')?.value() || '')
.addField('Phone', contactSection.textbox('phone')?.value() || '')
.addField('Email', contactSection.email('email')?.value() || '')
.addField('Installation address', fullAddress()));
});
 
pdf.addSection('Building', section => {
section.addRow(row => row
.addField('Type', buildingNames[kind()] || '')
.addField('Size', describeSize() + (isSteel() ? ' (' + bays() + ' bays)' : ''))
.addField('Floor area', area() + ' sq ft')
.addField('Roof', roofName()));
section.addRow(row => row
.addField('Colors', describeColors())
.addField('Frame', describeFrame())
.addField('Doors', describeDoors())
.addField('Extras', describeExtras()));
});
 
pdf.addSection('Site', section => {
section.addRow(row => row
.addField('Pad', padNames[pad()] || '')
.addField('Permit', permit() ? 'pulled by us' : 'by the owner, where required')
.addField('Distance', distance() + ' mi from our yard')
.addField('Timing', timingNames[timing()] || ''));
});
 
pdf.addSection('Quote', section => {
const rows: string[][] = [
[(buildingNames[kind()] || '') + ', ' + area() + ' sq ft', describeSize() + ', ' + roofName().toLowerCase(), money(structure())]
];
if (sidesCost() > 0) rows.push(['Closed sides and gable ends', [closedSides() ? 'both sides' : '', gableEnds() ? 'both ends' : ''].filter(Boolean).join(', '), money(sidesCost())]);
if (insulationCost() > 0) rows.push(['Insulation', Math.round(insulatedArea()) + ' sq ft', money(insulationCost())]);
if (wainscotCost() > 0) rows.push(['Wainscot', perimeter() + ' ft', money(wainscotCost())]);
if (hasWalls()) rows.push(['Doors and windows', describeDoors() + (walkDoor() ? ', walk-in door' : '') + (windows() > 0 ? ', ' + windows() + ' window' + (windows() > 1 ? 's' : '') : ''), money(doorsCost())]);
rows.push(['Site work', (padNames[pad()] || '') + (permit() ? ', permit' : ''), money(padCost() + permitCost())]);
rows.push(['Delivery and installation', delivery() === 0 ? 'included' : distance() + ' mi', money(delivery())]);
rows.push(['Subtotal', '', money(subtotal())]);
if (prepayDiscount() > 0) rows.push(['Paid in full at the order', prepayPercent + '%', '-' + money(prepayDiscount())]);
if (promo() > 0) rows.push([promoText, promoPercent + '%', '-' + money(promo())]);
rows.push(['Total', 'plus sales tax', money(total())]);
section.addTable(['Item', 'Details', 'Amount'], rows);
section.addSpacer(6);
section.addText(prepay()
? 'Paid in full at the order: ' + money(total()) + '.'
: money(deposit()) + ' deposit at the order, ' + money(total() - deposit()) + ' at installation.');
});
 
pdf.addSection('What happens next', section => {
section.addText('We call to confirm the color, the site and the access for the delivery truck. '
+ (permit() ? 'We pull the building permit' + (certified() ? ' with the engineer-stamped drawings' : '') + '; the county fee is billed at cost. ' : 'Where your county requires a permit, you pull it' + (certified() ? '; we supply the engineer-stamped drawings' : '') + '. ')
+ (pad() === 'existing' ? 'You have the site level within 3 inches and clear before the crew arrives. ' : 'Our crew does the site work a few days before the building. ')
+ 'Delivery and installation about ' + leadWeeks + ' weeks from the order' + (prepay() ? '.' : '; the balance is due when the crew is done.'));
});
 
const notes = contactSection.textarea('notes')?.value();
if (notes) {
pdf.addSection('Notes', section => section.addText(notes));
}
 
pdf.addSection('Terms', section => {
section.addText('Prices include the building, delivery and installation on a prepared site; sales tax not included. Site preparation and permit as noted above. This quote is not an offer to contract; the order is confirmed by phone.');
});
});
 
form.configureSubmitButton({
label: 'Send My Configuration for a Quote'
});
 
form.configureCompletionScreen({
type: 'text',
title: 'Thank you!',
message: 'The quote is on its way to your email. We will call to confirm the site and the access for the truck; delivery and installation about ' + leadWeeks + ' weeks from the order.'
});
}
 

Frequently Asked Questions

Can I change the prices to match my dealer price list?

Yes. The per-square-foot rate of each building, the roof and frame surcharges, every door price, the slab, gravel and anchor prices and the delivery rate are constants at the top of the code. Change them once and every estimate follows.

How does the calculator handle steel building lengths?

Steel buildings come in 5-foot bays, so the calculator rounds the length to the nearest bay and shows the built length and the number of bays. Wood garages and pole barns use the length as entered.

Are delivery and installation included in the price?

Yes, within the free radius you set. Beyond it the calculator adds a per-mile charge. The customer prepares a level site, or picks a concrete slab or a gravel pad and the calculator prices that too.

Can I remove buildings or doors I do not sell?

Yes. Delete the building type from the list, or remove a door type or size, and the pricing adjusts. You can also add a building with its own rate and typical size.

What does the customer get after submitting?

A PDF quote with the configuration, the price breakdown and the deposit schedule, while you receive the configuration, the installation address and the contact details to confirm the order by phone.