Certificate Generator - Free Online Tool - Small Study Tools
100% Free · No Signup · Browser-Based

Certificate Generator

Design a certificate with 11 templates, drag any element to reposition it, add QR verification, and export print-ready PDF or PNG. Pro Mode adds bulk generation from CSV, custom fonts, dual signatures, and more.

100% Private — everything is generated in your browser
Pro Version is Free for now
✓ All templates · Custom fonts · Backgrounds ·
QR verification · Bulk CSV generation · 300 DPI unlocked
Day Night
🔤 Font & Style
34
0
16
✅ Verification
Certificate ID
QR Code
📑 Saved / Duplicates
📝 Certificate Text
📅 Date & Signature
Two Signatures (left & right)
Signature
Right Signature
🖼️ Logo
🏢
50
🖌️ Colors
Scroll for more options
🖼️ Certificate Preview
📊 Bulk Generation

Your sheet needs a Name column (and an optional Reason column), exactly like this:

NameReason
Maria Gomezfor excellence in mathematics
Alex Riverafor perfect attendance
Sana Khanfor outstanding leadership
`); w.document.close(); setTimeout(() => w.print(), 500); toast('Sent to print! ⭐ Bookmark SmallStudyTools.com for future use — new free tools every week.', 5000); }// ── EXPORT: PDF ─────────────────────────────────────────────── // Fixed: (1) the correct @pdf-lib/fontkit build is now loaded, and font files // are fetched as WOFF (parseable by fontkit) instead of the WOFF2 that // Google's CSS API returns — so the chosen fonts genuinely embed instead of // silently falling back to Helvetica; (2) the PDF uses the SAME y-up layout // numbers as the preview (stack, alignment, drag offsets, letter spacing), // so the download finally matches the screen. function hexToRgb(hex) { const c = hex.replace('#',''); return rgb(parseInt(c.slice(0,2),16)/255, parseInt(c.slice(2,4),16)/255, parseInt(c.slice(4,6),16)/255); } const QR_DARK_RGB = rgb(17/255, 24/255, 39/255); async function embedRealFont(pdfDoc, fontKey, weight, italic, useCustom) { try { if (typeof fontkit !== 'undefined' && pdfDoc.registerFontkit) pdfDoc.registerFontkit(fontkit); if (useCustom && customFontBytes) { const bytes = await (await fetch(customFontBytes)).arrayBuffer(); return await pdfDoc.embedFont(bytes, { subset: true }); } if (fontKey === 'georgia' || fontKey === 'timesNewRoman') { const sf = weight >= 600 ? (italic ? StandardFonts.TimesRomanBoldItalic : StandardFonts.TimesRomanBold) : (italic ? StandardFonts.TimesRomanItalic : StandardFonts.TimesRoman); return await pdfDoc.embedFont(sf); } const bytes = await fetchFontBytes(fontKey, weight, italic); if (!bytes) throw new Error('no font bytes'); return await pdfDoc.embedFont(bytes, { subset: true }); } catch (e) { toast('Could not embed the selected font — using a close fallback in the PDF'); return await pdfDoc.embedFont(weight >= 600 ? StandardFonts.HelveticaBold : StandardFonts.Helvetica); } } async function embedImgIfAny(pdfDoc, dataUrl) { if (!dataUrl) return null; try { const bytes = await (await fetch(dataUrl)).arrayBuffer(); if (dataUrl.includes('image/png')) return await pdfDoc.embedPng(bytes); return await pdfDoc.embedJpg(bytes); } catch (e) { return null; } } function trackedWidth(font, text, size, tracking) { if (!tracking) return font.widthOfTextAtSize(text, size); let w = 0; for (const ch of text) w += font.widthOfTextAtSize(ch, size); return w + tracking * Math.max(0, text.length - 1); } function drawTrackedText(page, text, opts) { if (!opts.tracking) { page.drawText(text, opts); return; } let x = opts.x; for (const ch of text) { page.drawText(ch, { x, y: opts.y, size: opts.size, font: opts.font, color: opts.color, opacity: opts.opacity }); x += opts.font.widthOfTextAtSize(ch, opts.size) + opts.tracking; } } async function buildCertPdfDoc(nameOverride, reasonOverride) { const theme = getActiveTheme(); const qrOn = isPro && qrEnabled; const L = computeLayout(currentPageSize, orientation, dualSignature, qrOn); const f = getFields(); if (nameOverride) f.name = nameOverride; if (reasonOverride !== undefined && reasonOverride !== '') f.reason = reasonOverride;const pdfDoc = await PDFDocument.create(); if (typeof fontkit !== 'undefined' && pdfDoc.registerFontkit) pdfDoc.registerFontkit(fontkit); // Body text is DM Sans in the preview — embed the real thing (silent // fallback to Helvetica if the fetch fails, e.g. offline). let bodyFont; try { const b = await fetchFontBytes('dmSans', 400, false); bodyFont = b ? await pdfDoc.embedFont(b, { subset: true }) : await pdfDoc.embedFont(StandardFonts.Helvetica); } catch (e) { bodyFont = await pdfDoc.embedFont(StandardFonts.Helvetica); } const titleFont = await embedRealFont(pdfDoc, theme.titleFontKey, 700, false, false); const nameFont = await embedRealFont(pdfDoc, theme.nameFontKey, nameBold ? 700 : 500, nameItalic, !!customFontName);const page = pdfDoc.addPage([L.page.w, L.page.h]); const textColor = hexToRgb(theme.text), accentColor = hexToRgb(theme.accent), borderColor = hexToRgb(theme.borderColor);page.drawRectangle({ x:0, y:0, width:L.page.w, height:L.page.h, color: hexToRgb(theme.bg) });// background image const bgImg = await embedImgIfAny(pdfDoc, bgImageDataUrl); if (bgImg) page.drawImage(bgImg, { x: 0, y: 0, width: L.page.w, height: L.page.h, opacity: 0.14 });// border const b = BORDER_STYLES[currentBorder]; const m = 14; if (b.style === 'single') page.drawRectangle({ x:m, y:m, width:L.page.w-2*m, height:L.page.h-2*m, borderColor, borderWidth: b.thin?0.75:1.6, borderDashArray: b.dash ? b.dash.split(',').map(Number) : undefined }); else if (b.style === 'double') { page.drawRectangle({ x:m, y:m, width:L.page.w-2*m, height:L.page.h-2*m, borderColor, borderWidth:1.6, borderDashArray: b.dash ? b.dash.split(',').map(Number) : undefined }); page.drawRectangle({ x:m+7, y:m+7, width:L.page.w-2*(m+7), height:L.page.h-2*(m+7), borderColor, borderWidth:1 }); } else if (b.style === 'triple') { [0,7,14].forEach((off2,i) => page.drawRectangle({ x:m+off2, y:m+off2, width:L.page.w-2*(m+off2), height:L.page.h-2*(m+off2), borderColor, borderWidth:i===1?0.8:1.6 })); [[m+3,m+3],[L.page.w-m-3,m+3],[m+3,L.page.h-m-3],[L.page.w-m-3,L.page.h-m-3]].forEach(([x,y]) => { page.drawCircle({ x, y, size: 3, color: borderColor }); }); }// logo (same position preset + drag offset as preview) const logoImg = await embedImgIfAny(pdfDoc, logoDataUrl); if (logoImg) { let logoX = L.logo.x; if (logoPosition === 'top-left') logoX = L.contentX; else if (logoPosition === 'top-right') logoX = L.contentX + L.contentW - L.logo.w; const o = off('logo'); page.drawImage(logoImg, { x: logoX + o.dx, y: L.logo.y - o.dy, width: L.logo.w, height: L.logo.h }); }// text stack — identical math to the preview const nameText = truncate(f.name, 40); const requestedNameSize = parseInt(fv('nameSizeRange')) || 34; const tracking = parseFloat(fv('letterSpacingRange')) || 0; let nameSize = requestedNameSize; const maxNameWidth = L.contentW - 24; while (nameSize > 18 && trackedWidth(nameFont, nameText, nameSize, tracking) > maxNameWidth) nameSize -= 1; const lineGap = parseFloat(fv('lineHeightRange')) || 16; const stack = computeTextStack(L, nameSize, lineGap / 16);// title const titleText = truncate(f.title, 50); const titleW = titleFont.widthOfTextAtSize(titleText, 22); const to = off('title'); page.drawText(titleText, { x: pdfAlignedX(L, titleW) + to.dx, y: stack.titleY - to.dy, size: 22, font: titleFont, color: textColor });// name const nameW = trackedWidth(nameFont, nameText, nameSize, tracking); const no = off('name'); drawTrackedText(page, nameText, { x: pdfAlignedX(L, nameW) + no.dx, y: stack.nameY - no.dy, size: nameSize, font: nameFont, color: accentColor, tracking });// date const dateText = formatDate(f.date); if (dateText) { const dateW = bodyFont.widthOfTextAtSize(dateText, 10); const dofs = off('date'); page.drawText(dateText, { x: pdfAlignedX(L, dateW) + dofs.dx, y: stack.dateY - dofs.dy, size: 10, font: bodyFont, color: textColor, opacity: 0.75 }); }// reason / issuer if (f.reason) { const t = truncate(f.reason, 80), w = bodyFont.widthOfTextAtSize(t, 12), o = off('reason'); page.drawText(t, { x: pdfAlignedX(L, w) + o.dx, y: stack.subtitleY - o.dy, size: 12, font: bodyFont, color: textColor, opacity: 0.85 }); } if (f.issuer) { const t = truncate(f.issuer, 60), w = bodyFont.widthOfTextAtSize(t, 10.5), o = off('issuer'); page.drawText(t, { x: pdfAlignedX(L, w) + o.dx, y: stack.issuerY - o.dy, size: 10.5, font: bodyFont, color: textColor, opacity: 0.65 }); }// seal const sealColor = overrideSealColor || theme.accent; const so = off('seal'); drawSealPdf(page, L.badge.x + so.dx, L.badge.y - so.dy, L.badge.r, hexToRgb(sealColor), currentSeal);// signature(s): script text or image above the line, designation below const sig1 = getSig(1, f), sig2 = dualSignature && L.sig2 ? getSig(2, f) : null; let sigFont = null; if ((sig1.mode === 'text' && sig1.text) || (sig2 && sig2.mode === 'text' && sig2.text)) { try { const sb = await fetchFontBytes(SIG_SCRIPT_FONT_KEY, 400, false); sigFont = sb ? await pdfDoc.embedFont(sb, { subset: true }) : bodyFont; } catch (e) { sigFont = bodyFont; } } const sigImg1 = sig1.mode === 'image' ? await embedImgIfAny(pdfDoc, sig1.image) : null; const sigImg2 = sig2 && sig2.mode === 'image' ? await embedImgIfAny(pdfDoc, sig2.image) : null; await drawSigPdf(page, L.sig, off('sig1'), bodyFont, textColor, sig1, sigFont, sigImg1); if (sig2) await drawSigPdf(page, L.sig2, off('sig2'), bodyFont, textColor, sig2, sigFont, sigImg2);// verification if (isPro && certIdEnabled) page.drawText(certId, { x: L.margin+4, y: 22, size: 8, font: bodyFont, color: textColor, opacity: 0.5 }); if (qrOn) { const qrSize = 34, qrX = L.cx - qrSize/2, qrY = 20; drawQrPdf(page, f.verifyUrl || 'https://smallstudytools.com', qrX, qrY, qrSize); }return pdfDoc; } async function drawSigPdf(page, sig, o, bodyFont, textColor, sigData, sigFont, sigImg) { const x = sig.x + o.dx, y = sig.y - o.dy; // above the line: signature image or typed script text if (sigData.mode === 'image' && sigImg) { const maxW = sig.w * 0.85, maxH = 36; const ratio = Math.min(maxW / sigImg.width, maxH / sigImg.height); const iw = sigImg.width * ratio, ih = sigImg.height * ratio; page.drawImage(sigImg, { x: x + (sig.w - iw) / 2, y: y + 3, width: iw, height: ih }); } else if (sigData.mode === 'text' && sigData.text && sigFont) { const st = truncate(sigData.text, 26); const sw = sigFont.widthOfTextAtSize(st, 19); page.drawText(st, { x: x + sig.w/2 - sw/2, y: y + 7, size: 19, font: sigFont, color: textColor }); } page.drawLine({ start:{x, y}, end:{x: x + sig.w, y}, thickness:1, color: textColor }); // below the line: designation const lbl = truncate(sigData.designation, 28); const lw = bodyFont.widthOfTextAtSize(lbl, 9); page.drawText(lbl, { x: x + sig.w/2 - lw/2, y: y - 14, size: 9, font: bodyFont, color: textColor, opacity: 0.75 }); } function drawSealPdf(page, cx, cy, r, colorRgb, sealKey) { if (sealKey === 'sunburst') { for (let i = 0; i < 18; i++) { const angle = (i / 18) * Math.PI * 2; const x1 = cx + Math.cos(angle) * (r - 2), y1 = cy + Math.sin(angle) * (r - 2); const x2 = cx + Math.cos(angle) * (r + 14), y2 = cy + Math.sin(angle) * (r + 14); page.drawLine({ start:{x:x1,y:y1}, end:{x:x2,y:y2}, thickness: 3.5, color: colorRgb, opacity: 0.85 }); } } else if (sealKey === 'laurel') { [-1, 1].forEach(side => { for (let i = 0; i < 6; i++) { const t = i / 5; const angle = Math.PI * 0.5 + t * Math.PI * 0.42; const leafR = r + 8 + t * 11; const x = cx + side * Math.cos(angle) * leafR; // preview fans leaves BELOW the medallion (y-down: cy + sin), so in // PDF y-up coordinates that is cy − sin, with the rotation mirrored const y = cy - Math.sin(angle) * leafR; const rotation = side * (angle * 180/Math.PI - 90) + 90; page.drawEllipse({ x, y, xScale: 7, yScale: 3.2, rotate: degrees(-rotation), color: colorRgb, opacity: 0.8 }); } }); } else if (sealKey === 'ribbon') { // tails hang DOWN below the medallion (y-up: below = smaller y) const tailW = r * 0.55; page.drawLine({ start:{x:cx-tailW*0.5,y:cy-r*0.3}, end:{x:cx-tailW*0.5,y:cy-r*1.55}, thickness: tailW, color: colorRgb, opacity: 0.85 }); page.drawLine({ start:{x:cx+tailW*0.5,y:cy-r*0.3}, end:{x:cx+tailW*0.5,y:cy-r*1.55}, thickness: tailW, color: colorRgb, opacity: 0.85 }); } page.drawCircle({ x: cx, y: cy, size: r, borderColor: colorRgb, borderWidth: 2.5 }); page.drawCircle({ x: cx, y: cy, size: r-7, borderColor: colorRgb, borderWidth: 1, opacity: 0.6 }); page.drawCircle({ x: cx, y: cy, size: r-16, color: colorRgb, opacity: 0.10 }); } function drawQrPdf(page, text, x, y, size) { const { count, modules } = buildQrMatrix(text); const cell = size / count; page.drawRectangle({ x, y, width: size, height: size, color: rgb(1,1,1) }); modules.forEach(([r,c]) => { page.drawRectangle({ x: x+c*cell, y: y+size-(r+1)*cell, width: cell+0.3, height: cell+0.3, color: QR_DARK_RGB }); }); } async function downloadPDF() { try { const pdfDoc = await buildCertPdfDoc(); const bytes = await pdfDoc.save(); triggerDownload(new Blob([bytes], { type:'application/pdf' }), 'smallstudytools-certificate.pdf'); toast('PDF downloaded! ⭐ Enjoying this tool? Bookmark SmallStudyTools.com and keep visiting — new free tools every week.', 5000); } catch (e) { toast('PDF generation failed — please try again'); } }// ── BULK GENERATION FROM CSV/EXCEL ─────────────────────────── function parseCSV(text) { const lines = text.split(/\r?\n/).filter(l => l.trim()); if (!lines.length) return []; const headers = lines[0].split(',').map(h => h.trim().toLowerCase()); const nameIdx = headers.indexOf('name'); const reasonIdx = headers.indexOf('reason'); if (nameIdx === -1) return []; return lines.slice(1).map(line => { const cols = line.split(',').map(c => c.trim()); return { name: cols[nameIdx] || '', reason: reasonIdx !== -1 ? (cols[reasonIdx] || '') : '' }; }).filter(r => r.name); } function parseExcelRows(rows) { if (!rows.length) return []; const headers = rows[0].map(h => String(h||'').trim().toLowerCase()); const nameIdx = headers.indexOf('name'); const reasonIdx = headers.indexOf('reason'); if (nameIdx === -1) return []; return rows.slice(1).map(row => ({ name: String(row[nameIdx] || '').trim(), reason: reasonIdx !== -1 ? String(row[reasonIdx] || '').trim() : '', })).filter(r => r.name); } function onBulkFileUpload(file) { if (!isPro) { proToast('Bulk generation'); return; } if (!file) return; const isExcel = /\.xlsx?$/i.test(file.name); const reader = new FileReader(); reader.onload = e => { try { if (isExcel) { const wb = XLSX.read(e.target.result, { type: 'array' }); const sheet = wb.Sheets[wb.SheetNames[0]]; const rows = XLSX.utils.sheet_to_json(sheet, { header: 1 }); bulkFileRows = parseExcelRows(rows); } else { bulkFileRows = parseCSV(e.target.result); } if (!bulkFileRows.length) toast('No valid rows found — the file needs a "Name" column (download the sample CSV to see the format)', 4500); updateBulkStatus(); } catch (err) { bulkFileRows = []; document.getElementById('bulkStatus').textContent = 'Could not read this file — check it matches the sample format.'; updateBulkButton(); } }; if (isExcel) reader.readAsArrayBuffer(file); else reader.readAsText(file); } // ── Manual bulk table ───────────────────────────────────────── function renderBulkTable() { const wrap = document.getElementById('bulkTable'); if (!wrap) return; wrap.innerHTML = bulkManualRows.map((r, i) => `
`).join(''); } function onBulkCell(i, field, value) { if (bulkManualRows[i]) { bulkManualRows[i][field] = value; updateBulkStatus(); } } function addBulkRow() { if (!isPro) { proToast('Bulk generation'); return; } bulkManualRows.push({ name: '', reason: '' }); renderBulkTable(); } function removeBulkRow(i) { bulkManualRows.splice(i, 1); if (!bulkManualRows.length) bulkManualRows.push({ name: '', reason: '' }); renderBulkTable(); updateBulkStatus(); } function getManualBulkRows() { return bulkManualRows.map(r => ({ name: r.name.trim(), reason: r.reason.trim() })).filter(r => r.name); } function getAllBulkRows() { return [...getManualBulkRows(), ...bulkFileRows]; } function updateBulkStatus() { const status = document.getElementById('bulkStatus'); const manual = getManualBulkRows().length, fromFile = bulkFileRows.length, total = manual + fromFile; if (status) { if (!total) status.textContent = ''; else { const parts = []; if (manual) parts.push(`${manual} typed`); if (fromFile) parts.push(`${fromFile} from file`); status.textContent = `${parts.join(' + ')} — ${total} certificate${total !== 1 ? 's' : ''} ready to generate.`; } } updateBulkButton(); } function updateBulkButton() { const btn = document.getElementById('bulkGenBtn'); if (btn) btn.disabled = getAllBulkRows().length === 0; } function downloadSampleCsv() { const csv = 'Name,Reason\r\nMaria Gomez,for excellence in mathematics\r\nAlex Rivera,for perfect attendance\r\nSana Khan,for outstanding leadership\r\n'; triggerDownload(new Blob([csv], { type: 'text/csv;charset=utf-8' }), 'certificate-names-sample.csv'); toast('Sample CSV downloaded — fill it with your names and upload it back!', 4000); } async function downloadBulkZip() { const rows = getAllBulkRows(); if (!rows.length) { toast('Type some names above or upload a CSV/Excel file first'); return; } const zip = new JSZip(); const status = document.getElementById('bulkStatus'); try { for (let i = 0; i < rows.length; i++) { const row = rows[i]; status.textContent = `Generating ${i+1} of ${rows.length}...`; if (certIdEnabled) certId = generateCertId(); const pdfDoc = await buildCertPdfDoc(row.name, row.reason); const bytes = await pdfDoc.save(); const safeName = row.name.replace(/[^a-z0-9]/gi, '_').slice(0, 40); zip.file(`smallstudytools-certificate-${safeName || (i+1)}.pdf`, bytes); } status.textContent = `Done — ${rows.length} certificates generated.`; const zipBlob = await zip.generateAsync({ type: 'blob' }); triggerDownload(zipBlob, 'smallstudytools-certificates-bulk.zip'); toast(`${rows.length} certificates downloaded! ⭐ Bookmark SmallStudyTools.com — more free tools every week.`, 5000); } catch (e) { status.textContent = 'Bulk generation failed — please try again.'; toast('Bulk generation failed'); } }// ── UNDO/REDO ───────────────────────────────────────────────── // (Renamed from `history` — that shadowed window.history.) function createHistoryStack(maxSize) { let stack = [], pointer = -1; return { push(state) { stack = stack.slice(0, pointer + 1); stack.push(JSON.parse(JSON.stringify(state))); if (stack.length > maxSize) stack.shift(); pointer = stack.length - 1; }, undo() { if (pointer > 0) { pointer--; return stack[pointer]; } return null; }, redo() { if (pointer < stack.length - 1) { pointer++; return stack[pointer]; } return null; }, canUndo: () => pointer > 0, canRedo: () => pointer < stack.length - 1, }; } const editHistory = createHistoryStack(60); let restoringState = false; function captureState() { return { title: fv('fieldTitle'), name: fv('fieldName'), reason: fv('fieldReason'), issuer: fv('fieldIssuer'), date: fv('fieldDate'), sig1: fv('fieldSig1'), sig2: fv('fieldSig2'), verifyUrl: fv('fieldVerifyUrl'), currentTemplate, currentPageSize, orientation, dualSignature, currentBorder, currentSeal, logoPosition, overrideAccent, overrideText, overrideBg, overrideBorderColor, overrideSealColor, overrideTitleFont, overrideNameFont, nameBold, nameItalic, textAlign, certIdEnabled, qrEnabled, certId, sig1Mode: sigModes[1], sig2Mode: sigModes[2], sig1SigText: fv('fieldSig1SigText'), sig2SigText: fv('fieldSig2SigText'), elementOffsets: JSON.parse(JSON.stringify(elementOffsets)), nameSize: fv('nameSizeRange'), letterSpacing: fv('letterSpacingRange'), lineHeight: fv('lineHeightRange'), logoSize: fv('logoSizeRange'), }; } function restoreState(s) { if (!s) return; restoringState = true; const setv = (id, val) => { const el = document.getElementById(id); if (el) el.value = val || ''; }; setv('fieldTitle', s.title); setv('fieldName', s.name); setv('fieldReason', s.reason); setv('fieldIssuer', s.issuer); setv('fieldDate', s.date); setv('fieldSig1', s.sig1); setv('fieldSig2', s.sig2); setv('fieldVerifyUrl', s.verifyUrl); setv('nameSizeRange', s.nameSize || 34); setv('letterSpacingRange', s.letterSpacing || 0); setv('lineHeightRange', s.lineHeight || 16); setv('logoSizeRange', s.logoSize || 50); currentTemplate = s.currentTemplate; currentPageSize = s.currentPageSize; orientation = s.orientation; dualSignature = s.dualSignature; currentBorder = s.currentBorder; currentSeal = s.currentSeal || 'sunburst'; logoPosition = s.logoPosition || 'top-center'; overrideAccent = s.overrideAccent; overrideText = s.overrideText; overrideBg = s.overrideBg; overrideBorderColor = s.overrideBorderColor; overrideSealColor = s.overrideSealColor; overrideTitleFont = s.overrideTitleFont; overrideNameFont = s.overrideNameFont; nameBold = s.nameBold; nameItalic = s.nameItalic; textAlign = s.textAlign; certIdEnabled = s.certIdEnabled; qrEnabled = s.qrEnabled; certId = s.certId; sigModes = { 1: s.sig1Mode || 'text', 2: s.sig2Mode || 'text' }; setv('fieldSig1SigText', s.sig1SigText); setv('fieldSig2SigText', s.sig2SigText); elementOffsets = s.elementOffsets ? JSON.parse(JSON.stringify(s.elementOffsets)) : {}; document.getElementById('dualSigToggle').checked = dualSignature; document.getElementById('certIdToggle').checked = certIdEnabled; document.getElementById('qrToggle').checked = qrEnabled; document.getElementById('nameSizeVal').textContent = fv('nameSizeRange'); document.getElementById('letterSpacingVal').textContent = fv('letterSpacingRange'); document.getElementById('lineHeightVal').textContent = fv('lineHeightRange'); document.getElementById('logoSizeVal').textContent = fv('logoSizeRange'); restoringState = false; renderAllUI(); } function pushHistory() { if (!restoringState) { editHistory.push(captureState()); updateUndoRedoButtons(); } } function updateUndoRedoButtons() { document.getElementById('undoBtn').disabled = !editHistory.canUndo(); document.getElementById('redoBtn').disabled = !editHistory.canRedo(); } function doUndo() { const s = editHistory.undo(); if (s) { restoreState(s); updateUndoRedoButtons(); toast('Undone'); } } function doRedo() { const s = editHistory.redo(); if (s) { restoreState(s); updateUndoRedoButtons(); toast('Redone'); } }// ── DUPLICATE / SAVE ────────────────────────────────────────── function duplicateCertificate() { const label = prompt('Name this saved certificate:', fv('fieldName') || 'My Certificate'); if (!label) return; savedCerts.unshift({ id: 'c'+Date.now(), label, state: captureState() }); if (savedCerts.length > 12) savedCerts.pop(); renderSavedList(); toast('Duplicated and saved!'); } function renderSavedList() { const wrap = document.getElementById('savedList'); if (!savedCerts.length) { wrap.innerHTML = '
No saved certificates yet.
'; return; } wrap.innerHTML = savedCerts.map(c => ` `).join(''); } function loadSaved(id) { const c = savedCerts.find(x => x.id === id); if (c) { restoreState(c.state); pushHistory(); toast(`Loaded "${c.label}"`); } } function deleteSaved(id) { savedCerts = savedCerts.filter(c => c.id !== id); renderSavedList(); }// ── SHARE ───────────────────────────────────────────────────── // On phones/modern browsers: native share sheet WITH the certificate image // attached (WhatsApp, Instagram, email — whatever the device offers). // Fallback everywhere else: a chip menu covering all major platforms. const SHARE_URL = 'https://smallstudytools.com/certificate-generator/'; const SHARE_TEXT = 'I made this certificate free with the SmallStudyTools Certificate Generator — 11 templates, QR verification, bulk generation, 100% private:'; async function shareCertificate() { try { const { svgStr, page } = await buildExportSvgString(); const canvas = await svgToCanvas(svgStr, page.w, page.h, 2); const blob = await new Promise(r => canvas.toBlob(r, 'image/png')); const file = new File([blob], 'certificate.png', { type: 'image/png' }); if (navigator.canShare && navigator.canShare({ files: [file] })) { await navigator.share({ files: [file], title: 'My Certificate', text: SHARE_TEXT, url: SHARE_URL }); toast('Shared! ⭐ Bookmark SmallStudyTools.com for future use.', 4500); return; } } catch (e) { if (e && e.name === 'AbortError') return; // user closed the share sheet } toggleShareMenu(); } // shares the TOOL PAGE itself (not a certificate image) — used by the // hero's "Share This Tool" button, reusing the same URL/text and the // same fallback menu as shareCertificate(), just without the image step async function sharePageTool() { if (navigator.share) { try { await navigator.share({ title: 'Certificate Generator — SmallStudyTools', text: SHARE_TEXT, url: SHARE_URL }); return; } catch (e) { if (e && e.name === 'AbortError') return; } } toggleShareMenu(); } function renderShareMenu() { const el = document.getElementById('shareMenu'); if (!el) return; const u = encodeURIComponent(SHARE_URL), t = encodeURIComponent(SHARE_TEXT); el.innerHTML = [ ``, ``, ``, ``, ``, ``, ``, ``, ``, ].join(''); } function toggleShareMenu() { const el = document.getElementById('shareMenu'); if (!el) return; document.getElementById('moreMenu')?.classList.remove('open'); el.classList.toggle('open'); } function renderMoreMenu() { const el = document.getElementById('moreMenu'); if (!el) return; el.innerHTML = ` `; } function toggleMoreMenu() { const el = document.getElementById('moreMenu'); if (!el) return; if (!el.innerHTML.trim()) renderMoreMenu(); document.getElementById('shareMenu')?.classList.remove('open'); el.classList.toggle('open'); } function copyShareLink() { navigator.clipboard.writeText(SHARE_URL) .then(() => toast('Link copied! Share it anywhere you like 🎉')) .catch(() => toast('Could not copy — long-press the address bar to copy the link')); } document.addEventListener('click', e => { const share = document.getElementById('shareMenu'); if (share && share.classList.contains('open') && !share.contains(e.target) && !e.target.closest('.ft-btn')) share.classList.remove('open'); const more = document.getElementById('moreMenu'); if (more && more.classList.contains('open') && !more.contains(e.target) && !e.target.closest('.ft-btn')) more.classList.remove('open'); });// ── MODE (Simple/Pro) ────────────────────────────────────────── function setMode(mode) { isPro = mode === 'pro'; document.getElementById('btnSimple').classList.toggle('active', !isPro); document.getElementById('btnPro').classList.toggle('active', isPro); document.body.classList.toggle('pro-mode', isPro); const pb = document.getElementById('proBar'); if (pb) pb.style.display = isPro ? 'flex' : 'none'; if (!isPro) { if (TEMPLATES[currentTemplate].pro) { currentTemplate = 'classicGold'; currentBorder = TEMPLATES.classicGold.border; } dualSignature = false; overrideBorderColor = null; document.getElementById('dualSigToggle').checked = false; } renderMoreMenu(); renderAllUI(); }// ── INIT ────────────────────────────────────────────────────── document.getElementById('fieldDate').value = new Date().toISOString().slice(0,10); renderMoreMenu();// hide the "scroll for more" hint once the panel is already scrolled to // the bottom, or if everything fits without scrolling in the first place function updatePanelScrollHint(){ const p = document.getElementById('leftPanel'); const hint = document.getElementById('panelScrollHint'); if(!p || !hint) return; const needsScroll = p.scrollHeight > p.clientHeight + 4; const atBottom = p.scrollTop + p.clientHeight >= p.scrollHeight - 4; hint.classList.toggle('hidden', !needsScroll || atBottom); } document.getElementById('leftPanel')?.addEventListener('scroll', updatePanelScrollHint); window.addEventListener('resize', updatePanelScrollHint); setTimeout(updatePanelScrollHint, 300);// ── favorite THIS page — uses the shared favorites engine directly // (SSTFavorites.toggle) since this is a standalone page, not a tool card // wrapped in a link the way the homepage sections are. The icon is baked // in as a fully self-contained snippet (same real icon + gradient shown // everywhere else this tool appears) so it renders correctly in the // drawer regardless of which page someone opens it from. ── const CERT_FAV_URL = 'https://smallstudytools.com/certificate-generator/'; function setPageFavLabel(isFav){ const label = document.getElementById('pageFavHeartLabel'); if(label) label.textContent = isFav ? 'Your Favorite' : 'Add to Favorites'; } function togglePageFavorite(){ if(typeof SSTFavorites === 'undefined') return; // shared engine not loaded on this page const icon = '
'; const nowFav = SSTFavorites.toggle(CERT_FAV_URL, {name:'Certificate Generator', url:CERT_FAV_URL, icon:icon}); const btn = document.getElementById('pageFavHeart'); if(btn){ btn.classList.toggle('active', nowFav); if(nowFav){ btn.classList.remove('active'); void btn.offsetWidth; btn.classList.add('active'); } } setPageFavLabel(nowFav); if(typeof SSTToast === 'function') SSTToast(nowFav ? '❤️ Added to favorites' : 'Removed from favorites'); } document.addEventListener('DOMContentLoaded', function(){ if(typeof SSTFavorites === 'undefined') return; const btn = document.getElementById('pageFavHeart'); const isFav = SSTFavorites.isFav(CERT_FAV_URL); if(btn && isFav) btn.classList.add('active'); setPageFavLabel(isFav); });// ── dark mode ──────────────────────────────────────────────── const DARK_MODE_KEY = 'sst_certgen_dark_mode'; const SUN_ICON = ''; const MOON_ICON = ''; function applyDarkMode(isDark){ document.body.classList.toggle('dark-mode', isDark); const icon = document.getElementById('themeToggleIcon'); if(icon) icon.innerHTML = isDark ? MOON_ICON : SUN_ICON; const dayLabel = document.getElementById('themeToggleDay'); const nightLabel = document.getElementById('themeToggleNight'); if(dayLabel) dayLabel.classList.toggle('active', !isDark); if(nightLabel) nightLabel.classList.toggle('active', isDark); } function toggleDarkMode(){ const isDark = !document.body.classList.contains('dark-mode'); applyDarkMode(isDark); try{ localStorage.setItem(DARK_MODE_KEY, isDark ? 'on' : 'off'); }catch(e){} } (function initDarkMode(){ let saved = null; try{ saved = localStorage.getItem(DARK_MODE_KEY); }catch(e){} const isDark = saved === 'on'; // always defaults to light unless the user has explicitly turned dark mode on themselves applyDarkMode(isDark); })();renderShareMenu(); renderAllUI(); initDrag(); pushHistory(); let _resizeT = null; window.addEventListener('resize', () => { clearTimeout(_resizeT); _resizeT = setTimeout(renderCert, 150); });

How to Use the Certificate Generator

From a blank page to a downloadable certificate in under a minute — no design experience, software, or account required. Just five quick steps.

1
Pick a Template
Choose from multiple ready-made designs, from classic and formal to modern and minimal.
2
Fill in the Details
Type in the name, reason, date, and issuer — or generate many certificates at once in bulk.
3
Customize the Look
Swap colors and fonts, upload your own logo, and drag any element exactly where you want it to sit.
4
Add Verification
Turn on a scannable QR code and a unique certificate ID so anyone can confirm it's genuine.
5
Download or Share
Export as a print-ready PDF or a PNG image, or share it directly — no watermark, ever.

Everything above runs entirely in your browser — nothing you type or upload is ever sent to a server, so your certificate stays completely private from start to finish.

Specifications
Price Free
Signup Not Required
Data Storage No
Watermark No
Platform Optimized for Web & Mobile
Category Design & Documents
Found a bug or something not working right? Let us know and we'll fix it — every report helps make this tool better.
Report an Issue

Free Certificate Generator — Design, Verify and Download in Minutes

An online certificate generator is a browser-based tool that turns a name, a reason and a date into a finished, printable certificate — no design software, template purchase, or account required. This one builds a real vector PDF with the fonts embedded, supports 11 templates, optional QR verification, and can generate an entire class or team's certificates at once from a spreadsheet.

Most "free" certificate tools online either watermark the download, lock the good templates behind a paywall, or export a flattened image instead of a real document. This one doesn't do any of that — everything runs client-side in your browser, the PDF is built from genuine vector text rather than a screenshot, and Pro Mode (currently free) unlocks bulk generation, verification, and print-grade export with no account and no card required.

1How the Generator Actually Works

The tool builds each certificate as a genuine vector PDF, not a rasterized image. Every letter is drawn as real, embedded font outlines rather than pixels, which is why the text stays sharp at any zoom level, stays selectable and copyable, and why the file prints cleanly on a professional press instead of just a home inkjet. Page geometry follows two real paper standards: A4 (210 × 297mm) under ISO 216, the standard used across most of the world, and US Letter (8.5 × 11in) under the ANSI standard used in North America — both available in landscape or portrait.

The optional QR code follows the ISO/IEC 18004 standard used for QR codes everywhere, encoding whatever verification URL you choose so a scan takes the viewer straight to a page confirming the certificate is genuine. Standard PNG export renders at roughly 2.5× the on-screen scale; Pro Mode's 300 DPI export matches the resolution generally required for professional offset and large-format printing.

Worked Example

A teacher exporting 30 end-of-term certificates uploads one CSV with a Name column (and an optional Reason column), picks a template once, and clicks generate. The tool produces 30 individually named, fully designed PDFs and downloads them together as one ZIP file — the entire class done in under a minute, instead of duplicating and retyping the same document 30 times.

2Who This Is Built For, and Why It Beats the Manual Route

This tool is built for anyone who needs a professional-looking certificate without hiring a designer: teachers and schools issuing achievement or completion awards, HR teams and corporate trainers recognizing course completion, event and workshop organizers, online course creators, and community or nonprofit leaders running volunteer or member-recognition programs.

The manual alternative — building a certificate in Word, PowerPoint or a general design tool — means starting from a blank page, manually centering text, hunting for a matching font pairing, and repeating that entire process by hand for every single recipient. This tool replaces that workflow with a live, drag-to-reposition editor and genuine bulk generation, so the difference between issuing one certificate and issuing fifty is a single CSV upload rather than fifty separate documents built one at a time.

It's also a reasonable alternative to paid certificate software for anyone who only needs this occasionally — a one-off award ceremony, a single course cohort, or a small team recognition doesn't justify a recurring subscription. Because everything runs locally in the browser, it's equally suited to sensitive use cases like student names or internal staff lists, where uploading that data to a third-party server isn't something every organization wants to do.

🎓
Achievement
Completion
🙌
Appreciation
🏅
Participation
📚
Training
💼
Employee Award
🧒
School & Kids
🏆
Custom Award

3How It Compares to the Alternatives

FeatureThis ToolWord / Slides TemplatePaid Certificate Software
CostFree, no watermarkFree, manual workOften a monthly fee
Bulk generation from CSVYes, one clickManual mail-mergeUsually yes
Real vector PDF outputYesDepends on exportUsually yes
QR / ID verificationYes, built inNot availableSometimes, as an add-on
Account or signupNot requiredDepends on platformAlmost always required
Data leaves your deviceNeverDepends on platformUsually, to their servers

4Frequently Asked Questions

Is this certificate generator really free?
Yes. Three templates, PDF/PNG/print export, logo upload, and typed or uploaded signatures are free for everyone, no watermark and no signup. Pro Mode — also free right now — adds 8 more templates, dual signatures, QR verification, custom fonts, 300 DPI export, and bulk generation.
How does bulk certificate generation actually work?
Upload a CSV or Excel file with a Name column and an optional Reason column, or type names directly into the built-in table. The tool generates one fully-designed PDF per person using your chosen template, then downloads all of them together as a single ZIP file — no repeated manual editing.
Will the downloaded PDF match what I see on screen?
Yes, exactly. The PDF is built from real vector text and shapes with the actual display fonts embedded directly in the file, rather than a flattened screenshot — so it prints sharp at any size, the text stays selectable, and nothing shifts between preview and download.
What are the QR code and certificate ID actually for?
They make a certificate independently verifiable. Pro Mode can stamp an auto-generated ID (like CERT-2026-4NVF00) on the certificate and print a scannable QR code that links to any verification URL you choose — useful for confirming a certificate is genuine without contacting the issuer directly.
Is my data uploaded anywhere when I use this tool?
No. Every name, logo, signature image and bulk list is processed entirely inside your browser — nothing is sent to a server. That also makes it safe to use for sensitive lists like student rosters or employee names.
What paper sizes and print resolutions are supported?
A4 and US Letter, each in landscape or portrait. Standard export renders at roughly 2.5x screen scale, and Pro Mode adds a 300 DPI PNG — the resolution generally used for professional offset and large-format printing.

Built on real, published standards — page sizes follow ISO 216 (A4) and the ANSI standard (US Letter), the QR code follows ISO/IEC 18004, and print export follows the 300 DPI convention used across the professional printing industry — not arbitrary defaults.

Lilly
Here to help you find a tool
Search tools Search blogs
Try me to find a tool! 👋