Screen Printing Calculator

Give your screen printing shop a calculator that turns garment costs, ink colors, print locations and screen setup into an itemized estimate before the customer ever emails you. Visitors choose cotton tees, premium tees or hoodies, enter the quantity, set front and back ink colors and add an underbase for dark garments. Quantity breaks reduce printing charges only, while blanks, setup and artwork stay unchanged. Artwork cleanup, rush production, a production minimum and shipping are shown line by line, together with the average price per garment. The customer leaves their details and gets a Letter-size PDF estimate, and you receive a qualified lead with the full job specification.

B2B Services

Try the Calculator

Screen Printing Estimate
Choose a garment, print locations and ink colors. One garment style and one shared design per batch; we confirm artwork, sizes and stock before production.
Step 1 of 4: Your batch
Garment and quantity
 
 
Quantity breaks on printing: 10% off from 50 garments, 20% off from 100 garments. Blank garments, setup and artwork stay unchanged.
Step 2 of 4: Your print
Colors and locations
 
 
One design across the batch. Sleeve prints, individual names, mixed garment styles and special inks need a separate quote. Send your artwork by replying to the estimate email.
Screens to set up: 2 ($25.00 each)
Step 3 of 4: Job services
Artwork and delivery
 
Step 4 of 4: Your estimate
Your Estimate
$ 369.50
$ 7.39
Illustrative rates, before applicable tax; subject to artwork, stock and production review.
What goes into the estimate
Batch: 50 x Cotton tee; front 2 colors; back 0 colors; underbase none
$ 225.00
$105.00 $ 94.50
Save $10.50 (10% off)
+
$ 50.00
Ask the shop to review this job
 
 
 
 
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
export function screenPrintingCalculator(form: FormTs) {
// Blank garment prices per piece (quantity breaks never reduce these)
const garmentPrices: Record<string, number> = {
'cotton': 4.5,
'premium': 7,
'hoodie': 18
};
 
const garmentNames: Record<string, string> = {
'cotton': 'Cotton tee',
'premium': 'Premium tee',
'hoodie': 'Hoodie'
};
 
// Printing rates per garment
const printing = {
firstColor: 1.5, // first color at each active location
extraColor: 0.6, // each additional color pass at the same location (underbase counts as one)
screenSetup: 25 // once per screen (color and location, underbases included)
};
 
// Quantity breaks on printing only (they replace, not stack)
const quantityBreaks = {
from50Percent: 10,
from100Percent: 20
};
 
// Job-level services
const jobServices = {
artworkFee: 45, // one cleanup charge per batch
rushPercent: 25, // of garments plus discounted printing
minimumOrder: 150, // minimum production value before shipping
shippingFee: 20 // pickup is free
};
 
const garmentPrice = (id: string) => garmentPrices[id] ?? 0;
const garmentName = (id: string) => garmentNames[id] ?? id;
 
const money = (value: number) => {
const fixed = Math.abs(value).toFixed(2);
const parts = fixed.split('.');
const whole = (parts[0] ?? '0').replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return (value < 0 ? '-' : '') + '$' + whole + '.' + (parts[1] ?? '00');
};
const cents = (value: number) => Math.round(value * 100) / 100;
const clamp = (value: number | null | undefined, fallback: number, min: number, max: number) => {
const n = typeof value === 'number' ? Math.floor(value) : fallback;
return Math.min(max, Math.max(min, n));
};
 
const headerStyles: CustomStyles = { 'font-size': '1.5rem', 'font-weight': '600', 'color': '#1e293b' };
const stepStyles: CustomStyles = { 'font-size': '1.1rem', 'font-weight': '600', 'color': '#1e293b' };
const hintStyles: CustomStyles = { 'font-size': '0.9rem', 'color': '#64748b' };
const infoStyles: CustomStyles = { 'font-size': '0.9rem', 'color': '#0369a1', 'background': '#e0f2fe', 'padding': '10px', 'border-radius': '6px' };
 
form.addRow(row => {
row.addTextPanel('header', {
computedValue: () => 'Screen Printing Estimate',
customStyles: headerStyles
});
});
 
form.addRow(row => {
row.addTextPanel('intro', {
computedValue: () => 'Choose a garment, print locations and ink colors. One garment style and one shared design per batch; we confirm artwork, sizes and stock before production.',
customStyles: hintStyles
});
});
 
form.addSpacer({ height: 10 });
 
const pages = form.addPages('steps');
 
// Step 1: Your batch
const jobPage = pages.addPage('job');
 
jobPage.addRow(row => {
row.addTextPanel('jobTitle', {
computedValue: () => 'Step 1 of 4: Your batch',
customStyles: stepStyles
});
});
 
const jobSection = jobPage.addSubform('batch', { title: 'Garment and quantity', isCollapsible: false });
 
jobSection.addRow(row => {
row.addRadioButton('garment', {
label: 'Garment style',
options: [
{ id: 'cotton', name: 'Cotton tee - ' + money(garmentPrice('cotton')) + ' blank / piece' },
{ id: 'premium', name: 'Premium tee - ' + money(garmentPrice('premium')) + ' blank / piece' },
{ id: 'hoodie', name: 'Hoodie - ' + money(garmentPrice('hoodie')) + ' blank / piece' }
],
defaultValue: 'cotton',
orientation: 'vertical',
isRequired: true
}, '1fr');
row.addInteger('quantity', {
label: 'How many garments?',
min: 12,
max: 10000,
defaultValue: 50,
placeholder: 'e.g. 50',
tooltip: 'One garment style and one design. Size mix and stock are confirmed after your request.',
isRequired: true
}, '1fr');
});
 
jobSection.addRow(row => {
row.addTextPanel('breaksInfo', {
computedValue: () => 'Quantity breaks on printing: ' + quantityBreaks.from50Percent + '% off from 50 garments, '
+ quantityBreaks.from100Percent + '% off from 100 garments. Blank garments, setup and artwork stay unchanged.',
customStyles: infoStyles
});
});
 
const garmentKey = form.computedValue(() => {
const id = jobSection.radioButton('garment')?.value();
return id && garmentPrices[id] !== undefined ? id : 'cotton';
});
const quantity = form.computedValue(() => clamp(jobSection.integer('quantity')?.value(), 50, 12, 10000));
 
// Step 2: Your print
const printPage = pages.addPage('print');
 
printPage.addRow(row => {
row.addTextPanel('printTitle', {
computedValue: () => 'Step 2 of 4: Your print',
customStyles: stepStyles
});
});
 
const printSection = printPage.addSubform('design', { title: 'Colors and locations', isCollapsible: false });
 
printSection.addRow(row => {
row.addInteger('frontColors', {
label: 'Front ink colors',
min: 1,
max: 6,
defaultValue: 2,
tooltip: 'Design colors only; the dark-garment underbase is added separately.',
isRequired: true
}, '1fr');
row.addInteger('backColors', {
label: 'Back ink colors',
min: 0,
max: 6,
defaultValue: 0,
tooltip: 'Leave at 0 for no back print.',
isRequired: true
}, '1fr');
});
 
printSection.addRow(row => {
row.addCheckbox('underbase', {
label: 'Dark garments requiring an underbase (+1 screen and +1 color pass at each active location)',
defaultValue: false
});
});
 
printSection.addRow(row => {
row.addTextPanel('scopeInfo', {
computedValue: () => 'One design across the batch. Sleeve prints, individual names, mixed garment styles and special inks need a separate quote. Send your artwork by replying to the estimate email.',
customStyles: infoStyles
});
});
 
const frontColors = form.computedValue(() => clamp(printSection.integer('frontColors')?.value(), 2, 1, 6));
const backColors = form.computedValue(() => clamp(printSection.integer('backColors')?.value(), 0, 0, 6));
const underbase = form.computedValue(() => printSection.checkbox('underbase')?.value() === true);
const locations = form.computedValue(() => backColors() > 0 ? 2 : 1);
const screens = form.computedValue(() => frontColors() + backColors() + (underbase() ? locations() : 0));
 
printSection.addRow(row => {
row.addTextPanel('screensInfo', {
computedValue: () => 'Screens to set up: ' + screens() + ' (' + money(printing.screenSetup) + ' each)'
+ (underbase() ? ', including ' + locations() + ' underbase screen' + (locations() > 1 ? 's' : '') : ''),
customStyles: hintStyles
});
});
 
// Step 3: Job services
const servicesPage = pages.addPage('services');
 
servicesPage.addRow(row => {
row.addTextPanel('servicesTitle', {
computedValue: () => 'Step 3 of 4: Job services',
customStyles: stepStyles
});
});
 
const servicesSection = servicesPage.addSubform('services', { title: 'Artwork and delivery', isCollapsible: false });
 
servicesSection.addRow(row => {
row.addCheckbox('artwork', {
label: 'Artwork cleanup (+' + money(jobServices.artworkFee) + ' per job)',
tooltip: 'One job-level charge. Complex redesigns are quoted after review.',
defaultValue: false
}, '1fr');
row.addCheckbox('rush', {
label: 'Rush production (+' + jobServices.rushPercent + '% on garments and printing)',
tooltip: 'An estimate surcharge, not a confirmed deadline. We check availability before accepting.',
defaultValue: false
}, '1fr');
});
 
servicesSection.addRow(row => {
row.addRadioButton('delivery', {
label: 'Delivery',
options: [
{ id: 'pickup', name: 'Shop pickup (free)' },
{ id: 'shipping', name: 'Ship to me (+' + money(jobServices.shippingFee) + ')' }
],
defaultValue: 'pickup',
orientation: 'vertical',
isRequired: true
});
});
 
const artworkRequested = form.computedValue(() => servicesSection.checkbox('artwork')?.value() === true);
const rushRequested = form.computedValue(() => servicesSection.checkbox('rush')?.value() === true);
const shipping = form.computedValue(() => servicesSection.radioButton('delivery')?.value() === 'shipping');
 
// Pricing
const discountPercent = form.computedValue(() => {
if (quantity() >= 100) return quantityBreaks.from100Percent;
if (quantity() >= 50) return quantityBreaks.from50Percent;
return 0;
});
const garmentAmount = form.computedValue(() => cents(quantity() * garmentPrice(garmentKey())));
const printingList = form.computedValue(() => cents(quantity() * (locations() * printing.firstColor + (screens() - locations()) * printing.extraColor)));
const printingDiscount = form.computedValue(() => cents(printingList() * discountPercent() / 100));
const printingAmount = form.computedValue(() => cents(printingList() - printingDiscount()));
const setupAmount = form.computedValue(() => cents(screens() * printing.screenSetup));
const artworkAmount = form.computedValue(() => artworkRequested() ? jobServices.artworkFee : 0);
const rushAmount = form.computedValue(() => rushRequested() ? cents((garmentAmount() + printingAmount()) * jobServices.rushPercent / 100) : 0);
const production = form.computedValue(() => cents(garmentAmount() + printingAmount() + setupAmount() + artworkAmount() + rushAmount()));
const minimumAdjustment = form.computedValue(() => cents(Math.max(0, jobServices.minimumOrder - production())));
const shippingAmount = form.computedValue(() => shipping() ? jobServices.shippingFee : 0);
const total = form.computedValue(() => cents(production() + minimumAdjustment() + shippingAmount()));
const perPiece = form.computedValue(() => cents(total() / quantity()));
 
const batchSpec = () => quantity() + ' x ' + garmentName(garmentKey()) + '; front ' + frontColors() + ' color' + (frontColors() === 1 ? '' : 's')
+ '; back ' + backColors() + ' color' + (backColors() === 1 ? '' : 's') + '; underbase ' + (underbase() ? 'at each location' : 'none');
 
const priceRows = (): { label: string; amount: number }[] => {
const rows: { label: string; amount: number }[] = [
{ label: 'Blank garments (' + quantity() + ' x ' + money(garmentPrice(garmentKey())) + ')', amount: garmentAmount() },
{ label: 'Printing before quantity break', amount: printingList() }
];
if (printingDiscount() > 0) rows.push({ label: 'Printing quantity break (' + discountPercent() + '%)', amount: -printingDiscount() });
rows.push({ label: 'Screen setup (' + screens() + ' screens)', amount: setupAmount() });
if (artworkAmount() > 0) rows.push({ label: 'Artwork cleanup', amount: artworkAmount() });
if (rushAmount() > 0) rows.push({ label: 'Rush production (' + jobServices.rushPercent + '%)', amount: rushAmount() });
if (minimumAdjustment() > 0) rows.push({ label: 'Minimum production adjustment', amount: minimumAdjustment() });
rows.push({ label: shipping() ? 'Shipping' : 'Pickup', amount: shippingAmount() });
return rows;
};
 
// Step 4: Your estimate
const resultPage = pages.addPage('result');
 
resultPage.addRow(row => {
row.addTextPanel('resultTitle', {
computedValue: () => 'Step 4 of 4: Your estimate',
customStyles: stepStyles
});
});
 
const summarySection = resultPage.addSubform('summary', { title: 'Your Estimate', isCollapsible: false });
 
summarySection.addRow(row => {
row.addPriceDisplay('total', {
label: 'Estimated order total',
computedValue: () => total(),
currency: '$',
decimals: 2,
variant: 'large'
}, '2fr');
row.addPriceDisplay('perPiece', {
label: 'Average per garment',
computedValue: () => perPiece(),
currency: '$',
decimals: 2,
variant: 'highlight'
}, '1fr');
});
 
summarySection.addRow(row => {
row.addTextPanel('totalNote', {
computedValue: () => 'Illustrative rates, before applicable tax; subject to artwork, stock and production review.',
customStyles: { 'font-size': '0.85rem', 'color': '#64748b', 'font-style': 'italic' }
});
});
 
const breakdownSection = resultPage.addSubform('breakdown', { title: 'What goes into the estimate', isCollapsible: false });
 
breakdownSection.addRow(row => {
row.addTextPanel('batchSpec', {
computedValue: () => 'Batch: ' + batchSpec(),
customStyles: hintStyles
});
});
 
breakdownSection.addRow(row => {
row.addPriceDisplay('garments', {
label: 'Blank garments',
computedValue: () => garmentAmount(),
currency: '$',
decimals: 2
}, '1fr');
row.addPriceDisplay('printing', {
label: () => discountPercent() > 0 ? 'Printing (' + discountPercent() + '% quantity break applied)' : 'Printing',
computedValue: () => printingAmount(),
currency: '$',
decimals: 2,
originalPrice: () => printingDiscount() > 0 ? printingList() : undefined
}, '1fr');
});
 
breakdownSection.addRow(row => {
row.addPriceDisplay('setup', {
label: () => 'Screen setup (' + screens() + ' screens)',
computedValue: () => setupAmount(),
currency: '$',
decimals: 2,
prefix: '+'
}, '1fr');
row.addPriceDisplay('artwork', {
label: 'Artwork cleanup',
computedValue: () => artworkAmount(),
currency: '$',
decimals: 2,
prefix: '+',
isVisible: () => artworkAmount() > 0
}, '1fr');
});
 
breakdownSection.addRow(row => {
row.addPriceDisplay('rush', {
label: 'Rush production',
computedValue: () => rushAmount(),
currency: '$',
decimals: 2,
prefix: '+',
isVisible: () => rushAmount() > 0
}, '1fr');
row.addPriceDisplay('minimum', {
label: 'Minimum production adjustment',
computedValue: () => minimumAdjustment(),
currency: '$',
decimals: 2,
prefix: '+',
isVisible: () => minimumAdjustment() > 0
}, '1fr');
row.addPriceDisplay('shipping', {
label: 'Shipping',
computedValue: () => shippingAmount(),
currency: '$',
decimals: 2,
prefix: '+',
isVisible: () => shippingAmount() > 0
}, '1fr');
});
 
breakdownSection.addRow(row => {
row.addTextPanel('minimumNote', {
computedValue: () => 'Our minimum production value is ' + money(jobServices.minimumOrder) + '; the adjustment adds only the difference needed to reach it.',
customStyles: hintStyles,
isVisible: () => minimumAdjustment() > 0
});
});
 
// Contact
const contactSection = resultPage.addSubform('contact', { title: 'Ask the shop to review this job', isCollapsible: false });
 
contactSection.addRow(row => {
row.addTextbox('name', {
label: 'Name',
placeholder: 'John Smith',
isRequired: true
}, '1fr');
row.addEmail('email', {
label: 'Email for your estimate',
placeholder: 'john@example.com',
isRequired: true
}, '1fr');
});
 
contactSection.addRow(row => {
row.addTextbox('company', {
label: 'Company or team (optional)',
placeholder: 'Acme Running Club'
}, '1fr');
row.addTextbox('phone', {
label: 'Phone (optional)',
placeholder: '(555) 123-4567'
}, '1fr');
});
 
contactSection.addRow(row => {
row.addTextarea('notes', {
label: 'Sizes, garment color and target date',
placeholder: 'Size mix, artwork notes, shipping destination and the date you need the garments.',
rows: 3,
maxLength: 1500
});
});
 
form.configureSubmitButton({
label: 'Request this estimate'
});
 
form.configureCompletionScreen({
type: 'text',
title: 'Your request is ready for review',
message: 'The shop confirms garment stock, artwork, sizes, tax and the production date before accepting an order. Reply to the estimate email with your artwork.'
});
 
form.configurePdf('estimate', pdf => {
pdf.configure({
filename: 'screen-printing-estimate',
pageSize: 'Letter',
allowUserDownload: true,
downloadButtonLabel: 'Download estimate (PDF)',
header: {
title: 'Screen printing estimate',
subtitle: 'One garment style and one shared design per batch'
},
footer: {
text: 'Illustrative estimate before applicable tax. Not an accepted order.',
showPageNumbers: true
}
});
 
pdf.addSection('Your batch', section => {
section.addText(batchSpec());
section.addRow(row => {
row.addField('Screens including underbase', String(screens()));
row.addField('Artwork', artworkRequested() ? 'Cleanup requested' : 'Ready for review');
});
section.addRow(row => {
row.addField('Production', rushRequested() ? 'Rush requested, subject to confirmation' : 'Standard');
row.addField('Delivery', shipping() ? 'Shipping' : 'Pickup');
});
});
 
pdf.addSection('Estimated cost', section => {
const rows = priceRows().map(row => [row.label, money(row.amount)]);
rows.push(['Estimated total before tax', money(total())]);
section.addTable(['Item', 'Amount'], rows);
section.addText('Average per garment: ' + money(perPiece()) + '. Includes setup and selected job services; unit average is rounded to cents.');
});
 
pdf.addSection('Contact', section => {
section.addRow(row => {
row.addField('Name', contactSection.textbox('name')?.value() || '');
row.addField('Email', contactSection.email('email')?.value() || '');
});
section.addRow(row => {
row.addField('Company', contactSection.textbox('company')?.value() || '-');
row.addField('Phone', contactSection.textbox('phone')?.value() || '-');
});
});
 
const notes = contactSection.textarea('notes')?.value();
if (notes) {
pdf.addSection('Job notes', section => section.addText(notes));
}
 
pdf.addSection('Before production', section => {
section.addText('Illustrative estimate, not an accepted order. The shop confirms artwork, size mix, garment availability, delivery, applicable tax and production timing. Send artwork by replying to the estimate email. Mixed styles, sleeve prints, personalization and special inks require a separate quote.');
});
});
}
 

Frequently Asked Questions

Can I use my own garment and printing rates?

Yes. Blank garment prices, the first-color and additional-color rates, the screen setup fee, quantity breaks, artwork cleanup, rush surcharge, minimum order and shipping are all constants at the top of the calculator code, so you change them in one place.

How are screens and underbases counted?

Every ink color at every print location is one screen. A back print adds a second location. The underbase for dark garments adds one screen and one printing pass at each active location, exactly as most shops charge it. You can change the rule in the code if your shop prices underbases differently.

Do quantity breaks reduce the whole order?

In this template the breaks apply to printing only: blank garments, screen setup and artwork are not discounted, and the 100-piece break replaces the 50-piece break instead of stacking with it. You can move the discount to the whole order with a one-line change.

Can I add more garment styles or print locations?

Yes. Add tank tops, long sleeves or caps to the garment list with their blank price, and add sleeve or pocket prints as extra locations. The calculator keeps one garment style and one design per batch; mixed jobs are best handled with a separate quote.

Does the customer receive something after submitting?

The customer can download a PDF estimate with the batch specification, the itemized cost and your terms, and you get the lead with their contact details and notes about sizes, garment color and target date, so you can confirm stock and artwork before accepting the order.