Interior Door Calculator

Give your door and trim company an interactive quote calculator that prices a whole house of doors the way your consultants do. Homeowners pick one style for the house (2-panel shaker, 3-panel, 5-panel, 6-panel colonial, craftsman, flush, French glass or louvered), the construction (hollow-core, solid-core, wood veneer or solid wood), primed or a paint or stain finish, prehung with a jamb sized to the wall and a casing style or slabs only on the existing jambs, and a knob or lever in five finishes. Then they count the doors: hinged doors with privacy or keyed locks, pocket and barn doors, bifold and bypass closets and double doors by opening width. Installation and haul-away, delivery only, a whole-house package above a number of doors and a minimum job size are built in, and the result is an estimate range in US dollars before sales tax. Capture the homeowner's contact details and book the in-home measure.

Home Services

Try the Calculator

Interior Door Estimate
🏠 Your Home
 
🎨 Style for the Whole House
 
 
 
🚪 Your Doors
 
 
 
 
 
 
 
 
 
Whole-house package: 8% off every door, jamb, casing and hardware set on this order (8 openings).
🔧 Installation
 

💰 Your Estimate
$ 1,650
+
$ 1,414
+
$ 1,300
$ -245

$ 4,119
installed, before sales tax
Expect $3,700 - $4,550 after the in-home measure (range of 10%). Financing available: 0% APR for 12 months on approved credit.
What happens next: a consultant calls to go over your doors and books a free in-home measure within 5 business days. After the measure you get a firm quote, and we hang the doors about 4 weeks from your order.
🧾 Summary
$ 4,119
Estimate before sales tax. A firm quote follows the free in-home measure.
📋 Request Your Estimate
 
 
 
 
 
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
export function interiorDoorCalculator(form: FormTs) {
// Slab prices: one primed hollow-core 30 x 80 in slab in this style, without jamb, hardware or installation
const stylePrices: Record<string, number> = {
'shaker-2': 165,
'shaker-3': 185,
'panel-5': 195,
'colonial-6': 120,
'craftsman-3': 190,
'flush': 95,
'glass-frosted': 420,
'glass-10-lite': 460,
'louvered': 210
};
const styleNames: Record<string, string> = {
'shaker-2': '2-panel shaker', 'shaker-3': '3-panel shaker', 'panel-5': '5-panel', 'colonial-6': '6-panel colonial',
'craftsman-3': '3-panel craftsman', 'flush': 'Flush', 'glass-frosted': 'French, frosted glass', 'glass-10-lite': 'French, 10-lite clear glass', 'louvered': 'Louvered'
};
// A glass door is solid by construction, so the solid-core surcharge does not apply to it
const solidByConstruction = ['glass-frosted', 'glass-10-lite'];
 
// Construction surcharge on the slab price (%) and whether it takes stain
const constructions: Record<string, { name: string; surcharge: number; stainable: boolean }> = {
'hollow': { name: 'Hollow-core', surcharge: 0, stainable: false },
'solid-core': { name: 'Solid-core', surcharge: 65, stainable: false },
'veneer': { name: 'Wood veneer', surcharge: 100, stainable: true },
'solid-wood': { name: 'Solid wood', surcharge: 130, stainable: true }
};
const tallSurcharge = 40; // % for 96 in doors instead of 80 in
 
// Finish per slab: primed is included
const finishes: Record<string, { name: string; price: number; stain: boolean }> = {
'primed': { name: 'Primed, you paint', price: 0, stain: false },
'white': { name: 'White paint', price: 95, stain: false },
'alabaster': { name: 'Alabaster paint', price: 95, stain: false },
'gray': { name: 'Gray paint', price: 95, stain: false },
'black': { name: 'Black paint', price: 95, stain: false },
'natural': { name: 'Natural stain', price: 140, stain: true },
'honey': { name: 'Honey stain', price: 140, stain: true },
'walnut': { name: 'Walnut stain', price: 140, stain: true }
};
 
// Jambs, casing and slab prep (per door)
const jambPrices: Record<string, number> = { 'std': 85, 'wide': 115, 'not-sure': 85 }; // 4-9/16 in for 2x4 walls, 6-9/16 in for 2x6 walls
const slabPrepPrice = 35; // bore and mortise a slab to the existing jamb
const casingPrices: Record<string, number> = { 'colonial': 45, 'craftsman': 70, 'modern': 55 };
const casingNames: Record<string, string> = { 'colonial': 'Colonial 2-1/4 in', 'craftsman': 'Craftsman 3-1/2 in', 'modern': 'Modern flat 2-1/2 in' };
 
// Hardware (per door)
const shapePrices: Record<string, number> = { 'knob': 28, 'lever': 42 };
const hardwareFinishes: Record<string, { name: string; premium: boolean }> = {
'nickel': { name: 'Satin nickel', premium: false },
'black': { name: 'Matte black', premium: false },
'chrome': { name: 'Polished chrome', premium: false },
'bronze': { name: 'Oil-rubbed bronze', premium: true },
'brass': { name: 'Polished brass', premium: true }
};
const premiumFinishPrice = 15;
const privacyPrice = 8; // push-button lock for bedrooms and bathrooms
const keyedPrice = 35; // keyed entry set for an office or storage room
 
// Pocket, barn and closet door kits
const kitPrices: Record<string, number> = { 'pocket': 180, 'barn': 260, 'bifold': 45, 'bypass': 65, 'double': 140 };
 
// Installation
const installPrices: Record<string, number> = {
'prehung': 140, 'slab': 75, 'pocket': 320, 'barn': 160, 'closet': 110, 'removal': 30, 'delivery': 95
};
const minimumJob = 600;
const packageMinDoors = 8; // whole-house package from this many openings
const packagePercent = 8; // discount on doors, jambs, casing and hardware (not installation)
const promotion = { percent: 0, name: 'Whole-house door sale' }; // 0 hides the banner
const rangePercent = 10; // the estimate is shown as a range of +/- this
const measureDays = 5; // free in-home measure within N business days
const leadWeeks = 4; // order to installation
 
const SLAB_WIDTH = 30; // an opening is priced in slab equivalents of this width
 
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 left = whole.length - i - 1;
if (left > 0 && left % 3 === 0) out += ',';
}
return (amount < 0 ? '-$' : '$') + out;
};
const plural = (count: number, one: string, many: string) => count + ' ' + (count === 1 ? one : many);
 
form.addRow(row => {
row.addTextPanel('header', {
computedValue: () => 'Interior Door Estimate',
customStyles: { 'font-size': '1.5rem', 'font-weight': '600', 'color': '#1e293b' }
});
});
 
form.addSpacer({ height: 20 });
 
// Home Section
const homeSection = form.addSubform('home', { title: '🏠 Your Home' });
 
homeSection.addRow(row => {
row.addDropdown('property', {
label: 'Where are the doors going?',
options: [
{ id: 'house', name: 'Single-family home - replacing the doors I have' },
{ id: 'condo', name: 'Condo or townhome - replacing the doors I have' },
{ id: 'new-build', name: 'New construction - openings framed, no doors yet' }
],
defaultValue: 'house',
isRequired: true
}, '1fr');
row.addRadioButton('job', {
label: 'New frames or just the doors?',
options: [
{ id: 'prehung', name: 'Prehung: doors with new jambs and casing (the usual choice)' },
{ id: 'slab', name: 'Slabs only: new doors hung on the jambs I have' }
],
defaultValue: 'prehung',
orientation: 'vertical',
isRequired: true,
isVisible: () => replacement()
}, '1fr');
});
 
const property = form.computedValue(() => homeSection.dropdown('property')?.value() || 'house');
const replacement = form.computedValue(() => property() !== 'new-build');
// Slabs only is a replacement job; a new build always gets jambs and casing
const slabsOnly = form.computedValue(() => replacement() && homeSection.radioButton('job')?.value() === 'slab');
 
homeSection.addRow(row => {
row.addDropdown('wall', {
label: 'How thick are the walls?',
tooltip: 'The jamb width follows the wall: 4-9/16 in for 2x4 walls, 6-9/16 in for 2x6 walls. Not sure? We measure',
options: [
{ id: 'std', name: '2x4 framing (standard, $' + jambPrices['std'] + ' jamb)' },
{ id: 'wide', name: '2x6 framing ($' + jambPrices['wide'] + ' jamb)' },
{ id: 'not-sure', name: 'Not sure' }
],
defaultValue: 'not-sure',
isVisible: () => !slabsOnly()
}, '1fr');
row.addDropdown('oldDoors', {
label: 'What do you have now?',
options: [
{ id: 'hollow-flush', name: 'Hollow flush doors' },
{ id: 'colonial', name: 'Molded 6-panel doors' },
{ id: 'wood', name: 'Solid wood doors' },
{ id: 'mixed', name: 'A mix' },
{ id: 'not-sure', name: 'Not sure' }
],
defaultValue: 'hollow-flush',
isVisible: () => replacement()
}, '1fr');
});
 
const wall = form.computedValue(() => slabsOnly() ? 'std' : (homeSection.dropdown('wall')?.value() || 'not-sure'));
const jambPrice = () => jambPrices[wall()] || 85;
 
// Style Section
const styleSection = form.addSubform('style', { title: '🎨 Style for the Whole House' });
 
styleSection.addRow(row => {
row.addRadioButton('style', {
label: 'Door style (the same style goes on every door)',
options: [
{ id: 'shaker-2', name: '2-panel shaker - two flat panels, square edges (from $165)' },
{ id: 'shaker-3', name: '3-panel shaker - three equal flat panels (from $185)' },
{ id: 'panel-5', name: '5-panel - five horizontal flat panels (from $195)' },
{ id: 'colonial-6', name: '6-panel colonial - classic raised panels (from $120)' },
{ id: 'craftsman-3', name: '3-panel craftsman - two square panels over one tall (from $190)' },
{ id: 'flush', name: 'Flush - smooth slab, no panels (from $95)' },
{ id: 'glass-frosted', name: 'French, frosted glass - one full frosted lite (from $420)' },
{ id: 'glass-10-lite', name: 'French, 10-lite clear glass (from $460)' },
{ id: 'louvered', name: 'Louvered - for closets and laundry (from $210)' }
],
defaultValue: 'shaker-2',
orientation: 'vertical',
isRequired: true
}, '1fr');
row.addRadioButton('construction', {
label: 'Construction',
tooltip: 'Solid-core doors are heavier, quieter and feel like wood. A glass door is solid by construction',
options: [
{ id: 'hollow', name: 'Hollow-core - light, primed MDF skin (included)' },
{ id: 'solid-core', name: 'Solid-core - heavy, quiet, primed (+65%)' },
{ id: 'veneer', name: 'Wood veneer - birch or oak, stain-grade (+100%)' },
{ id: 'solid-wood', name: 'Solid wood - pine, poplar or knotty alder (+130%)' }
],
defaultValue: 'hollow',
orientation: 'vertical',
isRequired: true
}, '1fr');
});
 
const style = form.computedValue(() => styleSection.radioButton('style')?.value() || 'shaker-2');
const construction = form.computedValue(() => styleSection.radioButton('construction')?.value() || 'hollow');
const constructionInfo = () => constructions[construction()] || { name: 'Hollow-core', surcharge: 0, stainable: false };
const stainable = form.computedValue(() => constructionInfo().stainable);
 
styleSection.addRow(row => {
row.addDropdown('finish', {
label: 'Finish',
tooltip: 'Stain shows the grain of a wood veneer or solid wood door; primed is included',
options: () => Object.keys(finishes)
.filter(id => !(finishes[id]?.stain) || stainable())
.map(id => {
const f = finishes[id] || { name: id, price: 0, stain: false };
return { id, name: f.name + (f.price > 0 ? ' (+$' + f.price + '/door)' : ' (included)') };
}),
defaultValue: 'primed',
isRequired: true
}, '1fr');
row.addDropdown('casing', {
label: 'Casing on both sides of every new jamb',
options: [
{ id: 'colonial', name: 'Colonial 2-1/4 in, curved profile (+$' + casingPrices['colonial'] + '/door)' },
{ id: 'craftsman', name: 'Craftsman 3-1/2 in flat with a head (+$' + casingPrices['craftsman'] + '/door)' },
{ id: 'modern', name: 'Modern flat 2-1/2 in square edge (+$' + casingPrices['modern'] + '/door)' }
],
defaultValue: 'colonial',
isRequired: true,
isVisible: () => !slabsOnly()
}, '1fr');
});
 
// A stain picked on wood does not follow to a hollow-core door
const finish = form.computedValue(() => {
const chosen = styleSection.dropdown('finish')?.value() || 'primed';
return finishes[chosen]?.stain && !stainable() ? 'primed' : chosen;
});
const finishInfo = () => finishes[finish()] || { name: 'Primed', price: 0, stain: false };
const casing = form.computedValue(() => styleSection.dropdown('casing')?.value() || 'colonial');
const casingPrice = () => casingPrices[casing()] || 45;
 
styleSection.addRow(row => {
row.addTextPanel('slabNote', {
computedValue: () => 'Your casing stays: we bore and mortise every slab to fit your existing jamb ($' + slabPrepPrice + ' per door), so the trim and paint around the openings are untouched.',
customStyles: { 'font-size': '0.9rem', 'color': '#0369a1', 'background': '#e0f2fe', 'padding': '10px', 'border-radius': '6px' },
isVisible: () => slabsOnly()
});
});
 
styleSection.addRow(row => {
row.addRadioButton('shape', {
label: 'Knob or lever',
tooltip: 'Hinges come in the same finish. Closet doors get matching knobs or pulls with their hardware kit',
options: [
{ id: 'knob', name: 'Knob ($' + shapePrices['knob'] + '/door)' },
{ id: 'lever', name: 'Lever ($' + shapePrices['lever'] + '/door)' }
],
defaultValue: 'lever',
isRequired: true
}, '1fr');
row.addDropdown('hardwareFinish', {
label: 'Hardware finish',
options: Object.keys(hardwareFinishes).map(id => {
const f = hardwareFinishes[id] || { name: id, premium: false };
return { id, name: f.name + (f.premium ? ' (+$' + premiumFinishPrice + '/door)' : ' (included)') };
}),
defaultValue: 'nickel',
isRequired: true
}, '1fr');
});
 
const shape = form.computedValue(() => styleSection.radioButton('shape')?.value() || 'lever');
const hardwareFinish = form.computedValue(() => styleSection.dropdown('hardwareFinish')?.value() || 'nickel');
const hardwareSetPrice = () => (shapePrices[shape()] || 0) + (hardwareFinishes[hardwareFinish()]?.premium ? premiumFinishPrice : 0);
 
// Doors Section
const doorsSection = form.addSubform('doors', { title: '🚪 Your Doors' });
 
doorsSection.addRow(row => {
row.addRadioButton('height', {
label: 'Door height',
tooltip: 'Standard doors are 80 in tall; homes with 9 or 10 ft ceilings often take 96 in',
options: [
{ id: 'standard', name: '80 in (standard)' },
{ id: 'tall', name: '96 in, 8 ft (+' + tallSurcharge + '% on every slab)' }
],
defaultValue: 'standard',
orientation: 'horizontal'
}, '1fr');
row.addDropdown('closetWidth', {
label: 'Closet and double door opening width',
tooltip: 'The finished opening, side to side. We confirm at the measure',
options: [
{ id: '36', name: '36 in' },
{ id: '48', name: '48 in' },
{ id: '60', name: '60 in' },
{ id: '72', name: '72 in' }
],
defaultValue: '60'
}, '1fr');
});
 
doorsSection.addRow(row => {
row.addInteger('hingedDoors', {
label: 'Hinged doors (bedrooms, bathrooms, offices, hallways)',
min: 0,
max: 30,
defaultValue: 6,
isRequired: true
}, '1fr');
row.addInteger('privacyDoors', {
label: 'Of which with a privacy lock (+$' + privacyPrice + ' each)',
tooltip: 'Bedrooms and bathrooms: a push-button lock inside with an emergency release outside',
min: 0,
max: 30,
defaultValue: 4
}, '1fr');
row.addInteger('keyedDoors', {
label: 'Of which with a keyed lock (+$' + keyedPrice + ' each)',
tooltip: 'An office or a storage room',
min: 0,
max: 30,
defaultValue: 0
}, '1fr');
});
 
doorsSection.addRow(row => {
row.addInteger('pocketDoors', {
label: 'Pocket doors - slide into the wall (+$' + kitPrices['pocket'] + ' frame kit)',
min: 0,
max: 10,
defaultValue: 0
}, '1fr');
row.addInteger('barnDoors', {
label: 'Barn doors - slide on a track outside (+$' + kitPrices['barn'] + ' hardware kit)',
min: 0,
max: 10,
defaultValue: 0
}, '1fr');
});
 
doorsSection.addRow(row => {
row.addInteger('bifoldDoors', {
label: 'Bifold closet openings (+$' + kitPrices['bifold'] + ' hardware)',
min: 0,
max: 10,
defaultValue: 2
}, '1fr');
row.addInteger('bypassDoors', {
label: 'Bypass closet openings (+$' + kitPrices['bypass'] + ' hardware)',
min: 0,
max: 10,
defaultValue: 0
}, '1fr');
row.addInteger('doubleDoors', {
label: 'Double doors - a pair that swings (+$' + kitPrices['double'] + ' per pair)',
min: 0,
max: 10,
defaultValue: 0
}, '1fr');
});
 
const tall = form.computedValue(() => doorsSection.radioButton('height')?.value() === 'tall');
const closetWidth = form.computedValue(() => Number(doorsSection.dropdown('closetWidth')?.value() || '60') || 60);
const count = (id: string) => doorsSection.integer(id)?.value() || 0;
const hingedDoors = form.computedValue(() => count('hingedDoors'));
const privacyDoors = form.computedValue(() => Math.min(count('privacyDoors'), hingedDoors()));
const keyedDoors = form.computedValue(() => Math.min(count('keyedDoors'), hingedDoors() - privacyDoors()));
const pocketDoors = form.computedValue(() => count('pocketDoors'));
const barnDoors = form.computedValue(() => count('barnDoors'));
const bifoldDoors = form.computedValue(() => count('bifoldDoors'));
const bypassDoors = form.computedValue(() => count('bypassDoors'));
const doubleDoors = form.computedValue(() => count('doubleDoors'));
const doorCount = form.computedValue(() => hingedDoors() + pocketDoors() + barnDoors() + bifoldDoors() + bypassDoors() + doubleDoors());
const packageOn = form.computedValue(() => doorCount() >= packageMinDoors && packagePercent > 0);
 
doorsSection.addRow(row => {
row.addTextPanel('packageInfo', {
computedValue: () => packageOn()
? 'Whole-house package: ' + packagePercent + '% off every door, jamb, casing and hardware set on this order (' + plural(doorCount(), 'opening', 'openings') + ').'
: 'Whole-house package from ' + packageMinDoors + ' doors: add ' + plural(packageMinDoors - doorCount(), 'more door', 'more doors')
+ ' and ' + packagePercent + '% comes off every door, jamb, casing and hardware set.',
customStyles: () => packageOn()
? { 'font-size': '0.9rem', 'color': '#166534', 'background': '#dcfce7', 'padding': '10px', 'border-radius': '6px' }
: { 'font-size': '0.9rem', 'color': '#0369a1', 'background': '#e0f2fe', 'padding': '10px', 'border-radius': '6px' },
isVisible: () => packagePercent > 0
});
});
 
// Installation Section
const installSection = form.addSubform('installation', { title: '🔧 Installation' });
 
installSection.addRow(row => {
row.addRadioButton('work', {
label: 'Who installs?',
options: [
{ id: 'install', name: 'Measure and install' },
{ id: 'supply', name: 'Delivery only (+$' + installPrices['delivery'] + ')' }
],
defaultValue: 'install',
isRequired: true
}, '1fr');
row.addDropdown('timing', {
label: 'When would you like this done?',
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');
});
 
const withInstall = form.computedValue(() => installSection.radioButton('work')?.value() !== 'supply');
 
installSection.addRow(row => {
row.addCheckbox('removal', {
label: () => 'Remove and haul away the old doors' + (slabsOnly() ? '' : ', jambs and casing') + ' (+$' + installPrices['removal'] + '/door)',
defaultValue: true,
isVisible: () => withInstall() && replacement()
});
});
 
const removal = form.computedValue(() => withInstall() && replacement() && installSection.checkbox('removal')?.value() === true);
 
// Pricing helpers
// The slab: the style price with the construction and height surcharges, times the slab equivalents of the opening
const slabEach = (slabs: number): number => {
const constructionPct = solidByConstruction.includes(style()) && construction() === 'solid-core' ? 0 : constructionInfo().surcharge;
const surcharge = constructionPct + (tall() ? tallSurcharge : 0);
return (stylePrices[style()] || 165) * (1 + surcharge / 100) * slabs + finishInfo().price * slabs;
};
const openingSlabs = () => Math.round(closetWidth() / SLAB_WIDTH * 10) / 10;
const newFrame = () => jambPrice() + casingPrice();
 
// Jamb and casing of a new frame, or the prep of the slabs to the existing one, plus kits and hardware sets
const frameEach = (type: string): number => {
if (type === 'pocket') return slabsOnly() ? slabPrepPrice : (kitPrices['pocket'] || 0) + casingPrice();
if (type === 'barn') return kitPrices['barn'] || 0;
if (type === 'bifold') return (kitPrices['bifold'] || 0) + (slabsOnly() ? 0 : newFrame());
if (type === 'bypass') return (kitPrices['bypass'] || 0) + (slabsOnly() ? 0 : newFrame());
if (type === 'double') return (kitPrices['double'] || 0) + (slabsOnly() ? 2 * slabPrepPrice : newFrame());
return slabsOnly() ? slabPrepPrice : newFrame();
};
const installEach = (type: string): number => {
if (!withInstall()) return 0;
const haul = removal() ? (installPrices['removal'] || 0) * (type === 'double' ? 2 : 1) : 0;
const hang = slabsOnly() ? (installPrices['slab'] || 0) : (installPrices['prehung'] || 0);
if (type === 'pocket') return (slabsOnly() ? (installPrices['slab'] || 0) : (installPrices['pocket'] || 0)) + haul;
if (type === 'barn') return (installPrices['barn'] || 0) + haul;
if (type === 'bifold' || type === 'bypass') return (installPrices['closet'] || 0) + haul;
if (type === 'double') return 2 * hang + haul;
return hang + haul;
};
 
const slabsTotal = form.computedValue(() =>
(hingedDoors() + pocketDoors() + barnDoors()) * slabEach(1)
+ (bifoldDoors() + bypassDoors() + doubleDoors()) * slabEach(openingSlabs()));
// Hinged and double doors take a knob or lever set; pocket and barn doors bring a pull in their kit; closet doors come with their kit
const framesTotal = form.computedValue(() =>
hingedDoors() * (frameEach('hinged') + hardwareSetPrice())
+ privacyDoors() * privacyPrice + keyedDoors() * keyedPrice
+ pocketDoors() * frameEach('pocket')
+ barnDoors() * frameEach('barn')
+ bifoldDoors() * frameEach('bifold')
+ bypassDoors() * frameEach('bypass')
+ doubleDoors() * (frameEach('double') + hardwareSetPrice()));
const goodsTotal = form.computedValue(() => slabsTotal() + framesTotal());
const installTotal = form.computedValue(() =>
hingedDoors() * installEach('hinged')
+ pocketDoors() * installEach('pocket')
+ barnDoors() * installEach('barn')
+ bifoldDoors() * installEach('bifold')
+ bypassDoors() * installEach('bypass')
+ doubleDoors() * installEach('double'));
const packageDiscount = form.computedValue(() => packageOn() ? goodsTotal() * packagePercent / 100 : 0);
const deliveryCost = form.computedValue(() => withInstall() ? 0 : (installPrices['delivery'] || 0));
const beforePromo = form.computedValue(() => goodsTotal() - packageDiscount() + installTotal() + deliveryCost());
const promoDiscount = form.computedValue(() => beforePromo() * promotion.percent / 100);
const afterPromo = form.computedValue(() => beforePromo() - promoDiscount());
// The minimum job applies last, after every discount
const minimumApplied = form.computedValue(() => doorCount() > 0 && afterPromo() < minimumJob);
const total = form.computedValue(() => doorCount() > 0 ? Math.round(Math.max(afterPromo(), minimumJob)) : 0);
const roundTo50 = (amount: number) => Math.round(amount / 50) * 50;
const totalLow = form.computedValue(() => roundTo50(total() * (1 - rangePercent / 100)));
const totalHigh = form.computedValue(() => roundTo50(total() * (1 + rangePercent / 100)));
 
form.addSpacer({ height: 20, showLine: true, lineStyle: 'dashed' });
 
// Estimate Section
const estimateSection = form.addSubform('estimate', { title: '💰 Your Estimate', isCollapsible: false });
 
estimateSection.addRow(row => {
row.addTextPanel('promo', {
computedValue: () => promotion.name + ': ' + promotion.percent + '% off is already included in the estimate below.',
customStyles: { 'font-size': '0.9rem', 'color': '#166534', 'background': '#dcfce7', 'padding': '10px', 'border-radius': '6px' },
isVisible: () => promotion.percent > 0
});
});
 
estimateSection.addRow(row => {
row.addPriceDisplay('slabs', {
label: () => plural(doorCount(), 'door', 'doors') + ' (' + (styleNames[style()] || '') + ', ' + constructionInfo().name.toLowerCase() + ', ' + finishInfo().name.toLowerCase() + ')',
computedValue: () => Math.round(slabsTotal()),
decimals: 0,
variant: 'default'
}, '1fr');
row.addPriceDisplay('frames', {
label: () => slabsOnly() ? 'Slab prep, hardware kits and lock sets' : 'Jambs, casing, hardware kits and lock sets',
computedValue: () => Math.round(framesTotal()),
decimals: 0,
variant: 'default',
prefix: '+'
}, '1fr');
});
 
estimateSection.addRow(row => {
row.addPriceDisplay('installation', {
label: () => 'Installation' + (removal() ? ' with removal and haul-away' : ''),
computedValue: () => Math.round(installTotal()),
decimals: 0,
variant: 'default',
prefix: '+',
isVisible: () => withInstall()
}, '1fr');
row.addPriceDisplay('delivery', {
label: 'Delivery (no installation)',
computedValue: () => deliveryCost(),
decimals: 0,
variant: 'default',
prefix: '+',
isVisible: () => deliveryCost() > 0
}, '1fr');
});
 
estimateSection.addRow(row => {
row.addPriceDisplay('packageDiscount', {
label: 'Whole-house package (-' + packagePercent + '%)',
computedValue: () => -Math.round(packageDiscount()),
decimals: 0,
variant: 'success',
isVisible: () => packageDiscount() > 0
}, '1fr');
row.addPriceDisplay('promoDiscount', {
label: () => promotion.name + ' (-' + promotion.percent + '%)',
computedValue: () => -Math.round(promoDiscount()),
decimals: 0,
variant: 'success',
isVisible: () => promoDiscount() > 0
}, '1fr');
row.addPriceDisplay('minimum', {
label: 'Minimum job (small jobs are billed at $' + minimumJob + ')',
computedValue: () => Math.round(total() - afterPromo()),
decimals: 0,
variant: 'default',
prefix: '+',
isVisible: () => minimumApplied()
}, '1fr');
});
 
estimateSection.addSpacer({ showLine: true, lineStyle: 'solid', lineColor: '#e2e8f0' });
 
estimateSection.addRow(row => {
row.addPriceDisplay('total', {
label: 'Estimated cost',
computedValue: () => total(),
decimals: 0,
variant: 'large',
suffix: () => (withInstall() ? 'installed' : 'delivered') + ', before sales tax'
});
});
 
estimateSection.addRow(row => {
row.addTextPanel('range', {
computedValue: () => 'Expect ' + money(totalLow()) + ' - ' + money(totalHigh()) + ' after the in-home measure (range of ' + rangePercent + '%). '
+ 'Financing available: 0% APR for 12 months on approved credit.',
customStyles: { 'font-size': '0.9rem', 'color': '#059669', 'font-weight': '500' }
});
});
 
estimateSection.addRow(row => {
row.addTextPanel('nextSteps', {
computedValue: () => 'What happens next: a consultant calls to go over your doors and books a free in-home measure within ' + measureDays
+ ' business days. After the measure you get a firm quote, and we hang the doors about ' + leadWeeks + ' weeks from your order.',
customStyles: { 'font-size': '0.9rem', 'color': '#0369a1', 'background': '#e0f2fe', 'padding': '10px', 'border-radius': '6px' }
});
});
 
// Sticky Summary Section
const finalSection = form.addSubform('final', {
title: '🧾 Summary',
isCollapsible: false,
sticky: 'bottom'
});
 
finalSection.addRow(row => {
row.addPriceDisplay('total', {
label: () => plural(doorCount(), 'door', 'doors') + ' - estimated cost (' + money(totalLow()) + ' - ' + money(totalHigh()) + ')',
computedValue: () => total(),
decimals: 0,
variant: 'large'
});
});
 
finalSection.addRow(row => {
row.addTextPanel('disclaimer', {
computedValue: () => 'Estimate before sales tax. A firm quote follows the free in-home measure.',
customStyles: { 'font-size': '0.85rem', 'color': '#94a3b8', 'font-style': 'italic' }
});
});
 
// Contact Section
const contactSection = form.addSubform('contact', { title: '📋 Request Your Estimate' });
 
contactSection.addRow(row => {
row.addTextbox('name', {
label: 'Your Name',
placeholder: 'John Smith',
isRequired: true
}, '1fr');
row.addTextbox('phone', {
label: 'Phone Number',
placeholder: '(614) 555-0100',
isRequired: true
}, '1fr');
});
 
contactSection.addRow(row => {
row.addEmail('email', {
label: 'Email Address',
placeholder: 'john@example.com',
isRequired: true
}, '1fr');
row.addTextbox('cityZip', {
label: 'City and ZIP Code',
placeholder: 'Columbus, 43207',
tooltip: 'Where we come out to measure',
isRequired: true
}, '1fr');
});
 
contactSection.addRow(row => {
row.addRadioButton('callTime', {
label: 'Best time to call?',
options: [
{ id: 'any', name: 'Any time' },
{ id: 'morning', name: 'Before noon' },
{ id: 'afternoon', name: 'After noon' }
],
defaultValue: 'any',
orientation: 'horizontal'
});
});
 
contactSection.addRow(row => {
row.addTextarea('notes', {
label: 'Anything else?',
placeholder: 'E.g. a door that should differ from the rest, matching baseboards, a door to the garage, days that work for the measure',
rows: 3,
isRequired: false
});
});
 
// Descriptions shared by the PDF
const describeStyle = (): string => (styleNames[style()] || '') + ', ' + constructionInfo().name.toLowerCase() + ', ' + finishInfo().name.toLowerCase()
+ (slabsOnly() ? ', slabs only' : ', ' + (casingNames[casing()] || '').toLowerCase() + ' casing')
+ ', ' + (hardwareFinishes[hardwareFinish()]?.name || '').toLowerCase() + ' ' + shape() + 's';
const describeInstall = (): string => {
if (!withInstall()) return 'delivery only, sizes to be written down with the consultant';
const parts = ['measure and install', slabsOnly() ? 'slabs hung on the existing jambs' : 'prehung with new casing'];
if (removal()) parts.push('old doors hauled away');
return parts.join(', ');
};
const wallNames: Record<string, string> = { 'std': '2x4 walls, 4-9/16 in jamb', 'wide': '2x6 walls, 6-9/16 in jamb', 'not-sure': 'not sure, jamb confirmed at the measure' };
const propertyNames: Record<string, string> = { 'house': 'single-family home', 'condo': 'condo or townhome', 'new-build': 'new construction or addition' };
const oldDoorNames: Record<string, string> = { 'hollow-flush': 'hollow-core flush doors', 'colonial': 'molded 6-panel doors', 'wood': 'solid wood doors', 'mixed': 'a mix', 'not-sure': 'not sure' };
const timingNames: Record<string, string> = { 'asap': 'as soon as possible', 'quarter': 'within 3 months', 'year': 'this year', 'comparing': 'still comparing quotes' };
const heightText = () => tall() ? '96 in' : '80 in';
 
form.configurePdf('estimate', pdf => {
pdf.configure({
filename: 'interior-door-estimate',
pageSize: 'Letter',
allowUserDownload: true,
downloadButtonLabel: 'Download the PDF estimate',
header: {
title: 'Interior door estimate',
subtitle: plural(doorCount(), 'door', 'doors') + ' - ' + describeStyle()
},
footer: {
text: 'Estimate only, before sales tax. A firm quote follows the free in-home measure.',
showPageNumbers: true
}
});
 
pdf.addSection('Style for the house', section => {
section.addRow(row => row
.addField('Door style', styleNames[style()] || '')
.addField('Construction', constructionInfo().name)
.addField('Finish', finishInfo().name)
.addField('Height', heightText()));
section.addRow(row => row
.addField('Frames', slabsOnly() ? 'slabs only, existing jambs' : 'prehung, ' + (casingNames[casing()] || '').toLowerCase() + ' casing')
.addField('Jamb', slabsOnly() ? '-' : (wallNames[wall()] || ''))
.addField('Hardware', (hardwareFinishes[hardwareFinish()]?.name || '') + ' ' + shape())
.addField('Home', propertyNames[property()] || ''));
});
 
pdf.addSection('Doors', section => {
const rows: string[][] = [];
if (hingedDoors() > 0) rows.push(['Hinged doors', String(hingedDoors()), 'slab, ' + heightText() + '; ' + privacyDoors() + ' privacy, ' + keyedDoors() + ' keyed, ' + (hingedDoors() - privacyDoors() - keyedDoors()) + ' passage']);
if (pocketDoors() > 0) rows.push(['Pocket doors', String(pocketDoors()), 'slab, frame kit, recessed pull']);
if (barnDoors() > 0) rows.push(['Barn doors', String(barnDoors()), 'slab, flat track in matte black, pull']);
if (bifoldDoors() > 0) rows.push(['Bifold closet openings', String(bifoldDoors()), closetWidth() + ' in opening, track, pivots and knobs']);
if (bypassDoors() > 0) rows.push(['Bypass closet openings', String(bypassDoors()), closetWidth() + ' in opening, double track and pulls']);
if (doubleDoors() > 0) rows.push(['Double doors', String(doubleDoors()), closetWidth() + ' in opening, flush bolts, ball catch and dummy set']);
section.addTable(['Type', 'Qty', 'Details'], rows);
});
 
pdf.addSection('Installation and timing', section => {
section.addRow(row => row
.addField('Scope', describeInstall())
.addField('Timing', timingNames[installSection.dropdown('timing')?.value() || 'quarter'] || ''));
if (replacement()) {
section.addRow(row => row
.addField('Current doors', oldDoorNames[homeSection.dropdown('oldDoors')?.value() || 'not-sure'] || '')
.addField('Removal and haul-away', withInstall() ? (removal() ? 'yes' : 'no') : 'delivery only'));
}
});
 
pdf.addSection('Estimated cost', section => {
const rows: string[][] = [
['Doors: ' + (styleNames[style()] || '').toLowerCase() + ', ' + constructionInfo().name.toLowerCase() + ', ' + finishInfo().name.toLowerCase(), String(doorCount()), money(slabsTotal())],
[slabsOnly() ? 'Slab prep, hardware kits and lock sets' : 'Jambs, casing, hardware kits and lock sets', String(doorCount()), money(framesTotal())]
];
if (packageDiscount() > 0) rows.push(['Whole-house package (-' + packagePercent + '%)', '', '-' + money(packageDiscount())]);
rows.push(['Installation' + (removal() ? ' with removal and haul-away' : ''), withInstall() ? String(doorCount()) : '', withInstall() ? money(installTotal()) : 'delivery only']);
if (deliveryCost() > 0) rows.push(['Delivery', '1', money(deliveryCost())]);
if (promoDiscount() > 0) rows.push([promotion.name + ' (-' + promotion.percent + '%)', '', '-' + money(promoDiscount())]);
if (minimumApplied()) rows.push(['Minimum job', '', money(total() - afterPromo())]);
rows.push(['Estimated total, before sales tax', '', money(total())]);
section.addTable(['Item', 'Qty', 'Amount'], rows);
section.addText('Expected range after the measure: ' + money(totalLow()) + ' to ' + money(totalHigh()) + ' (' + rangePercent + '%).');
});
 
pdf.addSection('What happens next', section => {
section.addText('We call you to go over your doors and book your free in-home measure within ' + measureDays + ' business days. '
+ 'After the measure you get a firm quote, and we hang the doors about ' + leadWeeks + ' weeks from your order.');
const notes = contactSection.textarea('notes')?.value();
if (notes) section.addText('Notes: ' + notes);
section.addDivider();
section.addText('This is an estimate, not a binding offer. Prices are before sales tax and may change after the in-home measure, '
+ 'which confirms sizes, wall thickness, swing and the condition of the openings. A firm quote in writing follows the measure.');
});
});
 
form.configureCompletionScreen({
type: 'text',
title: 'Your request is with a consultant',
message: () => 'We will call you ' + (contactSection.radioButton('callTime')?.value() === 'morning' ? 'before noon' : contactSection.radioButton('callTime')?.value() === 'afternoon' ? 'after noon' : 'shortly')
+ ' to go over your doors and book your free in-home measure within ' + measureDays + ' business days.'
});
 
form.configureSubmitButton({
label: 'Request My Estimate'
});
}
 

Frequently Asked Questions

Can I set my own door, jamb and installation prices?

Yes. Every price is a named constant at the top of the calculator: the slab price per style, construction surcharges in percent, finishes, jambs by wall thickness, casing, knobs and levers, lock functions, pocket, barn and closet kits and the installation rates per door type. Change the numbers and the estimate follows.

How are closet and double doors priced?

A single door is priced per slab. Bifold, bypass and double door openings are priced in slab equivalents of their opening width (36 to 72 inches against a 30 inch slab), plus the hardware kit for that door type and, for prehung jobs, a jamb and casing.

What is the whole-house package?

When the homeowner counts at least the package threshold (8 openings by default), a percentage comes off every door, jamb, casing and hardware set, but not off installation. The calculator tells visitors how many more doors get them to the package, which nudges them toward a bigger order.

Does it handle slabs-only jobs on existing jambs?

Yes. The homeowner chooses prehung doors with new jambs and casing or slabs only on the jambs they have. Slabs only swaps the jamb and casing for a slab prep charge and uses the lighter installation rate, and the casing choice disappears.

Can I collect the homeowner's details and send a PDF?

Yes. The calculator collects name, phone, e-mail, city and ZIP code and the best time to call, and generates a PDF estimate with the style, the door list, the cost breakdown and the next steps. The submission reaches you as a lead ready for the in-home measure.