Driveway Gate Calculator

Give your gate and access company website a quote calculator that works the way an estimator does. Customers choose the gate style (wrought iron, ornamental aluminum, wood, aluminum privacy, ranch rail or farm tube), the color, a flat or arched top and spear finials; then how it opens (single swing, double swing or cantilever slide), the width and height in feet, a walk gate, matching fence sections and the columns it hangs on. The next step prices the opener (swing-arm, in-ground or slide operator, solar where there is no power), the keypad, video intercom, phone app, free-exit loop and fire department access, then the site details an installer needs before the visit: driveway surface and slope, power at the gate, tear-out of the old gate, HOA and permit. Customers who already have a gate can price the opener alone. Every rate is a constant you edit to match your price list.

Home Services

Try the Calculator

Driveway Gate & Opener Estimate
Pick the gate style, how it opens, the opener and the access control. We come out to measure within 3 business days.
🏡 Your Entry
 
 
🚪 The Gate
 
 
 
🧱 Walk Gate, Fence & Columns
0ft
0 ft
080
 
 
⚙️ Opener & Access
 
🏗️ Site & Schedule
 
💰 Your Estimate
$ 3,110
+
$ 0
+
$ 0
+
$ 1,400

$ 3,950
$ 5,050
installed
What happens next: an estimator calls you and comes out within 3 business days to measure the opening and check the power. After the visit you get a firm quote, and the gate is hung and the opener programmed about 5 weeks after you sign. The range (±12%) covers what the site visit usually turns up.
📋 Your Contact Details
 
 
 
 
 
🧾 Summary
$ 3,950
$ 5,050
Estimated cost, installed. The firm quote follows the free site visit.
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
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
export function drivewayGateCalculator(form: FormTs) {
// Gate price per foot of opening by style (fabricated, powder coated or sealed)
const stylePrices: Record<string, number> = {
'iron': 240,
'aluminum': 190,
'wood': 230,
'privacy': 260,
'ranch': 150,
'farm': 55
};
 
const styleNames: Record<string, string> = {
'iron': 'Wrought iron',
'aluminum': 'Ornamental aluminum',
'wood': 'Wood',
'privacy': 'Aluminum privacy',
'ranch': 'Ranch rail',
'farm': 'Farm tube gate'
};
 
// Colors each style is coated in; the first two are standard (no charge)
const styleColors: Record<string, string[]> = {
'iron': ['black', 'bronze', 'white', 'charcoal', 'green'],
'aluminum': ['black', 'bronze', 'white', 'charcoal'],
'wood': ['cedar', 'redwood'],
'privacy': ['black', 'bronze', 'charcoal', 'white'],
'ranch': ['black', 'galvanized', 'green', 'charcoal'],
'farm': ['galvanized', 'green', 'black']
};
 
const colorNames: Record<string, string> = {
'black': 'Black', 'bronze': 'Bronze', 'white': 'White', 'charcoal': 'Charcoal', 'green': 'Hunter green',
'galvanized': 'Galvanized', 'cedar': 'Cedar', 'redwood': 'Redwood'
};
 
// Percent surcharges on the gate (and on the walk gate and fence where noted)
const surcharges = {
doubleSwing: 8, // two leaves: two sets of hinges, drop rod, center stop
slide: 30, // cantilever frame, roller posts, receiver
tall: 15, // 7 ft and 8 ft gates, walk gate and fence
arch: 12, // rolled top rail
customColor: 8, // a color beyond the two standard ones of the style
redwood: 20 // redwood instead of cedar
};
 
const extras = {
finialsPerFt: 18, // spear finials, iron and aluminum only
walkGate: 1250, // 4 ft walk gate with latch and post, installed
fencePerFt: 78, // matching fence sections, per linear foot
lantern: 420 // per lantern on a column or post
};
 
const columnPrices: Record<string, number> = {
'steel': 0,
'stone': 2900,
'brick': 2300,
'stucco': 1900
};
 
const columnNames: Record<string, string> = {
'steel': 'Steel posts',
'stone': 'Stone columns',
'brick': 'Brick columns',
'stucco': 'Stucco columns'
};
 
const openerPrices = {
armPerLeaf: 1650,
undergroundPerLeaf: 3400,
slideOperator: 2900,
solarKit: 850,
app: 320,
remote: 45
};
 
const openerNames: Record<string, string> = {
'none': 'Manual, no opener',
'arm': 'Swing-arm opener',
'underground': 'In-ground opener',
'slide': 'Slide gate operator'
};
 
const accessPrices: Record<string, number> = {
'keypad': 520,
'intercom': 1450,
'exitLoop': 680,
'knox': 390
};
 
const accessNames: Record<string, string> = {
'keypad': 'Keypad on a pedestal',
'intercom': 'Video intercom',
'exitLoop': 'Free-exit loop',
'knox': 'Fire department access'
};
 
const siteWork = {
installGate: 1400,
installOpener: 950,
tearOut: 375,
powerPerFt: 14,
electrician: 350,
slideFooting: 1750,
permitFee: 275
};
 
// Promotion applied last on the whole job; 0 hides the banner
const promoPercent = 0;
const promoText = 'Free keypad with any opener';
// The range shown to the customer, in percent each way
const rangePercent = 12;
const estimateDays = 3;
const leadTimeWeeks = 5;
 
const BASE_HEIGHT = 6;
const WALK_GATE_FT = 4;
const SLIDE_RUNOFF = 1.5;
const SINGLE_SWING_MAX_FT = 16;
 
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 feet = (value: number) => value + ' ft';
 
form.addRow(row => {
row.addTextPanel('header', {
computedValue: () => 'Driveway Gate & Opener Estimate',
customStyles: { 'font-size': '1.5rem', 'font-weight': '600', 'color': '#1e293b' }
});
});
 
form.addRow(row => {
row.addTextPanel('intro', {
computedValue: () => 'Pick the gate style, how it opens, the opener and the access control. We come out to measure within ' + estimateDays + ' business days.',
customStyles: { 'font-size': '0.95rem', 'color': '#475569' }
});
});
 
form.addSpacer({ height: 20 });
 
const steps = form.addPages('steps', { heightMode: 'current-page' });
 
// ---- Step 1: the project -------------------------------------------------------
const projectPage = steps.addPage('project');
const projectSection = projectPage.addSubform('project', { title: '🏡 Your Entry', isCollapsible: false });
 
projectSection.addRow(row => {
row.addRadioButton('projectType', {
label: 'What is the project?',
options: [
{ id: 'new', name: 'New gate at an open driveway' },
{ id: 'replace', name: 'Replace an old gate (we take the old one down)' },
{ id: 'openerOnly', name: 'Opener and access for my existing gate' }
],
defaultValue: 'new',
orientation: 'vertical',
isRequired: true
}, '1fr');
row.addRadioButton('propertyKind', {
label: 'Where is the gate?',
options: [
{ id: 'home', name: 'House in town' },
{ id: 'acreage', name: 'Acreage or ranch' }
],
defaultValue: 'home',
orientation: 'vertical',
isRequired: true
}, '1fr');
});
 
const projectType = form.computedValue(() => projectSection.radioButton('projectType')?.value() || 'new');
const replacing = form.computedValue(() => projectType() === 'replace');
// With an opener-only job the gate is the customer's: described for the opener's sake, never priced
const gatePriced = form.computedValue(() => projectType() !== 'openerOnly');
 
// ---- Step 2: the gate ----------------------------------------------------------
const gatePage = steps.addPage('gate');
const gateSection = gatePage.addSubform('gate', { title: '🚪 The Gate', isCollapsible: false });
 
gateSection.addRow(row => {
row.addTextPanel('existingHint', {
computedValue: () => 'Describe the gate you have: the style, how it opens and the size tell us which opener carries it. Nothing on this step is charged.',
customStyles: { 'font-size': '0.9rem', 'color': '#0369a1', 'background': '#e0f2fe', 'padding': '10px', 'border-radius': '6px' },
isVisible: () => !gatePriced()
});
});
 
gateSection.addRow(row => {
row.addRadioButton('style', {
label: () => gatePriced() ? 'Gate style' : 'Closest to your gate',
options: () => Object.keys(stylePrices).map(id => ({
id,
name: (styleNames[id] || id) + (gatePriced() ? ' - from ' + money(stylePrices[id] || 0) + '/ft' : '')
})),
defaultValue: 'iron',
orientation: 'vertical',
isRequired: true
}, '1fr');
row.addRadioButton('gateType', {
label: 'How does it open?',
options: [
{ id: 'single', name: 'Single swing (one leaf, up to ' + SINGLE_SWING_MAX_FT + ' ft)' },
{ id: 'double', name: 'Double swing (two leaves, +' + surcharges.doubleSwing + '%)' },
{ id: 'slide', name: 'Cantilever slide gate (+' + surcharges.slide + '%)' }
],
defaultValue: 'double',
orientation: 'vertical',
isRequired: true
}, '1fr');
});
 
const style = form.computedValue(() => {
const raw = gateSection.radioButton('style')?.value();
return raw && stylePrices[raw] !== undefined ? raw : 'iron';
});
const gateType = form.computedValue(() => gateSection.radioButton('gateType')?.value() || 'double');
const sliding = form.computedValue(() => gateType() === 'slide');
const leaves = form.computedValue(() => gateType() === 'double' ? 2 : 1);
const isWood = form.computedValue(() => style() === 'wood');
const hasArch = form.computedValue(() => style() !== 'privacy' && style() !== 'farm');
const hasFinials = form.computedValue(() => style() === 'iron' || style() === 'aluminum');
 
gateSection.addRow(row => {
row.addDropdown('color', {
label: () => isWood() ? 'Wood' : 'Color',
options: () => (styleColors[style()] || []).map((id, index) => {
let note = 'included';
if (id === 'redwood') note = '+' + surcharges.redwood + '%';
else if (index > 1) note = '+' + surcharges.customColor + '%';
return { id, name: (colorNames[id] || id) + (gatePriced() ? ' (' + note + ')' : '') };
}),
defaultValue: 'black',
isRequired: true
}, '1fr');
row.addDropdown('width', {
label: 'Opening width (between the posts)',
options: () => [10, 12, 14, 16, 20].map(w => ({
id: String(w),
name: feet(w) + (gateType() === 'single' && w > SINGLE_SWING_MAX_FT ? ' (double or slide only)' : '')
})),
defaultValue: '12',
isRequired: true
}, '1fr');
row.addDropdown('height', {
label: 'Height',
options: [4, 5, 6, 7, 8].map(h => ({
id: String(h),
name: feet(h) + (h > BASE_HEIGHT ? ' (+' + surcharges.tall + '%)' : '')
})),
defaultValue: '6',
isRequired: true
}, '1fr');
});
 
// The chosen color if the style comes in it, else the style's first color
const color = form.computedValue(() => {
const colors = styleColors[style()] || ['black'];
const raw = gateSection.dropdown('color')?.value() || '';
return colors.includes(raw) ? raw : (colors[0] || 'black');
});
const colorPercent = form.computedValue(() => {
const colors = styleColors[style()] || [];
const index = colors.indexOf(color());
if (color() === 'redwood') return surcharges.redwood;
return index > 1 ? surcharges.customColor : 0;
});
// A single swing leaf is offered up to 16 ft: a wider pick falls back to that limit
const width = form.computedValue(() => {
const raw = Number(gateSection.dropdown('width')?.value() || 12);
const max = gateType() === 'single' ? SINGLE_SWING_MAX_FT : 24;
return Math.min(raw || 12, max);
});
const height = form.computedValue(() => Number(gateSection.dropdown('height')?.value() || 6) || 6);
const tall = form.computedValue(() => height() > BASE_HEIGHT);
 
gateSection.addRow(row => {
row.addRadioButton('top', {
label: 'Top',
options: [
{ id: 'flat', name: 'Flat' },
{ id: 'arch', name: 'Arched (+' + surcharges.arch + '%)' }
],
defaultValue: 'flat',
orientation: 'horizontal',
isVisible: () => hasArch()
}, '1fr');
row.addCheckbox('finials', {
label: 'Spear finials (+' + money(extras.finialsPerFt) + '/ft of gate and fence)',
defaultValue: false,
isVisible: () => hasFinials()
}, '1fr');
});
 
const arched = form.computedValue(() => hasArch() && gateSection.radioButton('top')?.value() === 'arch');
const finials = form.computedValue(() => hasFinials() && gateSection.checkbox('finials')?.value() === true);
 
// Walk gate, fence and columns: only when the company builds the gate
const entrySection = gatePage.addSubform('entry', {
title: '🧱 Walk Gate, Fence & Columns',
isCollapsible: false,
isVisible: () => gatePriced()
});
 
entrySection.addRow(row => {
row.addCheckbox('walkGate', {
label: 'Walk gate beside the drive gate - 4 ft, same style, with a latch (+' + money(extras.walkGate) + ')',
defaultValue: false
}, '1fr');
row.addSlider('fenceFeet', {
label: 'Matching fence sections, both sides together (+' + money(extras.fencePerFt) + '/ft)',
min: 0,
max: 80,
step: 2,
unit: 'ft',
defaultValue: 0
}, '1fr');
});
 
entrySection.addRow(row => {
row.addRadioButton('columns', {
label: 'What does the gate hang on?',
options: [
{ id: 'steel', name: 'Steel posts in the gate color (included)' },
{ id: 'stone', name: 'Stone veneer columns with a cast stone cap (+' + money(columnPrices['stone'] || 0) + '/column)' },
{ id: 'brick', name: 'Brick columns with a cap (+' + money(columnPrices['brick'] || 0) + '/column)' },
{ id: 'stucco', name: 'Stucco columns to match the house (+' + money(columnPrices['stucco'] || 0) + '/column)' }
],
defaultValue: 'steel',
orientation: 'vertical'
}, '1fr');
row.addRadioButton('ownColumns', {
label: 'Do you already have columns or posts at the entry?',
options: [
{ id: 'no', name: 'No, build new' },
{ id: 'yes', name: 'Yes, hang the gate on mine' }
],
defaultValue: 'no',
orientation: 'vertical'
}, '1fr');
});
 
entrySection.addRow(row => {
row.addCheckbox('lanterns', {
label: 'Lanterns, one on each gate column or post (+' + money(extras.lantern) + ' each)',
defaultValue: false
});
});
 
const walkGate = form.computedValue(() => gatePriced() && entrySection.checkbox('walkGate')?.value() === true);
const fenceFeet = form.computedValue(() => gatePriced() ? (entrySection.slider('fenceFeet')?.value() || 0) : 0);
const ownColumns = form.computedValue(() => gatePriced() && entrySection.radioButton('ownColumns')?.value() === 'yes');
const columns = form.computedValue(() => {
if (!gatePriced() || ownColumns()) return 'steel';
return entrySection.radioButton('columns')?.value() || 'steel';
});
const columnCount = form.computedValue(() => columns() !== 'steel' ? 2 + (walkGate() ? 1 : 0) : 0);
const lanterns = form.computedValue(() => gatePriced() && entrySection.checkbox('lanterns')?.value() === true);
 
// ---- Step 3: the opener and the access control -------------------------------
const accessPage = steps.addPage('access');
const openerSection = accessPage.addSubform('opener', { title: '⚙️ Opener & Access', isCollapsible: false });
 
openerSection.addRow(row => {
row.addRadioButton('opener', {
label: 'Opener (every opener comes with two remotes, photo eyes and a warning light)',
options: () => {
const options: ControlOption[] = [];
if (gatePriced()) options.push({ id: 'none', name: 'Manual - latch and drop rod, no opener' });
if (sliding()) {
options.push({ id: 'slide', name: 'Slide gate operator, chain drive (+' + money(openerPrices.slideOperator) + ')' });
} else {
options.push({ id: 'arm', name: 'Swing-arm opener, one per leaf (+' + money(openerPrices.armPerLeaf) + '/leaf)' });
options.push({ id: 'underground', name: 'In-ground opener, hidden under the hinge (+' + money(openerPrices.undergroundPerLeaf) + '/leaf)' });
}
return options;
},
defaultValue: 'none',
orientation: 'vertical',
isRequired: true
});
});
 
// The chosen opener only while it fits the gate type and the project
const opener = form.computedValue(() => {
const raw = openerSection.radioButton('opener')?.value() || 'none';
if (raw === 'none') return 'none';
if (sliding()) return raw === 'slide' ? 'slide' : 'none';
return raw === 'arm' || raw === 'underground' ? raw : 'none';
});
const automated = form.computedValue(() => opener() !== 'none');
 
openerSection.addRow(row => {
row.addTextPanel('pickOpener', {
computedValue: () => 'For a gate you already have we quote the opener and the access control, so the estimate needs an opener picked above.',
customStyles: { 'font-size': '0.9rem', 'color': '#b45309', 'background': '#fef3c7', 'padding': '10px', 'border-radius': '6px' },
isVisible: () => !gatePriced() && !automated()
});
});
 
openerSection.addRow(row => {
row.addCheckbox('solar', {
label: 'Solar-powered - a panel on a pole and a battery, no trench (+' + money(openerPrices.solarKit) + ')',
defaultValue: false,
isVisible: () => automated()
}, '1fr');
row.addCheckbox('app', {
label: 'Phone app - open from anywhere, share access (+' + money(openerPrices.app) + ')',
defaultValue: false,
isVisible: () => automated()
}, '1fr');
});
 
const solar = form.computedValue(() => automated() && openerSection.checkbox('solar')?.value() === true);
const app = form.computedValue(() => automated() && openerSection.checkbox('app')?.value() === true);
 
openerSection.addRow(row => {
row.addCheckboxList('access', {
label: 'Access control',
options: Object.keys(accessPrices).map(id => ({
id,
name: (accessNames[id] || id) + ' (+' + money(accessPrices[id] || 0) + ')'
})),
orientation: 'vertical',
isVisible: () => automated()
}, '1fr');
row.addInteger('remotes', {
label: 'Extra remotes (two come with the opener, ' + money(openerPrices.remote) + ' each more)',
min: 0,
max: 10,
defaultValue: 0,
isVisible: () => automated()
}, '1fr');
});
 
const access = form.computedValue(() => automated() ? (openerSection.checkboxList('access')?.value() || []).filter(id => accessPrices[id] !== undefined) : []);
const remotes = form.computedValue(() => automated() ? (openerSection.integer('remotes')?.value() || 0) : 0);
 
openerSection.addRow(row => {
row.addRadioButton('power', {
label: 'Power at the gate (the opener needs a 120 V circuit)',
options: [
{ id: 'yes', name: 'Within reach' },
{ id: 'no', name: 'Needs a run from the house' },
{ id: 'unsure', name: 'Not sure - check on the site visit' }
],
defaultValue: 'unsure',
orientation: 'vertical',
isVisible: () => automated() && !solar()
}, '1fr');
row.addSlider('powerFeet', {
label: 'Distance from the house to the gate (' + money(siteWork.powerPerFt) + '/ft plus the electrician)',
min: 20,
max: 400,
step: 10,
unit: 'ft',
defaultValue: 80,
isVisible: () => automated() && !solar() && openerSection.radioButton('power')?.value() === 'no'
}, '1fr');
});
 
const power = form.computedValue(() => automated() && !solar() ? (openerSection.radioButton('power')?.value() || 'unsure') : 'yes');
const powerRun = form.computedValue(() => power() === 'no');
const powerFeet = form.computedValue(() => powerRun() ? (openerSection.slider('powerFeet')?.value() || 80) : 0);
 
// ---- Step 4: the site ----------------------------------------------------------
const sitePage = steps.addPage('site');
const siteSection = sitePage.addSubform('site', { title: '🏗️ Site & Schedule', isCollapsible: false });
 
siteSection.addRow(row => {
row.addDropdown('timing', {
label: 'When would you like the gate working?',
options: [
{ id: 'asap', name: 'As soon as possible' },
{ id: 'quarter', name: 'Within 3 months' },
{ id: 'year', name: 'This year' },
{ id: 'comparing', name: 'Just comparing quotes' }
],
defaultValue: 'quarter'
}, '1fr');
row.addCheckbox('tearOut', {
label: 'Tear-out of the old gate and its posts, with haul-away (+' + money(siteWork.tearOut) + ')',
defaultValue: true,
isVisible: () => replacing()
}, '1fr');
});
 
const tearOut = form.computedValue(() => replacing() && siteSection.checkbox('tearOut')?.value() === true);
 
siteSection.addRow(row => {
row.addDropdown('surface', {
label: 'What is the driveway at the gate?',
options: [
{ id: 'concrete', name: 'Concrete' },
{ id: 'pavers', name: 'Pavers' },
{ id: 'asphalt', name: 'Asphalt' },
{ id: 'gravel', name: 'Gravel (an exit loop becomes a buried probe)' }
],
defaultValue: 'concrete'
}, '1fr');
row.addDropdown('slope', {
label: 'How does the driveway run at the gate?',
options: [
{ id: 'level', name: 'Level' },
{ id: 'up', name: 'Slopes up into the property' },
{ id: 'down', name: 'Slopes down from the street' }
],
defaultValue: 'level'
}, '1fr');
});
 
const surface = form.computedValue(() => siteSection.dropdown('surface')?.value() || 'concrete');
const slope = form.computedValue(() => siteSection.dropdown('slope')?.value() || 'level');
 
siteSection.addRow(row => {
row.addTextPanel('uphillWarning', {
computedValue: () => 'A swing gate on an uphill driveway: leaves swinging into the property hit the rising ground. On the site visit we check the grade; the fixes are uphill hinges, a shorter leaf or a slide gate.',
customStyles: { 'font-size': '0.9rem', 'color': '#b45309', 'background': '#fef3c7', 'padding': '10px', 'border-radius': '6px' },
isVisible: () => slope() === 'up' && !sliding()
});
});
 
siteSection.addRow(row => {
row.addRadioButton('footing', {
label: () => 'Footing for the slide gate - about ' + feet(Math.round(width() * SLIDE_RUNOFF)) + ' of concrete along the fence line',
options: [
{ id: 'have', name: 'I have one' },
{ id: 'need', name: 'Needs to be poured (+' + money(siteWork.slideFooting) + ')' },
{ id: 'unsure', name: 'Not sure' }
],
defaultValue: 'unsure',
orientation: 'vertical',
isVisible: () => sliding() && gatePriced()
});
});
 
const footing = form.computedValue(() => sliding() && gatePriced() ? (siteSection.radioButton('footing')?.value() || 'unsure') : 'have');
 
siteSection.addRow(row => {
row.addRadioButton('hoa', {
label: 'Is the property in an HOA?',
options: [
{ id: 'no', name: 'No HOA' },
{ id: 'yes', name: 'Yes, rules apply' }
],
defaultValue: 'no',
orientation: 'horizontal'
}, '1fr');
row.addTextarea('hoaNotes', {
label: 'HOA notes',
placeholder: 'E.g. 6 ft max, black or bronze only, no solid gates facing the street',
rows: 2,
isVisible: () => siteSection.radioButton('hoa')?.value() === 'yes'
}, '1fr');
});
 
const inHoa = form.computedValue(() => siteSection.radioButton('hoa')?.value() === 'yes');
 
// ---- Price logic ---------------------------------------------------------------
// Surcharges that apply to everything in the chosen style: the height and the color
const stylePercent = form.computedValue(() => (tall() ? surcharges.tall : 0) + colorPercent());
 
const gateCost = form.computedValue(() => {
if (!gatePriced()) return 0;
const typePercent = gateType() === 'double' ? surcharges.doubleSwing : gateType() === 'slide' ? surcharges.slide : 0;
const percent = stylePercent() + typePercent + (arched() ? surcharges.arch : 0);
const base = width() * (stylePrices[style()] || 0) * (1 + percent / 100);
return base + (finials() ? width() * extras.finialsPerFt : 0);
});
 
const walkGateCost = form.computedValue(() => walkGate()
? extras.walkGate * (1 + stylePercent() / 100) + (finials() ? WALK_GATE_FT * extras.finialsPerFt : 0)
: 0);
const fenceCost = form.computedValue(() => fenceFeet() * (extras.fencePerFt * (1 + stylePercent() / 100) + (finials() ? extras.finialsPerFt : 0)));
const columnsCost = form.computedValue(() => columnCount() * (columnPrices[columns()] || 0));
const lanternsCost = form.computedValue(() => lanterns() ? 2 * extras.lantern : 0);
 
const openerCost = form.computedValue(() => {
let cost = 0;
if (opener() === 'arm') cost = openerPrices.armPerLeaf * leaves();
else if (opener() === 'underground') cost = openerPrices.undergroundPerLeaf * leaves();
else if (opener() === 'slide') cost = openerPrices.slideOperator;
else return 0;
return cost + (solar() ? openerPrices.solarKit : 0) + (app() ? openerPrices.app : 0);
});
 
const accessCost = form.computedValue(() => {
let cost = 0;
for (const id of access()) cost += accessPrices[id] || 0;
return cost + remotes() * openerPrices.remote;
});
 
const powerCost = form.computedValue(() => powerRun() ? powerFeet() * siteWork.powerPerFt + siteWork.electrician : 0);
const siteCost = form.computedValue(() =>
(gatePriced() ? siteWork.installGate : 0)
+ (automated() ? siteWork.installOpener + siteWork.permitFee : 0)
+ (tearOut() ? siteWork.tearOut : 0)
+ powerCost()
+ (footing() === 'need' ? siteWork.slideFooting : 0));
 
const beforePromo = form.computedValue(() => gateCost() + walkGateCost() + fenceCost() + columnsCost() + lanternsCost() + openerCost() + accessCost() + siteCost());
const promo = form.computedValue(() => beforePromo() * promoPercent / 100);
const price = form.computedValue(() => Math.round(beforePromo() - promo()));
const priceFrom = form.computedValue(() => Math.round(price() * (1 - rangePercent / 100) / 50) * 50);
const priceTo = form.computedValue(() => Math.round(price() * (1 + rangePercent / 100) / 50) * 50);
 
const describeGate = (): string => {
const typeName = gateType() === 'single' ? 'Single swing' : gateType() === 'double' ? 'Double swing' : 'Slide gate';
const parts = [
typeName + ', ' + (styleNames[style()] || '').toLowerCase() + ', ' + (colorNames[color()] || '').toLowerCase(),
feet(width()) + ' opening, ' + feet(height()) + ' tall',
arched() ? 'arched top' : 'flat top'
];
if (finials()) parts.push('spear finials');
return parts.join(', ');
};
 
const describeOpener = (): string => {
if (!automated()) return gatePriced() ? 'manual, no opener' : 'not picked yet';
const parts = [(openerNames[opener()] || '').toLowerCase() + (opener() !== 'slide' ? ' x ' + leaves() : '')];
if (solar()) parts.push('solar-powered');
if (app()) parts.push('phone app');
if (remotes() > 0) parts.push(remotes() + ' extra remotes');
return parts.join(', ');
};
 
const describeAccess = (): string => access().map(id => (accessNames[id] || id).toLowerCase()).join(', ') || 'remotes only';
 
const describeSite = (): string => {
const slopeText = slope() === 'up' ? 'slopes up into the property' : slope() === 'down' ? 'slopes down from the street' : 'level';
const parts = ['driveway: ' + surface() + ', ' + slopeText];
if (automated()) {
const powerText = power() === 'yes' ? 'within reach' : power() === 'no' ? 'run of about ' + feet(powerFeet()) : 'to check on the site visit';
parts.push('power: ' + (solar() ? 'solar, no run' : powerText));
}
if (sliding() && gatePriced()) parts.push('slide footing: ' + (footing() === 'have' ? 'in place' : footing() === 'need' ? 'to pour' : 'to check'));
if (tearOut()) parts.push('tear-out of the old gate');
parts.push(inHoa() ? 'HOA rules apply' : 'no HOA');
return parts.join('; ');
};
 
// ---- Step 5: the estimate and contact ------------------------------------------
const quotePage = steps.addPage('quote');
 
quotePage.addRow(row => {
row.addTextPanel('promoBanner', {
computedValue: () => promoText + ': ' + promoPercent + '% off is already in the range 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 Estimate', isCollapsible: false });
 
summarySection.addRow(row => {
row.addPriceDisplay('gateCost', {
label: () => gatePriced() ? 'Gate: ' + (styleNames[style()] || '').toLowerCase() + ', ' + feet(width()) : 'Your gate (existing, not quoted)',
computedValue: () => Math.round(gateCost()),
decimals: 0,
variant: 'default'
}, '1fr');
row.addPriceDisplay('entryCost', {
label: 'Walk gate, fence, columns & lanterns',
computedValue: () => Math.round(walkGateCost() + fenceCost() + columnsCost() + lanternsCost()),
decimals: 0,
variant: 'default',
prefix: '+',
isVisible: () => gatePriced()
}, '1fr');
});
 
summarySection.addRow(row => {
row.addPriceDisplay('openerCost', {
label: () => 'Opener: ' + describeOpener(),
computedValue: () => Math.round(openerCost()),
decimals: 0,
variant: 'default',
prefix: '+'
}, '1fr');
row.addPriceDisplay('accessCost', {
label: () => 'Access control: ' + describeAccess(),
computedValue: () => Math.round(accessCost()),
decimals: 0,
variant: 'default',
prefix: '+',
isVisible: () => automated()
}, '1fr');
});
 
summarySection.addRow(row => {
row.addPriceDisplay('siteCost', {
label: () => 'Installation & site work' + (automated() ? ' (incl. ' + money(siteWork.permitFee) + ' electrical permit)' : ''),
computedValue: () => Math.round(siteCost()),
decimals: 0,
variant: 'default',
prefix: '+'
}, '1fr');
row.addPriceDisplay('promoCost', {
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('estimateFrom', {
label: 'Estimated cost, from',
computedValue: () => priceFrom(),
decimals: 0,
variant: 'large'
}, '1fr');
row.addPriceDisplay('estimateTo', {
label: 'to',
computedValue: () => priceTo(),
decimals: 0,
variant: 'large',
suffix: 'installed'
}, '1fr');
});
 
summarySection.addRow(row => {
row.addTextPanel('nextSteps', {
computedValue: () => 'What happens next: an estimator calls you and comes out within ' + estimateDays
+ ' business days to measure the opening and check the power. After the visit you get a firm quote, and the gate is hung and the opener programmed about '
+ leadTimeWeeks + ' weeks after you sign. The range (±' + rangePercent + '%) covers what the site visit usually turns up.',
customStyles: { 'font-size': '0.9rem', 'color': '#0369a1', 'background': '#e0f2fe', 'padding': '10px', 'border-radius': '6px' }
});
});
 
const contactSection = quotePage.addSubform('contact', { title: '📋 Your Contact Details', isCollapsible: false });
 
contactSection.addRow(row => {
row.addTextbox('name', {
label: 'Name',
placeholder: 'John Smith',
isRequired: true
}, '1fr');
row.addTextbox('phone', {
label: 'Phone',
placeholder: '(555) 123-4567',
isRequired: true
}, '1fr');
});
 
contactSection.addRow(row => {
row.addTextbox('address', {
label: 'Property address (street, city and ZIP)',
placeholder: '1740 W Mission Rd, Escondido, CA 92029',
isRequired: true
}, '1fr');
row.addEmail('email', {
label: 'Email (for the written estimate)',
placeholder: 'john@example.com',
isRequired: false
}, '1fr');
});
 
contactSection.addRow(row => {
row.addRadioButton('callTime', {
label: 'Best time to call',
options: [
{ id: 'anytime', name: 'Anytime' },
{ id: 'morning', name: 'Mornings' },
{ id: 'afternoon', name: 'Afternoons' }
],
defaultValue: 'anytime',
orientation: 'horizontal'
}, '1fr');
});
 
contactSection.addRow(row => {
row.addTextarea('notes', {
label: 'Anything else?',
placeholder: 'E.g. a gate code for the estimator, a second driveway, an old opener to keep, days that work for the visit',
rows: 3,
isRequired: false
});
});
 
// Sticky estimate, visible on every step
const finalSection = form.addSubform('final', {
title: '🧾 Summary',
isCollapsible: false,
sticky: 'bottom'
});
 
finalSection.addRow(row => {
row.addPriceDisplay('totalFrom', {
label: 'Estimated cost, from',
computedValue: () => priceFrom(),
decimals: 0,
variant: 'large'
}, '1fr');
row.addPriceDisplay('totalTo', {
label: 'to',
computedValue: () => priceTo(),
decimals: 0,
variant: 'large'
}, '1fr');
});
 
finalSection.addRow(row => {
row.addTextPanel('disclaimer', {
computedValue: () => 'Estimated cost, installed. The firm quote follows the free site visit.',
customStyles: { 'font-size': '0.85rem', 'color': '#94a3b8', 'font-style': 'italic' }
});
});
 
const whenWeCall = form.computedValue(() => {
const when = contactSection.radioButton('callTime')?.value();
return when === 'morning' ? 'in the morning' : when === 'afternoon' ? 'in the afternoon' : 'shortly';
});
 
// ---- PDF estimate ----------------------------------------------------------------
form.configurePdf('estimate', pdf => {
const yes = (value: boolean) => value ? 'yes' : 'no';
const timingNames: Record<string, string> = { asap: 'as soon as possible', quarter: 'within 3 months', year: 'this year', comparing: 'still comparing quotes' };
const typeName = gateType() === 'single' ? 'Single swing' : gateType() === 'double' ? 'Double swing' : 'Slide gate';
 
pdf.configure({
filename: 'gate-estimate',
pageSize: 'Letter',
allowUserDownload: true,
downloadButtonLabel: 'Download the estimate (PDF)',
header: {
title: gatePriced() ? 'Driveway gate estimate' : 'Gate opener estimate',
subtitle: typeName + ' - ' + (styleNames[style()] || '').toLowerCase() + ' - ' + feet(width()) + (automated() ? ' - ' + (openerNames[opener()] || '').toLowerCase() : '')
},
footer: {
text: 'Estimated cost. The firm quote follows the free site visit.',
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('Property address', contactSection.textbox('address')?.value() || ''));
});
 
pdf.addSection(gatePriced() ? 'The gate' : 'Your existing gate', section => {
section.addRow(row => row
.addField('Style', styleNames[style()] || '')
.addField('Opens', typeName)
.addField('Color', colorNames[color()] || '')
.addField('Size', feet(width()) + ' opening, ' + feet(height()) + ' tall'));
section.addRow(row => row
.addField('Top', arched() ? 'arched' : 'flat')
.addField('Finials', yes(finials()))
.addField('Walk gate', gatePriced() ? yes(walkGate()) : 'n/a')
.addField('Matching fence', gatePriced() && fenceFeet() > 0 ? feet(fenceFeet()) : 'none'));
if (gatePriced()) {
section.addRow(row => row
.addField('Columns', ownColumns() ? 'the customer\'s own' : (columnNames[columns()] || '') + (columnCount() > 0 ? ' x ' + columnCount() : ''))
.addField('Lanterns', yes(lanterns())));
}
if (sliding()) {
section.addRow(row => row.addField('Run-off for the slide gate', 'about ' + feet(Math.round(width() * SLIDE_RUNOFF)) + ' of clear fence line beside the opening'));
}
});
 
pdf.addSection('Opener and access', section => {
section.addRow(row => row
.addField('Opener', describeOpener())
.addField('Power', automated() ? (solar() ? 'solar kit' : power() === 'yes' ? 'within reach' : power() === 'no' ? 'power run needed' : 'to check on the site visit') : 'n/a')
.addField('Remotes', automated() ? String(2 + remotes()) : 'n/a'));
section.addRow(row => row
.addField('Access control', automated() ? describeAccess() : 'n/a')
.addField('Safety', automated() ? 'photo eyes and warning light, UL 325' : 'n/a'));
});
 
pdf.addSection('Site and schedule', section => {
const timing = siteSection.dropdown('timing')?.value() || 'quarter';
section.addRow(row => row
.addField('Project', projectType() === 'new' ? 'new gate at an open driveway' : projectType() === 'replace' ? 'replacing an old gate' : 'opener and access for an existing gate')
.addField('Property', projectSection.radioButton('propertyKind')?.value() === 'acreage' ? 'acreage or ranch' : 'house in town')
.addField('Timing', timingNames[timing] || ''));
section.addRow(row => row
.addField('Site', describeSite())
.addField('HOA', inHoa() ? (siteSection.textarea('hoaNotes')?.value() || 'rules apply') : 'no HOA'));
});
 
pdf.addSection('Estimated cost', section => {
const rows: string[][] = [];
if (gatePriced()) rows.push(['Gate: ' + describeGate(), money(gateCost())]);
if (walkGateCost() > 0) rows.push(['Walk gate, 4 ft', money(walkGateCost())]);
if (fenceCost() > 0) rows.push(['Matching fence, ' + feet(fenceFeet()), money(fenceCost())]);
if (columnsCost() > 0) rows.push([(columnNames[columns()] || '') + ' x ' + columnCount(), money(columnsCost())]);
if (lanternsCost() > 0) rows.push(['Lanterns x 2', money(lanternsCost())]);
if (automated()) rows.push(['Opener: ' + describeOpener(), money(openerCost())]);
if (accessCost() > 0) rows.push(['Access control: ' + describeAccess(), money(accessCost())]);
rows.push(['Installation and site work: ' + describeSite(), money(siteCost())]);
if (promo() > 0) rows.push([promoText + ' (-' + promoPercent + '%)', '-' + money(promo())]);
section.addTable(['Item', 'Amount'], rows);
section.addSpacer(6);
section.addText('Estimated: ' + money(priceFrom()) + ' - ' + money(priceTo()) + ' installed, range ±' + rangePercent + '%; firm quote after the site visit.');
});
 
pdf.addSection('What happens next', section => {
section.addText('We call you ' + whenWeCall() + ' to set up the free site visit. The estimator comes out within ' + estimateDays
+ ' business days, measures the opening and checks the power and the grade. You get a firm quote after the visit; the gate is hung and the opener programmed about '
+ leadTimeWeeks + ' weeks after you sign.');
});
 
const notes = contactSection.textarea('notes')?.value();
if (notes) {
pdf.addSection('Notes', section => section.addText(notes));
}
 
pdf.addSection('Terms', section => {
section.addText('Prices include materials, labor and standard footings; sales tax not included. The cost is an estimate, not an offer to contract; the firm price is set after the free site visit. Every automated gate is installed to UL 325 with photo eyes and a warning light. Permit and HOA approval, where required, are the owner\'s responsibility unless agreed otherwise.');
});
});
 
form.configureSubmitButton({
label: 'Request My Quote'
});
 
form.configureCompletionScreen({
type: 'text',
title: 'Your request is with our estimator',
message: () => 'We will call ' + whenWeCall() + ' to set up the free site visit within ' + estimateDays + ' business days.'
});
}
 

Frequently Asked Questions

Can I change the gate styles and prices to match what I fabricate?

Yes. Every style has a per-foot rate at the top of the code, with percentages for double swing, slide gates, tall gates, arched tops and custom colors. Remove the styles you do not build and set the rates to your own price list.

Why does the calculator show a price range instead of an exact price?

A gate quote depends on what the site visit turns up: the grade, the power run, the condition of existing columns. The range width is a single constant, so you can widen it, narrow it or show one figure.

Can customers who already have a gate price just the opener?

Yes. The first step offers an opener-only project: the gate is described so the right opener is picked, but only the opener, the access control and the site work are priced.

Does the calculator handle slide gates and uphill driveways?

It does. A slide gate adds the cantilever surcharge, the run-off length and the footing question, and a swing gate on a driveway that slopes up into the property gets a warning so the estimator knows to check the grade.

What happens after a customer sends the request?

The submission reaches you with every choice, the contact details and the best time to call, and the customer can download a PDF estimate. You call back, schedule the site visit and send the firm quote.