Jomatter

HTML Email Previewer

' + html.slice(idx2); } if (doctypeMatch) { var idx3 = doctypeMatch.index + doctypeMatch[0].length; return html.slice(0, idx3) + '' + charsetTag + metaTag + viewportTag + 'Email Preview' + html.slice(idx3) + ''; } return '' + charsetTag + metaTag + viewportTag + 'Email Preview' + html + ''; } /* ============================================================ Empty / preview state ============================================================ */ function showEmpty(show, message) { if (!emptyEl) { return; } if (show) { emptyEl.hidden = false; if (message) { emptyEl.textContent = message; } } else { emptyEl.hidden = true; } } function clearPreviewContent() { try { iframe.srcdoc = ''; } catch (e) { /* noop */ } } function updateFrameMode() { if (!frameWrap) { return; } if (state.view === 'mobile') { frameWrap.classList.add('is-mobile'); } else { frameWrap.classList.remove('is-mobile'); } for (var i = 0; i < segBtns.length; i++) { var b = segBtns[i]; var v = b.getAttribute('data-view'); b.setAttribute('aria-pressed', v === state.view ? 'true' : 'false'); } } /* ============================================================ Static HTML checks (using inert DOM parsing) ============================================================ */ function escapeForDisplay(text) { return String(text == null ? '' : text); } function runChecks(html) { var findings = []; if (!html || !html.trim()) { return findings; } var parser; var doc; try { parser = new DOMParser(); doc = parser.parseFromString(html, 'text/html'); } catch (e) { findings.push({ level: 'issue', title: 'Unable to parse HTML', detail: 'The supplied source could not be parsed for static checks.' }); return findings; } var lower = html.toLowerCase(); var isFullDoc = /]/.test(lower) || / 150000) { findings.push({ level: 'warn', title: 'Large HTML source (' + Math.round(html.length / 1024) + ' KB)', detail: 'Gmail clips messages over roughly 102 KB, which can hide the unsubscribe link and footer. Consider trimming inline styles or whitespace.' }); } else if (html.length > 102000) { findings.push({ level: 'warn', title: 'Source is near the Gmail clipping limit', detail: 'Gmail clips messages over roughly 102 KB. Consider reducing inline styles or whitespace.' }); } /* 2. Script elements */ var scripts = doc.querySelectorAll('script'); if (scripts.length) { findings.push({ level: 'issue', title: scripts.length + ' script element' + (scripts.length === 1 ? '' : 's'), detail: 'Scripts do not run in most email clients and are blocked in this preview. Remove script tags before sending.' }); } /* 3. Inline event handlers */ var handlerCount = 0; var handlerNames = {}; var allEls = doc.querySelectorAll('*'); for (var i = 0; i < allEls.length; i++) { var attrs = allEls[i].attributes; for (var a = 0; a < attrs.length; a++) { if (/^on[a-z]/i.test(attrs[a].name)) { handlerCount++; handlerNames[attrs[a].name.toLowerCase()] = true; } } } if (handlerCount) { var names = Object.keys(handlerNames).slice(0, 5).join(', '); findings.push({ level: 'issue', title: handlerCount + ' inline event handler attribute' + (handlerCount === 1 ? '' : 's'), detail: 'Found: ' + names + (handlerCount > 5 ? ', …' : '') + '. These do not execute in email clients and are blocked in this preview.' }); } /* 4. Form elements */ var forms = doc.querySelectorAll('form, input, select, textarea, button'); if (forms.length) { findings.push({ level: 'warn', title: forms.length + ' form-related element' + (forms.length === 1 ? '' : 's'), detail: 'Forms are unsupported in many email clients. Consider linking to a hosted form instead.' }); } /* 5. Embedded frames / objects */ var embeds = doc.querySelectorAll('iframe, object, embed'); if (embeds.length) { findings.push({ level: 'issue', title: embeds.length + ' embedded frame or object', detail: 'iframe, object, and embed elements are blocked by most email clients for security reasons.' }); } /* 6. Images */ var imgs = doc.querySelectorAll('img'); var missingAlt = 0, emptySrc = 0, noDim = 0, external = 0; for (var k = 0; k < imgs.length; k++) { var img = imgs[k]; if (!img.hasAttribute('alt')) { missingAlt++; } var src = (img.getAttribute('src') || '').trim(); if (!src) { emptySrc++; } if (!img.hasAttribute('width') && !img.hasAttribute('height')) { noDim++; } if (/^https?:/i.test(src)) { external++; } } if (missingAlt) { findings.push({ level: 'warn', title: missingAlt + ' image' + (missingAlt === 1 ? '' : 's') + ' missing alt text', detail: 'Add descriptive alt text for accessibility and for recipients who have images disabled.' }); } if (emptySrc) { findings.push({ level: 'warn', title: emptySrc + ' image' + (emptySrc === 1 ? '' : 's') + ' with empty src', detail: 'Empty image sources may render broken image icons or trigger network errors.' }); } if (noDim) { findings.push({ level: 'info', title: noDim + ' image' + (noDim === 1 ? '' : 's') + ' without width or height', detail: 'Explicit dimensions reduce layout shifts in some email clients.' }); } if (external) { findings.push({ level: 'info', title: external + ' external image URL' + (external === 1 ? '' : 's'), detail: 'Remote images are blocked by default in this preview. Enable "Allow Remote Images" to load them.' }); } /* 7. Links */ var links = doc.querySelectorAll('a'); var emptyHref = 0; for (var l = 0; l < links.length; l++) { var href = (links[l].getAttribute('href') || '').trim(); if (!href || href === '#') { emptyHref++; } } if (emptyHref) { findings.push({ level: 'warn', title: emptyHref + ' link' + (emptyHref === 1 ? '' : 's') + ' with missing or empty href', detail: 'Links without a destination can appear broken or inaccessible to recipients.' }); } /* 8. External stylesheets */ var extCss = doc.querySelectorAll('link[rel="stylesheet"]'); if (extCss.length) { findings.push({ level: 'warn', title: extCss.length + ' external stylesheet reference' + (extCss.length === 1 ? '' : 's'), detail: 'External stylesheets are blocked in this preview and are unreliable in many email clients. Prefer embedded or inline styles.' }); } /* 9. Wide fixed-width tables */ var wideTables = 0; var tables = doc.querySelectorAll('table'); for (var t = 0; t < tables.length; t++) { var w = tables[t].getAttribute('width') || ''; var styleAttr = (tables[t].getAttribute('style') || '').toLowerCase(); var m = styleAttr.match(/width\s*:\s*(\d+)\s*px/); if (m) { w = m[1]; } var n = parseInt(w, 10); if (isFinite(n) && n > 400) { wideTables++; } } if (wideTables) { findings.push({ level: 'warn', title: wideTables + ' fixed-width table' + (wideTables === 1 ? '' : 's') + ' wider than 400px', detail: 'Fixed-width tables can overflow on narrow mobile screens. Use max-width and width="100%" where possible.' }); } /* 10. Viewport meta (full docs only) */ if (isFullDoc) { var vp = doc.querySelector('meta[name="viewport"]'); if (!vp) { findings.push({ level: 'warn', title: 'Missing viewport meta tag', detail: 'Add for better mobile rendering.' }); } } /* 11. Responsive CSS */ if (!/@media/i.test(html)) { findings.push({ level: 'info', title: 'No media queries detected', detail: 'Responsive media queries improve mobile layout in clients that support them.' }); } return findings; } /* ============================================================ Render checks panel ============================================================ */ function renderChecks(findings, sourceEmpty) { if (!checksEl) { return; } while (checksEl.firstChild) { checksEl.removeChild(checksEl.firstChild); } if (sourceEmpty) { var emptyMsg = document.createElement('p'); emptyMsg.className = 'jm-email-checks-empty'; emptyMsg.textContent = 'Paste HTML to run static compatibility checks.'; checksEl.appendChild(emptyMsg); if (checksSummary) { checksSummary.textContent = 'Static analysis'; } return; } if (!findings.length) { var pass = document.createElement('div'); pass.className = 'jm-email-check-item is-pass'; var passBadge = document.createElement('span'); passBadge.className = 'jm-email-check-badge'; passBadge.textContent = 'Passed'; var passBody = document.createElement('div'); passBody.className = 'jm-email-check-body'; var passTitle = document.createElement('div'); passTitle.className = 'jm-email-check-title'; passTitle.textContent = 'No issues detected by the available checks'; var passDetail = document.createElement('p'); passDetail.className = 'jm-email-check-detail'; passDetail.textContent = 'The static checks found no common problems. Passing these checks does not guarantee compatibility across every email client.'; passBody.appendChild(passTitle); passBody.appendChild(passDetail); pass.appendChild(passBadge); pass.appendChild(passBody); checksEl.appendChild(pass); if (checksSummary) { checksSummary.textContent = 'No issues'; } return; } var issues = 0, warns = 0, infos = 0; for (var i = 0; i < findings.length; i++) { var f = findings[i]; if (f.level === 'issue') { issues++; } else if (f.level === 'warn') { warns++; } else { infos++; } var item = document.createElement('div'); item.className = 'jm-email-check-item is-' + f.level; var badge = document.createElement('span'); badge.className = 'jm-email-check-badge'; badge.textContent = f.level === 'issue' ? 'Issue' : (f.level === 'warn' ? 'Warning' : (f.level === 'info' ? 'Info' : 'Note')); var body = document.createElement('div'); body.className = 'jm-email-check-body'; var title = document.createElement('div'); title.className = 'jm-email-check-title'; title.textContent = f.title || ''; var detail = document.createElement('p'); detail.className = 'jm-email-check-detail'; detail.textContent = f.detail || ''; body.appendChild(title); body.appendChild(detail); item.appendChild(badge); item.appendChild(body); checksEl.appendChild(item); } if (checksSummary) { var parts = []; if (issues) { parts.push(issues + ' issue' + (issues === 1 ? '' : 's')); } if (warns) { parts.push(warns + ' warning' + (warns === 1 ? '' : 's')); } if (infos) { parts.push(infos + ' note' + (infos === 1 ? '' : 's')); } checksSummary.textContent = parts.length ? parts.join(' · ') : 'No issues'; } } /* ============================================================ Render preview ============================================================ */ function renderPreview(opts) { opts = opts || {}; if (autoTimer) { clearTimeout(autoTimer); autoTimer = null; } var html = sourceEl.value; if (!html || !html.trim()) { state.hasRendered = false; state.lastRenderedSource = ''; clearPreviewContent(); showEmpty(true, 'Paste HTML to preview your email.'); renderChecks([], true); setStatus('HTML source is empty.', ''); updateActionButtons(); return; } var doc; try { doc = buildPreviewDocument(html, state.allowRemote); } catch (e) { showEmpty(true, 'Unable to prepare the HTML preview.'); setStatus('Unable to prepare the HTML preview.', 'error'); return; } try { iframe.srcdoc = doc; } catch (e) { showEmpty(true, 'Unable to prepare the HTML preview.'); setStatus('Unable to prepare the HTML preview.', 'error'); return; } state.hasRendered = true; state.lastRenderedSource = html; showEmpty(false); var findings = runChecks(html); renderChecks(findings, false); updateActionButtons(); if (opts.silent) { // Called silently (e.g. auto-preview). Do not override a more specific status. if (!opts.keepStatus) { var baseMsg = 'Email preview updated successfully.'; if (state.allowRemote) { baseMsg += ' Remote images enabled. External image requests may reveal information to image hosts.'; } else { baseMsg += ' Remote images are blocked.'; } if (findings.length) { baseMsg += ' Potential email compatibility issues detected.'; } setStatus(baseMsg, findings.length ? 'warn' : 'success'); } } else { var msg = 'Email preview updated successfully.'; if (findings.length) { msg += ' Potential email compatibility issues detected.'; } setStatus(msg, findings.length ? 'warn' : 'success'); } } function markStale() { if (!state.hasRendered) { return; } setStatus('HTML source changed. The preview needs updating.', ''); } /* ============================================================ Action availability ============================================================ */ function updateActionButtons() { var hasSource = !!(sourceEl.value && sourceEl.value.trim()); if (copyBtn) { copyBtn.disabled = !hasSource; } if (downloadBtn) { downloadBtn.disabled = !hasSource; } } /* ============================================================ Debounced auto preview ============================================================ */ function scheduleAuto() { if (autoTimer) { clearTimeout(autoTimer); } autoTimer = setTimeout(function () { autoTimer = null; renderPreview({ silent: true }); }, 400); } function onSourceInput() { updateCount(); updateActionButtons(); if (state.auto) { scheduleAuto(); } else { markStale(); } } /* ============================================================ Clipboard helpers ============================================================ */ function fallbackCopy(text) { try { var ta = document.createElement('textarea'); ta.value = text; ta.setAttribute('readonly', 'readonly'); ta.style.position = 'fixed'; ta.style.top = '-1000px'; ta.style.left = '-1000px'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); ta.setSelectionRange(0, text.length); var ok = document.execCommand('copy'); document.body.removeChild(ta); return ok; } catch (e) { return false; } } function copyText(text, successMsg, failMsg) { if (!text) { setStatus(failMsg || 'Unable to copy the HTML source.', 'error'); return; } if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') { navigator.clipboard.writeText(text).then(function () { setStatus(successMsg, 'success'); }).catch(function () { if (fallbackCopy(text)) { setStatus(successMsg, 'success'); } else { setStatus(failMsg || 'Unable to copy the HTML source.', 'error'); } }); } else if (fallbackCopy(text)) { setStatus(successMsg, 'success'); } else { setStatus(failMsg || 'Unable to copy the HTML source.', 'error'); } } /* ============================================================ Example email (fictional, no network requests) ============================================================ */ function buildExample() { return [ '', '', '', '', '', 'Welcome to Jomatter', '', '', ' ', ' ', ' ', ' ', '
', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '
', ' JOMATTER', '
', '

Welcome aboard, Alex

', '

', ' Thanks for signing up. Here is a quick look at what you can do with your new account.', '

', '
', ' Jomatter dashboard preview', '
', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '
', ' Build faster', '

Use the ready-to-go templates to launch in minutes.

', '
', ' Stay organized', '

Keep all your projects in one place.

', '
', '
', ' Get Started', '
', '

', ' You received this email because you signed up for a Jomatter account.', '

', '

', ' Unsubscribe', ' · Jomatter Inc. · 100 Example Street', '

', '
', '
', '', '' ].join('\n'); } /* ============================================================ Event wiring — source ============================================================ */ sourceEl.addEventListener('input', onSourceInput); sourceEl.addEventListener('change', onSourceInput); /* ============================================================ Event wiring — buttons ============================================================ */ if (previewBtn) { previewBtn.addEventListener('click', function () { renderPreview(); }); } if (refreshBtn) { refreshBtn.addEventListener('click', function () { renderPreview(); }); } if (exampleBtn) { exampleBtn.addEventListener('click', function () { sourceEl.value = buildExample(); updateCount(); updateActionButtons(); if (state.auto) { renderPreview({ silent: true, keepStatus: true }); setStatus('Example loaded. Email preview updated successfully.', 'success'); } else { markStale(); setStatus('Example loaded. Click Preview Email to render it.', 'success'); } }); } if (pasteBtn) { pasteBtn.addEventListener('click', function () { if (!navigator.clipboard || typeof navigator.clipboard.readText !== 'function') { setStatus('Clipboard access was denied.', 'error'); return; } navigator.clipboard.readText().then(function (text) { if (typeof text !== 'string') { return; } sourceEl.value = text; updateCount(); updateActionButtons(); if (state.auto) { renderPreview({ silent: true, keepStatus: true }); setStatus('Pasted from clipboard. Email preview updated successfully.', 'success'); } else { markStale(); setStatus('Pasted from clipboard. Click Preview Email to render it.', 'success'); } }).catch(function () { setStatus('Clipboard access was denied.', 'error'); }); }); } if (clearBtn) { clearBtn.addEventListener('click', function () { if (autoTimer) { clearTimeout(autoTimer); autoTimer = null; } sourceEl.value = ''; updateCount(); updateActionButtons(); state.hasRendered = false; state.lastRenderedSource = null; clearPreviewContent(); showEmpty(true, 'Paste HTML to preview your email.'); renderChecks([], true); setStatus('Input cleared.', 'success'); }); } if (copyBtn) { copyBtn.addEventListener('click', function () { var text = sourceEl.value; if (!text || !text.trim()) { setStatus('HTML source is empty.', 'error'); return; } copyText(text, 'HTML copied successfully.', 'Unable to copy the HTML source.'); }); } if (downloadBtn) { downloadBtn.addEventListener('click', function () { var text = sourceEl.value; if (!text || !text.trim()) { setStatus('HTML source is empty.', 'error'); return; } var url = null; try { var blob = new Blob([text], { type: 'text/html;charset=utf-8' }); url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = 'email-preview.html'; a.style.display = 'none'; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(function () { if (url) { try { URL.revokeObjectURL(url); } catch (e) { /* noop */ } } }, 1500); setStatus('HTML file downloaded successfully.', 'success'); } catch (e) { if (url) { try { URL.revokeObjectURL(url); } catch (err) { /* noop */ } } setStatus('Unable to download the HTML file.', 'error'); } }); } if (autoCb) { autoCb.addEventListener('change', function () { state.auto = !!autoCb.checked; if (state.auto) { renderPreview({ silent: true, keepStatus: true }); setStatus('Auto Preview enabled. Email preview updated successfully.', 'success'); } else { if (state.hasRendered && state.lastRenderedSource !== sourceEl.value) { markStale(); } else { setStatus('Auto Preview disabled. Click Preview Email to refresh.', ''); } } }); } /* ============================================================ Event wiring — viewport toggle ============================================================ */ for (var s = 0; s < segBtns.length; s++) { (function (btn) { btn.addEventListener('click', function () { var v = btn.getAttribute('data-view'); if (!v || v === state.view) { return; } state.view = v; updateFrameMode(); setStatus('Preview mode: ' + (v === 'mobile' ? 'Mobile (375px)' : 'Desktop') + '.', ''); }); })(segBtns[s]); } /* ============================================================ Event wiring — remote images ============================================================ */ if (remoteCb) { remoteCb.addEventListener('change', function () { state.allowRemote = !!remoteCb.checked; if (noticeRemote) { if (state.allowRemote) { noticeRemote.textContent = 'Remote images enabled. External image requests may reveal your IP address and other information to image hosts, and may trigger tracking pixels.'; } else { noticeRemote.textContent = 'Remote images are blocked by default to help protect privacy and prevent tracking requests.'; } } if (state.hasRendered && sourceEl.value.trim()) { renderPreview({ silent: true, keepStatus: true }); if (state.allowRemote) { setStatus('Remote images enabled. External image requests may reveal information to image hosts.', 'warn'); } else { setStatus('Preview updated with remote images blocked.', 'success'); } } else { if (state.allowRemote) { setStatus('Remote images enabled. External image requests may reveal information to image hosts.', 'warn'); } else { setStatus('Preview updated with remote images blocked.', 'success'); } } }); } /* ============================================================ Initialization ============================================================ */ updateCount(); updateActionButtons(); updateFrameMode(); renderChecks([], true); showEmpty(true, 'Paste HTML to preview your email.'); setStatus('Paste HTML to preview your email.', ''); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();

All Developer Tools

Browse free online tools for JSON, HTML, CSS, XML, JavaScript, data conversion, and everyday development tasks.

50 tools