Kitchen Remodel Calculator

Give your cabinet shop or remodeling company an estimate tool that quotes the way you do: by the linear foot. Customers pick the layout (galley, L, U, island or peninsula), type the feet of base and wall cabinets and the number of tall cabinets, choose the cabinet line (stock, semi-custom, custom), door style, finish and hardware, then the countertop and backsplash by the square foot, appliances, tear-out and plumbing or electrical relocation. The calculator shows a price range with a minimum project value, a seasonal promotion, the lead time and a PDF estimate on Letter paper, and books the free in-home measure by collecting the customer's details. Every rate is a named constant you edit to match your price list.

Home Services

Try the Calculator

Kitchen Remodel Estimate
A ballpark estimate from the linear feet of cabinets. After the free in-home measure a designer draws the kitchen and gives you the final price, usually inside the range you see below.
📐 Layout
 
 
 
 
🚪 Cabinets & Doors
 
 
 
Soft-close hinges and drawer glides are included in the semi-custom line.
🪨 Countertops & Backsplash
 
 
About 26 sq ft of countertop, counted from the base feet at 25.5 inches deep. The fabricator templates the exact slabs after the cabinets are in.
🍳 Appliances
 
 
🏗️ Tear-out & Timing
 
 
 
 

💰 Your Estimate
$ 9,664
+
$ 0
+
$ 1,740
+
$ 1,989
+
$ 0
+
$ 0
+
$ 2,450

$ 15,843
A designer measures within 5 business days. Cabinets arrive 6 weeks after the deposit, and the crew is in your kitchen about 5 working days, plus a week or two for the countertop template and install.
🧾 Summary
$ 14,259
to $17,427
Range ±10%, before sales tax. Minimum project $6,000. Final price confirmed after the in-home measure.
📋 Book Your Free In-Home Measure
 
 
 
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
export function kitchenRemodelCalculator(form: FormTs) {
// Cabinet prices, stock line with shaker doors painted white (per linear foot / each)
const cabinetRates = {
baseLf: 260,
upperLf: 190,
tallEach: 1400
};
 
// Cabinet line surcharges (% on the cabinets)
type LineId = 'stock' | 'semi' | 'custom';
const linePct: Record<LineId, number> = { 'stock': 0, 'semi': 60, 'custom': 140 };
const lineNames: Record<LineId, string> = { 'stock': 'Stock', 'semi': 'Semi-custom', 'custom': 'Custom' };
// Semi-custom and custom lines include soft-close; custom adds lead time
const lineIncludesSoftClose: Record<LineId, boolean> = { 'stock': false, 'semi': true, 'custom': true };
 
// Door style surcharges (% on the cabinets)
type DoorId = 'shaker' | 'flat' | 'raised' | 'inset';
const doorPct: Record<DoorId, number> = { 'shaker': 0, 'flat': 0, 'raised': 8, 'inset': 22 };
const doorNames: Record<DoorId, string> = { 'shaker': 'Shaker', 'flat': 'Flat panel', 'raised': 'Raised panel', 'inset': 'Inset' };
 
// Finish surcharges (% on the cabinets)
type FinishId = 'white' | 'gray' | 'navy' | 'green' | 'oak' | 'walnut';
const finishPct: Record<FinishId, number> = { 'white': 0, 'gray': 6, 'navy': 6, 'green': 6, 'oak': 4, 'walnut': 40 };
const finishNames: Record<FinishId, string> = { 'white': 'Painted white', 'gray': 'Painted gray', 'navy': 'Painted navy', 'green': 'Painted green', 'oak': 'Stained oak', 'walnut': 'Walnut' };
 
// Layouts: an island or a peninsula takes a share of the base feet
type LayoutId = 'galley' | 'l' | 'u' | 'island' | 'peninsula';
const layoutNames: Record<LayoutId, string> = { 'galley': 'Galley', 'l': 'L-shape', 'u': 'U-shape', 'island': 'L with island', 'peninsula': 'L with peninsula' };
const layoutMaxIslandFt: Record<LayoutId, number> = { 'galley': 0, 'l': 0, 'u': 0, 'island': 10, 'peninsula': 8 };
const ISLAND_SHARE = 0.3;
 
// Cabinet extras
const extrasPrices = {
softCloseLf: 14, // per ft of base + wall cabinets, stock line only
rolloutEach: 180,
islandPanels: 850, // finished panels, toe kick and the code outlet
seatingOverhang: 450,
lightingLf: 42 // per ft of wall cabinets
};
 
// Countertops and backsplash ($/sq ft, installed)
type CounterId = 'quartz' | 'granite' | 'butcher' | 'laminate';
const counterRates: Record<CounterId, number> = { 'quartz': 78, 'granite': 68, 'butcher': 48, 'laminate': 32 };
const counterNames: Record<CounterId, string> = { 'quartz': 'Quartz', 'granite': 'Granite', 'butcher': 'Butcher block', 'laminate': 'Laminate' };
type BacksplashId = 'none' | 'tile' | 'slab';
const backsplashRates: Record<BacksplashId, number> = { 'none': 0, 'tile': 32, 'slab': 95 };
const backsplashNames: Record<BacksplashId, string> = { 'none': 'Paint only', 'tile': 'Subway tile', 'slab': 'Countertop slab' };
const SINK_AND_FAUCET = 900;
const COUNTER_DEPTH_FT = 25.5 / 12; // 25.5-inch countertop over 24-inch base cabinets
const SPLASH_HEIGHT_FT = 1.5; // 18 inches between the counter and the wall cabinets
const OVERHANG_FT = 1; // 12-inch seating overhang
 
// Appliances, mid-range models, delivered and installed
const appliancePrices: Record<string, number> = {
'range': 2200,
'cooktop': 1500,
'wallOven': 2600,
'hood': 900,
'dishwasher': 1100,
'fridge': 2900,
'microwave': 600
};
const applianceNames: Record<string, string> = {
'range': 'Range',
'cooktop': 'Cooktop',
'wallOven': 'Wall oven',
'hood': 'Range hood',
'dishwasher': 'Dishwasher',
'fridge': 'Refrigerator',
'microwave': 'Microwave'
};
 
// The project: installation, tear-out, hookups, relocation
const projectPrices = {
installPct: 18, // % of the cabinet price: delivery, setting, leveling, hardware
tearOut: 1800,
hookupsInPlace: 650,
plumbingRelocation: 1600,
electricalRelocation: 1900,
permit: 450
};
 
// Result
const RANGE_PCT = 10; // the estimate is shown as a range ±10%
const MINIMUM_PROJECT = 6000;
const PROMO_PCT = 0; // promotional discount; 0 hides the banner
const PROMO_TEXT = 'Fall remodel special';
const LEAD_WEEKS = 6; // cabinets from the deposit, stock and semi-custom
const CUSTOM_EXTRA_WEEKS = 4;
const MEASURE_DAYS = 5; // business days until the in-home measure
const INSTALL_DAYS = 5; // working days the crew is in the house
 
const money = (value: number) => (value < 0 ? '-$' : '$') + Math.round(Math.abs(value)).toLocaleString('en-US');
const feet = (value: number) => (Math.round(value * 10) / 10) + ' ft';
const sqft = (value: number) => Math.round(value) + ' sq ft';
const pct = (value: number) => value > 0 ? ' (+' + value + '%)' : '';
 
form.addRow(row => {
row.addTextPanel('header', {
computedValue: () => 'Kitchen Remodel Estimate',
customStyles: { 'font-size': '1.5rem', 'font-weight': '600', 'color': '#1e293b' }
});
});
 
form.addRow(row => {
row.addTextPanel('intro', {
computedValue: () => 'A ballpark estimate from the linear feet of cabinets. After the free in-home measure a designer draws the kitchen and gives you the final price, usually inside the range you see below.',
customStyles: { 'font-size': '0.95rem', 'color': '#475569' }
});
});
 
form.addSpacer({ height: 20 });
 
// Layout Section
const layoutSection = form.addSubform('layout', { title: '📐 Layout' });
 
layoutSection.addRow(row => {
row.addRadioButton('layout', {
label: 'Kitchen layout',
options: [
{ id: 'galley', name: 'Galley - two walls facing each other' },
{ id: 'l', name: 'L-shape - two walls in a corner (most common)' },
{ id: 'u', name: 'U-shape - three walls' },
{ id: 'island', name: 'L with island (+$' + extrasPrices.islandPanels + ' panels and outlet)' },
{ id: 'peninsula', name: 'L with peninsula (+$' + extrasPrices.islandPanels + ' panels and outlet)' }
],
defaultValue: 'l',
orientation: 'vertical',
isRequired: true
});
});
 
layoutSection.addRow(row => {
row.addDecimal('baseFeet', {
label: 'Base cabinets (linear ft)',
min: 2,
max: 40,
step: 0.5,
defaultValue: 12,
tooltip: 'Linear feet along the walls, island included',
isRequired: true
}, '1fr');
row.addDecimal('upperFeet', {
label: 'Wall cabinets (linear ft)',
min: 0,
max: 40,
step: 0.5,
defaultValue: 8,
tooltip: '0 for open shelves',
isRequired: true
}, '1fr');
row.addInteger('tallCabinets', {
label: 'Tall cabinets',
min: 0,
max: 6,
defaultValue: 1,
tooltip: 'Pantry, wall oven, refrigerator cabinet'
}, '1fr');
});
 
const getLayout = (): LayoutId => {
const value = layoutSection.radioButton('layout')?.value();
return value === 'galley' || value === 'u' || value === 'island' || value === 'peninsula' ? value : 'l';
};
const getBaseFeet = () => layoutSection.decimal('baseFeet')?.value() || 12;
const getUpperFeet = () => layoutSection.decimal('upperFeet')?.value() || 0;
const getTallCabinets = () => layoutSection.integer('tallCabinets')?.value() || 0;
const hasIsland = () => layoutMaxIslandFt[getLayout()] > 0;
const getIslandFeet = () => hasIsland() ? Math.min(layoutMaxIslandFt[getLayout()], Math.max(4, getBaseFeet() * ISLAND_SHARE)) : 0;
const islandWord = () => getLayout() === 'peninsula' ? 'peninsula' : 'island';
 
layoutSection.addRow(row => {
row.addCheckbox('seating', {
label: 'Seating overhang with room for counter stools (+$' + extrasPrices.seatingOverhang + ')',
defaultValue: false,
isVisible: () => hasIsland()
});
});
 
layoutSection.addRow(row => {
row.addTextPanel('islandInfo', {
computedValue: () => 'The ' + islandWord() + ' takes about ' + feet(getIslandFeet()) + ' of the base cabinets. Keep at least 42 inches of walkway between the ' + islandWord() + ' and the wall cabinets; the designer checks it at the measure.',
customStyles: { 'font-size': '0.9rem', 'color': '#0369a1', 'background': '#e0f2fe', 'padding': '10px', 'border-radius': '6px' },
isVisible: () => hasIsland()
});
});
 
const hasSeating = () => hasIsland() && layoutSection.checkbox('seating')?.value() === true;
 
// Cabinets Section
const cabinetsSection = form.addSubform('cabinets', { title: '🚪 Cabinets & Doors' });
 
cabinetsSection.addRow(row => {
row.addRadioButton('line', {
label: 'Cabinet line',
options: [
{ id: 'stock', name: 'Stock - standard sizes, quick from the warehouse (base price)' },
{ id: 'semi', name: 'Semi-custom - plywood boxes, more sizes, soft-close included' + pct(linePct['semi']) },
{ id: 'custom', name: 'Custom - built in our shop to the inch' + pct(linePct['custom']) }
],
defaultValue: 'semi',
orientation: 'vertical',
isRequired: true
}, '1fr');
row.addRadioButton('door', {
label: 'Door style',
options: [
{ id: 'shaker', name: 'Shaker - flat center panel in a square frame (included)' },
{ id: 'flat', name: 'Flat panel - plain slab, clean and modern (same price)' },
{ id: 'raised', name: 'Raised panel - profiled center panel, traditional' + pct(doorPct['raised']) },
{ id: 'inset', name: 'Inset - doors set flush inside a face frame' + pct(doorPct['inset']) }
],
defaultValue: 'shaker',
orientation: 'vertical',
isRequired: true
}, '1fr');
});
 
const getLine = (): LineId => {
const value = cabinetsSection.radioButton('line')?.value();
return value === 'stock' || value === 'custom' ? value : 'semi';
};
const getDoor = (): DoorId => {
const value = cabinetsSection.radioButton('door')?.value();
return value === 'flat' || value === 'raised' || value === 'inset' ? value : 'shaker';
};
 
cabinetsSection.addRow(row => {
row.addDropdown('finish', {
label: 'Finish',
options: [
{ id: 'white', name: 'Painted white (included)' },
{ id: 'gray', name: 'Painted gray' + pct(finishPct['gray']) },
{ id: 'navy', name: 'Painted navy' + pct(finishPct['navy']) },
{ id: 'green', name: 'Painted green' + pct(finishPct['green']) },
{ id: 'oak', name: 'Stained white oak' + pct(finishPct['oak']) },
{ id: 'walnut', name: 'Walnut' + pct(finishPct['walnut']) }
],
defaultValue: 'white',
tooltip: 'Painted doors are sprayed in the shop; oak and walnut are stained and clear-coated',
isRequired: true
}, '1fr');
row.addInteger('rollouts', {
label: 'Roll-out trays (+$' + extrasPrices.rolloutEach + ' each)',
min: 0,
max: 12,
defaultValue: 0,
tooltip: 'Pull-out shelves in base and pantry cabinets'
}, '1fr');
});
 
const getFinish = (): FinishId => {
const value = cabinetsSection.dropdown('finish')?.value();
return value === 'gray' || value === 'navy' || value === 'green' || value === 'oak' || value === 'walnut' ? value : 'white';
};
 
cabinetsSection.addRow(row => {
row.addCheckbox('softClose', {
label: 'Soft-close hinges and drawer glides (+$' + extrasPrices.softCloseLf + '/ft)',
defaultValue: false,
isVisible: () => !lineIncludesSoftClose[getLine()]
}, '1fr');
row.addCheckbox('lighting', {
label: 'Under-cabinet LED lighting (+$' + extrasPrices.lightingLf + '/ft of wall cabinets)',
defaultValue: false,
isVisible: () => getUpperFeet() > 0
}, '1fr');
});
 
cabinetsSection.addRow(row => {
row.addTextPanel('softCloseInfo', {
computedValue: () => 'Soft-close hinges and drawer glides are included in the ' + lineNames[getLine()].toLowerCase() + ' line.',
customStyles: { 'font-size': '0.85rem', 'color': '#64748b', 'font-style': 'italic' },
isVisible: () => lineIncludesSoftClose[getLine()]
});
});
 
const hasSoftClose = () => lineIncludesSoftClose[getLine()] || cabinetsSection.checkbox('softClose')?.value() === true;
const hasLighting = () => getUpperFeet() > 0 && cabinetsSection.checkbox('lighting')?.value() === true;
const getRollouts = () => cabinetsSection.integer('rollouts')?.value() || 0;
 
// Countertops Section
const countersSection = form.addSubform('countertops', { title: '🪨 Countertops & Backsplash' });
 
countersSection.addRow(row => {
row.addRadioButton('counter', {
label: 'Countertop',
options: [
{ id: 'quartz', name: 'Quartz - no sealing, consistent pattern ($' + counterRates['quartz'] + '/sq ft)' },
{ id: 'granite', name: 'Granite - natural stone, sealed once a year ($' + counterRates['granite'] + '/sq ft)' },
{ id: 'butcher', name: 'Butcher block - maple, warm, oiled ($' + counterRates['butcher'] + '/sq ft)' },
{ id: 'laminate', name: 'Laminate - budget-friendly, many patterns ($' + counterRates['laminate'] + '/sq ft)' }
],
defaultValue: 'quartz',
orientation: 'vertical',
isRequired: true
}, '1fr');
row.addRadioButton('backsplash', {
label: 'Backsplash',
options: [
{ id: 'none', name: 'Paint only - we leave the wall for the painter' },
{ id: 'tile', name: 'Subway tile - 3 x 6 ceramic, 18 in high ($' + backsplashRates['tile'] + '/sq ft)' },
{ id: 'slab', name: 'Countertop slab - the counter runs up the wall ($' + backsplashRates['slab'] + '/sq ft)' }
],
defaultValue: 'none',
orientation: 'vertical',
isRequired: true
}, '1fr');
});
 
countersSection.addRow(row => {
row.addCheckbox('sink', {
label: 'New stainless undermount sink with pull-down faucet (+$' + SINK_AND_FAUCET + ')',
defaultValue: false
});
});
 
const getCounter = (): CounterId => {
const value = countersSection.radioButton('counter')?.value();
return value === 'granite' || value === 'butcher' || value === 'laminate' ? value : 'quartz';
};
const getBacksplash = (): BacksplashId => {
const value = countersSection.radioButton('backsplash')?.value();
return value === 'tile' || value === 'slab' ? value : 'none';
};
const hasSink = () => countersSection.checkbox('sink')?.value() === true;
const getCounterSqft = () => getBaseFeet() * COUNTER_DEPTH_FT + (hasSeating() ? getIslandFeet() * OVERHANG_FT : 0);
// Backsplash goes on the wall runs only: the island or the peninsula has none
const getBacksplashSqft = () => getBacksplash() === 'none' ? 0 : Math.max(0, getBaseFeet() - getIslandFeet()) * SPLASH_HEIGHT_FT;
 
countersSection.addRow(row => {
row.addTextPanel('counterInfo', {
computedValue: () => 'About ' + sqft(getCounterSqft()) + ' of countertop'
+ (getBacksplashSqft() > 0 ? ' and ' + sqft(getBacksplashSqft()) + ' of backsplash' : '')
+ ', counted from the base feet at 25.5 inches deep' + (hasSeating() ? ' with the 12-inch seating overhang' : '')
+ '. The fabricator templates the exact slabs after the cabinets are in.',
customStyles: { 'font-size': '0.9rem', 'color': '#0369a1', 'background': '#e0f2fe', 'padding': '10px', 'border-radius': '6px' }
});
});
 
// Appliances Section
const appliancesSection = form.addSubform('appliances', { title: '🍳 Appliances' });
 
appliancesSection.addRow(row => {
row.addRadioButton('plan', {
label: 'Appliances',
options: [
{ id: 'own', name: 'I keep or buy my own - we leave the openings and connect them' },
{ id: 'ours', name: 'New from you - delivered, installed, connected' }
],
defaultValue: 'own',
orientation: 'vertical',
isRequired: true
});
});
 
const suppliesAppliances = () => appliancesSection.radioButton('plan')?.value() === 'ours';
 
appliancesSection.addRow(row => {
row.addCheckboxList('appliances', {
label: () => suppliesAppliances() ? 'What we supply and install (mid-range models)' : 'What goes into the layout',
options: () => Object.keys(applianceNames).map(id => ({
id,
name: (applianceNames[id] || id) + (suppliesAppliances() ? ' (+$' + (appliancePrices[id] || 0).toLocaleString('en-US') + ')' : '')
})),
defaultValue: ['range', 'dishwasher'],
orientation: 'vertical'
});
});
 
const getAppliances = () => (appliancesSection.checkboxList('appliances')?.value() || []).filter(id => id in applianceNames);
 
appliancesSection.addRow(row => {
row.addTextPanel('ovenInfo', {
computedValue: () => 'A wall oven needs a tall cabinet. Add one in the Layout section, or pick a range instead: the oven sits under the cooktop and no tall cabinet is needed.',
customStyles: { 'font-size': '0.9rem', 'color': '#b45309', 'background': '#fef3c7', 'padding': '10px', 'border-radius': '6px' },
isVisible: () => getAppliances().includes('wallOven') && getTallCabinets() === 0
});
});
 
// Project Section
const projectSection = form.addSubform('project', { title: '🏗️ Tear-out & Timing' });
 
projectSection.addRow(row => {
row.addRadioButton('existing', {
label: 'The kitchen today',
options: [
{ id: 'remove', name: 'Tear out the old kitchen - we remove and haul away (+$' + projectPrices.tearOut.toLocaleString('en-US') + ')' },
{ id: 'gutted', name: 'Already gutted - the room is empty' },
{ id: 'new', name: 'New construction - nothing to remove' }
],
defaultValue: 'remove',
orientation: 'vertical',
isRequired: true
}, '1fr');
row.addRadioButton('hookups', {
label: 'Plumbing and electrical',
options: [
{ id: 'same', name: 'Sink and range stay put - reconnect in place (+$' + projectPrices.hookupsInPlace + ')' },
{ id: 'move', name: 'Sink or range moves - new lines, circuits and a permit (+$' + (projectPrices.plumbingRelocation + projectPrices.electricalRelocation + projectPrices.permit).toLocaleString('en-US') + ')' }
],
defaultValue: 'same',
orientation: 'vertical',
isRequired: true
}, '1fr');
});
 
const needsTearOut = () => (projectSection.radioButton('existing')?.value() || 'remove') === 'remove';
const movesHookups = () => projectSection.radioButton('hookups')?.value() === 'move';
const getLeadWeeks = () => LEAD_WEEKS + (getLine() === 'custom' ? CUSTOM_EXTRA_WEEKS : 0);
 
projectSection.addRow(row => {
row.addTextPanel('permitInfo', {
computedValue: () => 'Moving the sink or the range means a permit: new supply and drain lines ($' + projectPrices.plumbingRelocation.toLocaleString('en-US') + '), new circuits ($' + projectPrices.electricalRelocation.toLocaleString('en-US') + ') and a city inspection ($' + projectPrices.permit + '). An island with an outlet needs a circuit too.',
customStyles: { 'font-size': '0.9rem', 'color': '#b45309', 'background': '#fef3c7', 'padding': '10px', 'border-radius': '6px' },
isVisible: () => movesHookups()
});
});
 
projectSection.addRow(row => {
row.addDatepicker('startDate', {
label: () => 'Preferred start (cabinets take ' + getLeadWeeks() + ' weeks from the deposit)',
isRequired: false
}, '1fr');
row.addTextarea('notes', {
label: 'Anything else we should know?',
placeholder: 'E.g. the window over the sink stays, the wall to the dining room comes down, we have the floor plan',
rows: 2,
maxLength: 1500
}, '1fr');
});
 
form.addSpacer({ height: 20, showLine: true, lineStyle: 'dashed' });
 
// Pricing helpers
const getCabinetsBase = () => getBaseFeet() * cabinetRates.baseLf + getUpperFeet() * cabinetRates.upperLf + getTallCabinets() * cabinetRates.tallEach;
const getCabinets = () => getCabinetsBase() * (1 + linePct[getLine()] / 100) * (1 + doorPct[getDoor()] / 100) * (1 + finishPct[getFinish()] / 100);
const getIslandExtra = () => (hasIsland() ? extrasPrices.islandPanels : 0) + (hasSeating() ? extrasPrices.seatingOverhang : 0);
const getSoftCloseCost = () => !lineIncludesSoftClose[getLine()] && hasSoftClose() ? (getBaseFeet() + getUpperFeet()) * extrasPrices.softCloseLf : 0;
const getRolloutCost = () => getRollouts() * extrasPrices.rolloutEach;
const getLightingCost = () => hasLighting() ? getUpperFeet() * extrasPrices.lightingLf : 0;
const getExtras = () => getIslandExtra() + getSoftCloseCost() + getRolloutCost() + getLightingCost();
const getInstall = () => getCabinets() * projectPrices.installPct / 100;
const getCounters = () => getCounterSqft() * counterRates[getCounter()];
const getBacksplashCost = () => getBacksplashSqft() * backsplashRates[getBacksplash()];
const getSinkCost = () => hasSink() ? SINK_AND_FAUCET : 0;
const getApplianceCost = () => suppliesAppliances() ? getAppliances().reduce((sum, id) => sum + (appliancePrices[id] || 0), 0) : 0;
const getSiteCost = () => (needsTearOut() ? projectPrices.tearOut : 0)
+ (movesHookups() ? projectPrices.plumbingRelocation + projectPrices.electricalRelocation + projectPrices.permit : projectPrices.hookupsInPlace);
const getBeforePromo = () => getCabinets() + getExtras() + getInstall() + getCounters() + getBacksplashCost() + getSinkCost() + getApplianceCost() + getSiteCost();
const getPromo = () => Math.round(getBeforePromo() * PROMO_PCT / 100);
// The minimum holds last: a promotion may never take a project below what the shop costs
const getTotal = () => Math.max(Math.round(getBeforePromo() - getPromo()), MINIMUM_PROJECT);
const getPriceFrom = () => Math.round(getTotal() * (1 - RANGE_PCT / 100));
const getPriceTo = () => Math.round(getTotal() * (1 + RANGE_PCT / 100));
const applianceLine = () => getAppliances().map(id => applianceNames[id] || id).join(', ') || 'none';
 
// Estimate Section
const summarySection = form.addSubform('summary', { title: '💰 Your Estimate', isCollapsible: false });
 
summarySection.addRow(row => {
row.addTextPanel('promoBanner', {
computedValue: () => PROMO_TEXT + ': the ' + PROMO_PCT + '% discount is already in the range, ' + money(getPromo()) + ' less on this kitchen.',
customStyles: { 'font-size': '0.95rem', 'font-weight': '500', 'color': '#166534', 'background': '#dcfce7', 'padding': '10px', 'border-radius': '6px' },
isVisible: () => getPromo() > 0
});
});
 
summarySection.addRow(row => {
row.addPriceDisplay('cabinetsCost', {
label: () => lineNames[getLine()] + ' cabinets (' + feet(getBaseFeet()) + ' base, ' + feet(getUpperFeet()) + ' wall, ' + getTallCabinets() + ' tall)',
computedValue: () => Math.round(getCabinets()),
decimals: 0,
variant: 'default'
}, '1fr');
row.addPriceDisplay('extrasCost', {
label: 'Cabinet extras',
computedValue: () => Math.round(getExtras()),
decimals: 0,
variant: 'default',
prefix: '+'
}, '1fr');
});
 
summarySection.addRow(row => {
row.addPriceDisplay('installCost', {
label: 'Installation (' + projectPrices.installPct + '% of the cabinets)',
computedValue: () => Math.round(getInstall()),
decimals: 0,
variant: 'default',
prefix: '+'
}, '1fr');
row.addPriceDisplay('countersCost', {
label: () => counterNames[getCounter()] + ' countertop, ' + sqft(getCounterSqft()),
computedValue: () => Math.round(getCounters()),
decimals: 0,
variant: 'default',
prefix: '+'
}, '1fr');
});
 
summarySection.addRow(row => {
row.addPriceDisplay('backsplashSinkCost', {
label: 'Backsplash and sink',
computedValue: () => Math.round(getBacksplashCost() + getSinkCost()),
decimals: 0,
variant: 'default',
prefix: '+'
}, '1fr');
row.addPriceDisplay('applianceCost', {
label: 'Appliances',
computedValue: () => Math.round(getApplianceCost()),
decimals: 0,
variant: 'default',
prefix: '+'
}, '1fr');
});
 
summarySection.addRow(row => {
row.addPriceDisplay('siteCost', {
label: 'Tear-out, hookups and permit',
computedValue: () => Math.round(getSiteCost()),
decimals: 0,
variant: 'default',
prefix: '+'
}, '1fr');
row.addPriceDisplay('promoDiscount', {
label: PROMO_TEXT,
computedValue: () => -getPromo(),
decimals: 0,
variant: 'success',
isVisible: () => getPromo() > 0
}, '1fr');
});
 
summarySection.addSpacer({ showLine: true, lineStyle: 'solid', lineColor: '#e2e8f0' });
 
summarySection.addRow(row => {
row.addPriceDisplay('totalEstimate', {
label: 'Middle of the range',
computedValue: () => getTotal(),
decimals: 0,
variant: 'large'
});
});
 
summarySection.addRow(row => {
row.addTextPanel('timeline', {
computedValue: () => 'A designer measures within ' + MEASURE_DAYS + ' business days. Cabinets arrive ' + getLeadWeeks() + ' weeks after the deposit, and the crew is in your kitchen about ' + INSTALL_DAYS + ' working days, plus a week or two for the countertop template and install.',
customStyles: { 'font-size': '0.9rem', 'color': '#059669', 'font-weight': '500' }
});
});
 
// Sticky Summary Section
const finalSection = form.addSubform('final', {
title: '🧾 Summary',
isCollapsible: false,
sticky: 'bottom'
});
 
finalSection.addRow(row => {
row.addPriceDisplay('priceRange', {
label: 'Estimated kitchen',
computedValue: () => getPriceFrom(),
decimals: 0,
variant: 'large',
suffix: () => 'to ' + money(getPriceTo())
});
});
 
finalSection.addRow(row => {
row.addTextPanel('disclaimer', {
computedValue: () => 'Range ±' + RANGE_PCT + '%, before sales tax. Minimum project $' + MINIMUM_PROJECT.toLocaleString('en-US') + '. Final price confirmed after the in-home measure.',
customStyles: { 'font-size': '0.85rem', 'color': '#94a3b8', 'font-style': 'italic' }
});
});
 
// Contact Section
const contactSection = form.addSubform('contact', { title: '📋 Book Your Free In-Home Measure' });
 
contactSection.addRow(row => {
row.addTextbox('name', {
label: 'Full Name',
placeholder: 'John Smith',
isRequired: true
}, '1fr');
row.addTextbox('phone', {
label: 'Phone',
placeholder: '(555) 123-4567',
isRequired: true
}, '1fr');
});
 
contactSection.addRow(row => {
row.addEmail('email', {
label: 'Email Address',
placeholder: 'john@example.com',
tooltip: 'Where we send the PDF estimate',
isRequired: true
}, '1fr');
row.addTextbox('zip', {
label: 'ZIP Code',
placeholder: '30062',
tooltip: 'Where we come to measure',
isRequired: true
}, '1fr');
});
 
// PDF estimate
form.configurePdf('estimate', pdf => {
pdf.configure({
filename: 'kitchen-estimate',
pageSize: 'Letter',
allowUserDownload: true,
downloadButtonLabel: 'Download the estimate as PDF',
header: {
title: 'Kitchen Estimate',
subtitle: layoutNames[getLayout()] + ', ' + feet(getBaseFeet()) + ' of base cabinets, ' + lineNames[getLine()].toLowerCase() + ' line'
},
footer: {
text: 'Estimate based on the configuration from the form. A binding quote follows the in-home measure within ' + MEASURE_DAYS + ' business days.',
showPageNumbers: true
}
});
 
pdf.addSection('Your Kitchen', section => {
section.addRow(row => {
row.addField('Layout', layoutNames[getLayout()] + (hasSeating() ? ', seating overhang' : ''));
row.addField('Base cabinets', feet(getBaseFeet()));
row.addField('Wall cabinets', feet(getUpperFeet()));
row.addField('Tall cabinets', String(getTallCabinets()));
});
section.addRow(row => {
row.addField('Cabinet line', lineNames[getLine()]);
row.addField('Door style', doorNames[getDoor()]);
row.addField('Finish', finishNames[getFinish()]);
row.addField('Hardware', [hasSoftClose() ? 'soft-close' : 'standard hinges', getRollouts() > 0 ? getRollouts() + ' roll-out trays' : '', hasLighting() ? 'LED lighting' : ''].filter(part => part !== '').join(', '));
});
section.addRow(row => {
row.addField('Countertop', counterNames[getCounter()] + ', ' + sqft(getCounterSqft()));
row.addField('Backsplash', getBacksplash() === 'none' ? 'paint only' : backsplashNames[getBacksplash()] + ', ' + sqft(getBacksplashSqft()));
row.addField('Sink and faucet', hasSink() ? 'new' : 'existing');
row.addField('Appliances', (suppliesAppliances() ? 'new from us: ' : 'customer\'s own: ') + applianceLine());
});
section.addRow(row => {
row.addField('Existing kitchen', needsTearOut() ? 'tear-out and haul-away' : 'nothing to remove');
row.addField('Plumbing and electrical', movesHookups() ? 'sink or range moves, permit' : 'hookups in place');
row.addField('Preferred start', projectSection.datepicker('startDate')?.value() || 'flexible');
row.addField('ZIP code', contactSection.textbox('zip')?.value() || '');
});
});
 
pdf.addSection('Estimate', section => {
const rows: string[][] = [
[lineNames[getLine()] + ' cabinets', feet(getBaseFeet()) + ' base, ' + feet(getUpperFeet()) + ' wall, ' + getTallCabinets() + ' tall; ' + doorNames[getDoor()].toLowerCase() + ', ' + finishNames[getFinish()].toLowerCase(), money(getCabinets())]
];
if (getExtras() > 0) {
rows.push(['Cabinet extras', [
hasIsland() ? islandWord() + ' panels' : '',
hasSeating() ? 'seating overhang' : '',
getSoftCloseCost() > 0 ? 'soft-close' : '',
getRollouts() > 0 ? getRollouts() + ' roll-out trays' : '',
hasLighting() ? 'under-cabinet lighting, ' + feet(getUpperFeet()) : ''
].filter(part => part !== '').join(', '), money(getExtras())]);
}
rows.push(['Installation', projectPrices.installPct + '% of the cabinets: delivery, setting, leveling, hardware', money(getInstall())]);
rows.push([counterNames[getCounter()] + ' countertop', sqft(getCounterSqft()) + ' at $' + counterRates[getCounter()] + '/sq ft, installed', money(getCounters())]);
if (getBacksplashCost() > 0) rows.push([backsplashNames[getBacksplash()] + ' backsplash', sqft(getBacksplashSqft()) + ' at $' + backsplashRates[getBacksplash()] + '/sq ft', money(getBacksplashCost())]);
if (getSinkCost() > 0) rows.push(['Sink and faucet', 'stainless undermount, pull-down faucet', money(getSinkCost())]);
if (getApplianceCost() > 0) rows.push(['Appliances', applianceLine() + ', installed', money(getApplianceCost())]);
rows.push(['Tear-out, hookups and permit', (needsTearOut() ? 'tear-out and haul-away, ' : '') + (movesHookups() ? 'plumbing and electrical relocation, permit' : 'hookups in place'), money(getSiteCost())]);
rows.push(['Total before discounts', '', money(getBeforePromo())]);
if (getPromo() > 0) rows.push([PROMO_TEXT, PROMO_PCT + '%', '-' + money(getPromo())]);
section.addTable(['Item', 'Details', 'Amount'], rows);
section.addSpacer(8);
section.addText('Estimate: ' + money(getPriceFrom()) + ' to ' + money(getPriceTo()) + ' (range ±' + RANGE_PCT + '%, sales tax as applicable, price confirmed after the in-home measure).');
});
 
pdf.addSection('What Happens Next', section => {
section.addText('In-home measure within ' + MEASURE_DAYS + ' business days: a designer measures the room and draws the kitchen.');
section.addText('Cabinets arrive about ' + getLeadWeeks() + ' weeks after the deposit' + (getLine() === 'custom' ? ' (custom line)' : '') + '.');
section.addText('Installation about ' + INSTALL_DAYS + ' working days, then the countertop template and install.');
const notes = projectSection.textarea('notes')?.value();
if (notes) {
section.addDivider();
section.addText('Notes: ' + notes);
}
});
});
 
form.configureSubmitButton({
label: 'Send and Book the Measure'
});
 
form.configureCompletionScreen({
type: 'text',
title: 'Thank you!',
message: 'Your estimate is ready to download. A designer will call to schedule the free in-home measure.'
});
}
 

Frequently Asked Questions

Can I change the per-foot cabinet prices and the surcharges?

Yes. Base, wall and tall cabinet prices, the line, door and finish surcharges, countertop and backsplash rates per square foot, appliance prices and the project costs are named constants at the top of the code. Change a number and the whole estimate follows.

Why does the calculator show a range instead of a single price?

A kitchen is never priced to the dollar before the measure. The calculator shows the middle of the range and a band of plus or minus 10 percent, which you can narrow or widen. The PDF says the binding quote follows the in-home measure.

How are countertops and backsplash counted?

The countertop area is derived from the base cabinet feet at a 25.5-inch depth, with the seating overhang added for an island or peninsula. The backsplash covers the wall runs only, 18 inches high. The fabricator templates the exact slabs after the cabinets are in.

Can I offer appliances or leave them to the customer?

Both. The customer chooses whether they keep or buy their own appliances or want new ones from you. Appliances are priced only when you supply them; otherwise the layout just leaves the openings.

Does the calculator handle tear-out and relocation?

Yes. The customer says whether the old kitchen comes out and whether the sink or range moves. Hookups in place or a relocation with plumbing, electrical and a permit are added to the estimate, with a note explaining why the permit is needed.