Word / Google Docs to Clean HTML

This free tool converts content copied from Microsoft Word or Google Docs into clean, semantic HTML. Paste your document, and the cleaner strips out MSO classes, Word-only tags, inline styles, endless nested spans, and empty paragraphs, then rebuilds proper headings, paragraphs, bold and italic tags, links, tables, and real bulleted and numbered lists. Get HTML that's ready to drop into WordPress or any CMS, with no signup and nothing uploaded to a server.

Copy from Word or Google Docs, then paste below with Ctrl+V (⌘V on Mac). Formatting is captured automatically. You can also paste raw HTML code.
2. Choose Your Options
Clean HTML Clear
No document handy?
0 Original Size
0 Clean Size
0% Smaller
- Source

What Was Cleaned

    Copied to clipboard!

    What Does the Word to HTML Cleaner Remove?

    Everything that controls how the text looked in Word or Google Docs, and nothing that controls what it means. The cleaner removes:

    • MSO classes and styles like class="MsoNormal", mso-list, mso-bidi-font-weight, and mso-spacerun.
    • Word-only tags like <o:p>, VML shapes, embedded XML, and conditional comments such as <!--[if gte mso 9]>.
    • Inline styles for fonts, sizes, colors, line heights, margins, and backgrounds.
    • Spans and font tags, including the one-span-per-word soup Google Docs produces.
    • Empty tags like <p>&nbsp;</p> spacer paragraphs and stray <br> tags between blocks.
    • Junk attributes like lang, dir, align, width, valign, Google Docs heading IDs, and Word bookmarks.
    • Hidden text, comments, invisible zero-width characters, and soft hyphens.

    What's left is the structure: headings, paragraphs, bold and italic text, links, lists, tables, block quotes, superscript, subscript, and images. If your text still has stray invisible characters or curly quotes you want to change, run it through our Whitespace / Invisible Character Stripper or Smart Quotes to Straight Quotes Converter.

    Why Is HTML From Word and Google Docs So Messy?

    Word's HTML is built to be pasted back into Word. When you copy from Word, it puts a version of your document on the clipboard that's designed so another copy of Word can rebuild it perfectly. That means every font, margin, and spacing setting rides along as MSO styles, and even bulleted lists aren't real lists. Each bullet is a paragraph with a fake bullet character tucked inside a hidden span. It's common for a one-page document to produce 30 to 50 KB of HTML for a few kilobytes of actual text.

    Google Docs wraps every run of text in its own styled span. Each change in formatting starts a new <span> with a long inline style that spells out the font, size, color, weight, and spacing. The whole paste is wrapped in a bold tag that's styled to not be bold, and links in Docs HTML exports are often routed through Google redirect URLs.

    That junk causes real problems when it lands in your CMS:

    • It fights your theme. Inline fonts, sizes, and colors override your site's CSS, so pasted posts look slightly off from everything else on the site.
    • It bloats your pages. Extra markup adds weight to every page load and makes the HTML harder to edit later.
    • It hides your structure. Fake lists and styled spans instead of real tags make it harder for screen readers and search engines to understand your content.

    What Is Semantic HTML, and Why Does It Matter?

    Semantic HTML uses tags that describe what content is, not just how it looks. A span styled with font-weight:700 looks bold, but a <strong> tag tells browsers, screen readers, and search engines that the text is important. A paragraph with a bullet character looks like a list, but a <ul> with <li> items is a list.

    This cleaner converts formatting into meaning wherever it can:

    • Bold styling becomes <strong>, and italic styling becomes <em>.
    • Word's fake list paragraphs become real, properly nested <ul> and <ol> lists, with the starting number kept.
    • Word's Title style becomes an <h1>, and its Quote styles become <blockquote>.
    • Superscript and subscript styling becomes <sup> and <sub>, and strikethrough becomes <s>.
    • Wrapper <div> tags are removed or turned into paragraphs.

    Real lists, tables, and headings are also easier for Google to understand and pull into featured snippets, and they let your theme style every post consistently.

    How Do I Paste the Clean HTML Into WordPress?

    There are three easy ways, depending on how you like to work:

    • Block editor, fastest: Click Copy Rich Text, then paste into an empty paragraph block. WordPress turns the headings, paragraphs, lists, and tables into separate blocks automatically.
    • Block editor, as code: Click Copy HTML, switch to the Code editor (Ctrl+Shift+Alt+M, or Cmd+Shift+Option+M on Mac), and paste.
    • Classic editor: Click Copy HTML, switch to the Text tab, and paste.

    Since WordPress uses your post title as the page's H1, turn on Shift headings down one level if your document starts with its own H1 or Title. Other platforms like Shopify, Webflow, HubSpot, and Squarespace all have an HTML or source view that accepts the same output.

    What Happens to My Images?

    From Word: When you copy from the Word desktop app, images are saved as temporary files on your own computer, and the HTML points to those local files (like file:///C:/Users/.../clip_image001.png). Nobody else can load them, so the cleaner removes them and tells you how many were dropped.

    From Google Docs: Images point to Google-hosted URLs that are tied to your document and aren't meant for permanent public use, so they can stop working later. The cleaner keeps them so you can see where they go, and flags them so you remember to swap them out.

    Either way, the right move is the same: download your images, upload them to your own media library, and add descriptive alt text. The cleaner also points out any images that are missing alt text.

    Why Did My Fonts, Colors, and Alignment Disappear?

    On purpose. Fonts, font sizes, colors, highlight colors, alignment, and spacing are presentation, and on a website, presentation belongs to your theme's stylesheet. When those settings come along as inline styles, they override your theme and make pasted posts look subtly different from the rest of your site.

    If a passage genuinely needs special treatment, like a centered call-out or a colored note, add it with your CMS's block settings or a CSS class after pasting. That way it stays consistent with your design and is easy to change site-wide later. And if you want to shrink the HTML even further for a static page or email, run the result through our HTML Minifier.

    Is My Document Uploaded Anywhere?

    No. The entire conversion runs in your browser. Your document is never sent to our server or anyone else's, nothing is saved, and there's no signup. Close the tab and it's gone.

    The tool is also built to be safe with untrusted HTML. Pasted content is processed in an isolated document where scripts can't run, only a strict list of tags and attributes makes it into the output, and links using dangerous schemes like javascript: are removed. The preview is shown in a sandboxed frame with scripts disabled.

    Let's Grow Your Business

    Want some free consulting? Let's hop on a call and talk about what we can do to help.

    ', '', '\x3C!--StartFragment-->', '

    Quarterly Content Plan

    ', '

    Our goal this quarter is to double organic traffic with fewer, better articles.

    ', '

     

    ', '

    Priorities

    ', '

    ·        Refresh our top 20 posts

    ', '

    o   Update stats and screenshots

    ', '

    ·        Publish two pillar pages

    ', '

     

    ', '

    1.     Audit existing content

    ', '

    2.     Build the editorial calendar

    ', '

    Content is king, but distribution is queen.

    ', '', '', '', '

    Month

    Posts

    October

    12

    ', '

     Questions? Email hello@example.com.

    ', '

    ', '\x3C!--EndFragment-->', '' ].join('\n'), gdocs: [ '', '

    How to Plan a Content Calendar

    ', '

    A good calendar is simple, visible, and easy to update.

    ', '
    ', '

    What to Include

    ', '', '
    ', '

    E = mc2  still applies to deadlines.

    ', '

    ', '

    ' ].join('') }; // ========================================================================= // HELPERS // ========================================================================= function wchParseStyle(text) { const style = new Map(); String(text || '').split(';').forEach(rule => { const colon = rule.indexOf(':'); if (colon > 0) style.set(rule.slice(0, colon).trim().toLowerCase(), rule.slice(colon + 1).trim().toLowerCase()); }); return style; } function wchUnwrap(el) { const parent = el.parentNode; if (!parent) return; while (el.firstChild) parent.insertBefore(el.firstChild, el); el.remove(); } function wchRename(el, tag) { const replacement = el.ownerDocument.createElement(tag); while (el.firstChild) replacement.appendChild(el.firstChild); el.replaceWith(replacement); return replacement; } function wchIsBlank(text) { return !/[^\s\u00A0]/.test(text); } // Only http(s), mailto, tel and relative links; images also allow inline PNG/JPEG/GIF/WebP data function wchSafeUrl(value, isImage) { const url = String(value || '').replace(/[\u0000-\u001F\u007F]/g, '').trim(); if (!url) return null; if (isImage) { return /^https?:\/\//i.test(url) || /^data:image\/(png|jpe?g|gif|webp);base64,[a-z0-9+\/=]+$/i.test(url) ? url : null; } const scheme = url.match(/^([a-z][a-z0-9+.\-]*):/i); if (scheme && !/^(https?|mailto|tel)$/i.test(scheme[1])) return null; return url; } // https://www.google.com/url?q=https://example.com&sa=D... -> https://example.com function wchUnwrapGoogleRedirect(url) { const match = url.match(/^https?:\/\/(?:www\.)?google\.[a-z.]+\/url\?(.*)$/i); if (!match) return null; const params = new URLSearchParams(match[1]); const target = params.get('q') || params.get('url'); return target && /^https?:\/\//i.test(target) ? target : null; } // Plain text can contain "<" too, so only real HTML tags or comments count as HTML const WCH_HTML_SIGNAL = /<\/?(html|head|body|meta|p|div|span|font|br|hr|h[1-6]|ul|ol|li|table|tr|td|th|a|b|i|u|s|strong|em|img|blockquote|pre|code|section|article|sup|sub)\b[^>]*>|'); const end = html.indexOf('\x3C!--EndFragment-->'); if (start !== -1 && end > start) html = html.slice(start + 20, end); // DOMParser documents are inert: no scripts run and no images load const doc = new DOMParser().parseFromString(html, 'text/html'); const body = doc.body; const ctx = { doc: doc, opts: opts, stats: stats }; wchInlineClassStyles(doc); if (opts.lists) wchWordLists(body, ctx); wchFixNestedLists(body); wchWalk(body, ctx); if (source === 'Word Online') wchMergeAdjacentLists(body); if (opts.unbold) body.querySelectorAll('h1 strong, h2 strong, h3 strong, h4 strong, h5 strong, h6 strong').forEach(wchUnwrap); body.querySelectorAll('strong strong, em em, u u, s s, sup sup, sub sub').forEach(wchUnwrap); wchMoveEdgeSpaces(body); wchMergeInline(body); if (opts.empty) wchRemoveEmpty(body, stats); wchRemoveEdgeBreaks(body); body.querySelectorAll('li, td, th').forEach(wchUnwrapLoneParagraph); wchWrapLooseInline(body); body.querySelectorAll('blockquote').forEach(wchWrapLooseInline); wchNormalizeText(body); const structure = { headings: body.querySelectorAll('h1, h2, h3, h4, h5, h6').length, paragraphs: body.querySelectorAll('p').length, lists: body.querySelectorAll('ul, ol').length - body.querySelectorAll('li ul, li ol').length, tables: body.querySelectorAll('table').length, links: body.querySelectorAll('a[href]').length, images: body.querySelectorAll('img').length }; return { html: wchSerialize(body, opts.minify), source: source, stats: stats, structure: structure }; } // ========================================================================= // RENDERING // ========================================================================= const wchEl = id => document.getElementById(id); let wchResult = null; const WCH_PREVIEW_CSS = 'body{font-family:Georgia,"Times New Roman",serif;font-size:17px;line-height:1.65;color:#222;margin:0;padding:20px 24px;}' + 'h1,h2,h3,h4,h5,h6{font-family:Arial,sans-serif;line-height:1.25;margin:1.2em 0 .5em;}' + 'h1:first-child,h2:first-child,h3:first-child,p:first-child{margin-top:0;}' + 'img{max-width:100%;height:auto;}table{border-collapse:collapse;margin:1em 0;}td,th{border:1px solid #ccc;padding:6px 10px;}' + 'blockquote{border-left:4px solid #2b579a;margin:1em 0;padding:.2em 1em;color:#555;}a{color:#1a0dab;pointer-events:none;}'; function wchEscHtml(text) { return String(text).replace(/&/g, '&').replace(//g, '>'); } function wchFormatBytes(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'; return (bytes / 1048576).toFixed(2) + ' MB'; } function wchByteLength(text) { return new Blob([text]).size; } function wchReadOptions() { const checked = id => wchEl('wch-opt-' + id).checked; return { lists: checked('lists'), empty: checked('empty'), nbsp: checked('nbsp'), links: checked('links'), images: checked('images'), unbold: checked('unbold'), underline: checked('underline'), shift: checked('shift'), minify: checked('minify') }; } // The preview runs in a sandboxed frame (no scripts) with a CSP that only allows images function wchRenderPreview(html) { wchEl('wch-preview').srcdoc = '' + '' + ' ' + html + ''; } function wchSizePreview() { const frame = wchEl('wch-preview'); try { // The body's own height; the document's scrollHeight never drops below the frame's current height const height = Math.ceil(frame.contentDocument.body.getBoundingClientRect().height); if (height) frame.style.height = Math.min(Math.max(height + 4, 200), 700) + 'px'; } catch (e) { // Leave the default height if the frame isn't readable } } function wchRenderSummary(r) { const s = r.stats; const plural = (n, word, many) => n + ' ' + (n === 1 ? word : (many || word + 's')); const cleaned = []; if (s.styles) cleaned.push('Removed ' + plural(s.styles, 'inline style attribute')); if (s.classes) cleaned.push('Removed ' + plural(s.classes, 'class attribute') + ', including MSO classes'); if (s.attrs) cleaned.push('Removed ' + plural(s.attrs, 'other attribute') + ' (IDs, lang, dir, alignment, widths)'); if (s.spans) cleaned.push('Unwrapped ' + plural(s.spans, ' or tag', ' and tags')); if (s.lists) cleaned.push('Rebuilt ' + plural(s.lists, 'Word list') + ' as real
      /
        (' + plural(s.listItems, 'item') + ')'); if (s.bold) cleaned.push('Converted ' + plural(s.bold, 'bold style') + ' to '); if (s.italic) cleaned.push('Converted ' + plural(s.italic, 'italic style') + ' to '); if (s.office) cleaned.push('Removed ' + plural(s.office, 'Word-only tag') + ' (, VML, XML)'); if (s.comments) cleaned.push('Removed ' + plural(s.comments, 'comment') + ' and conditional comments'); if (s.empty) cleaned.push('Removed ' + plural(s.empty, 'empty tag')); if (s.nbsp) cleaned.push('Replaced ' + plural(s.nbsp, 'non-breaking space') + ' ( )'); if (s.invisible) cleaned.push('Removed ' + plural(s.invisible, 'invisible character') + ' (zero-width spaces, soft hyphens)'); if (s.redirects) cleaned.push('Unwrapped ' + plural(s.redirects, 'Google redirect link') + ' to the real URL'); if (s.hidden) cleaned.push('Removed ' + plural(s.hidden, 'hidden element')); if (s.droppedLinks) cleaned.push('Removed ' + plural(s.droppedLinks, 'link') + ' (kept the link text)'); if (s.droppedImages) cleaned.push('Removed ' + plural(s.droppedImages, 'image')); if (!cleaned.length) cleaned.push('Nothing to clean. This HTML was already tidy.'); const warnings = []; if (s.localImages) warnings.push(plural(s.localImages, 'image was', 'images were') + ' saved only on your computer (Word clipboard files) and removed. Upload them to your CMS and add them back.'); if (s.tempImages) warnings.push(plural(s.tempImages, 'image uses', 'images use') + ' a Google-hosted URL that can stop working. Download and re-upload to your own media library.'); if (s.noAlt) warnings.push(plural(s.noAlt, 'image has', 'images have') + ' no alt text. Add a short description for accessibility and image SEO.'); if (s.unsafeLinks) warnings.push(plural(s.unsafeLinks, 'link', 'links') + ' used an unsafe or unsupported scheme (like javascript:) and were unwrapped.'); if (s.unsafeImages) warnings.push(plural(s.unsafeImages, 'image', 'images') + ' had an unsupported source and were removed.'); const icon = level => level === 'good' ? 'lucide-circle-check' : 'lucide-triangle-alert'; wchEl('wch-cleaned').innerHTML = cleaned.map(text => '
      1. ' + wchEscHtml(text) + '
      2. ').join(''); wchEl('wch-warnings').innerHTML = warnings.map(text => '
      3. ' + wchEscHtml(text) + '
      4. ').join(''); wchEl('wch-warnings-card').style.display = warnings.length ? 'block' : 'none'; const st = r.structure; const chips = [ [st.headings, 'heading'], [st.paragraphs, 'paragraph'], [st.lists, 'list'], [st.tables, 'table'], [st.links, 'link'], [st.images, 'image'] ].filter(c => c[0]).map(c => '' + plural(c[0], c[1]) + ''); wchEl('wch-structure').innerHTML = chips.join(''); } function wchRun() { const raw = wchEl('wch-input').value; wchEl('wch-error').textContent = ''; if (!raw.trim()) { wchEl('wch-error').textContent = 'Paste something from Word or Google Docs first.'; wchEl('wch-results').classList.remove('active'); return; } if (raw.length > WCH_MAX_INPUT) { wchEl('wch-error').textContent = 'That\'s more than 5 MB of HTML. Try cleaning the document in smaller sections.'; return; } const r = wchCleanHtml(raw, wchReadOptions()); wchResult = r; const before = wchByteLength(raw); const after = wchByteLength(r.html); wchEl('wch-stat-before').textContent = wchFormatBytes(before); wchEl('wch-stat-after').textContent = wchFormatBytes(after); wchEl('wch-stat-saved').textContent = before ? Math.max(0, Math.round((1 - after / before) * 100)) + '%' : '0%'; wchEl('wch-stat-source').textContent = r.source; wchEl('wch-detected').textContent = 'Captured ' + wchFormatBytes(before) + ' from ' + r.source; wchEl('wch-output').value = r.html; wchRenderPreview(r.html); wchRenderSummary(r); wchEl('wch-results').classList.add('active'); } // ========================================================================= // EVENTS // ========================================================================= function wchToast(message, isError) { const toast = wchEl('wch-toast'); toast.textContent = message; toast.classList.toggle('error', !!isError); toast.classList.add('show'); setTimeout(() => toast.classList.remove('show'), 2000); } function wchCopyText(text) { if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(() => wchToast('Copied to clipboard!'), () => wchToast('Copy failed. Select the HTML and copy it manually.', true)); return; } const ta = wchEl('wch-output'); ta.select(); document.execCommand('copy'); wchToast('Copied to clipboard!'); } // Puts formatted text on the clipboard so it pastes as headings, lists and links (e.g. into the WordPress block editor) function wchCopyRich() { if (!wchResult) return; let plain = ''; try { plain = wchEl('wch-preview').contentDocument.body.innerText; } catch (e) { plain = ''; } const item = new ClipboardItem({ 'text/html': new Blob([wchResult.html], { type: 'text/html' }), 'text/plain': new Blob([plain || wchResult.html], { type: 'text/plain' }) }); navigator.clipboard.write([item]).then(() => wchToast('Copied as rich text!'), () => wchToast('Your browser blocked rich text copying. Use Copy HTML instead.', true)); } function wchDownload() { if (!wchResult) return; const blob = new Blob([wchResult.html + '\n'], { type: 'text/html' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'clean-content.html'; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); } function wchShowPanel(name) { document.querySelectorAll('#wch-tool .wch-tab').forEach(tab => { const active = tab.getAttribute('data-panel') === name; tab.classList.toggle('active', active); tab.setAttribute('aria-selected', active ? 'true' : 'false'); }); document.querySelectorAll('#wch-tool .wch-panel').forEach(panel => { panel.classList.toggle('active', panel.getAttribute('data-panel') === name); }); if (name === 'preview') wchSizePreview(); } const wchInput = wchEl('wch-input'); wchInput.addEventListener('paste', e => { const data = e.clipboardData; if (!data) return; const html = data.getData('text/html'); const text = data.getData('text/plain'); if (html && !wchLooksLikeCode(text)) { // Formatted content: keep the HTML source instead of the plain text the textarea would insert e.preventDefault(); if (html.length > WCH_MAX_INPUT) { wchEl('wch-error').textContent = 'That\'s more than 5 MB of HTML. Try cleaning the document in smaller sections.'; return; } wchInput.value = html; wchRun(); } else { setTimeout(wchRun, 0); } }); wchEl('wch-clean-btn').addEventListener('click', e => { e.preventDefault(); wchRun(); }); wchEl('wch-clear-btn').addEventListener('click', e => { e.preventDefault(); wchInput.value = ''; wchResult = null; wchEl('wch-error').textContent = ''; wchEl('wch-detected').textContent = ''; wchEl('wch-results').classList.remove('active'); wchInput.focus(); }); document.querySelectorAll('#wch-tool .wch-sample').forEach(button => { button.addEventListener('click', () => { wchInput.value = WCH_SAMPLES[button.getAttribute('data-sample')]; wchRun(); }); }); document.querySelectorAll('#wch-tool .wch-options input').forEach(box => { box.addEventListener('change', () => { if (wchInput.value.trim()) wchRun(); }); }); document.querySelectorAll('#wch-tool .wch-tab').forEach(tab => { tab.addEventListener('click', () => wchShowPanel(tab.getAttribute('data-panel'))); }); wchEl('wch-preview').addEventListener('load', wchSizePreview); wchEl('wch-copy-btn').addEventListener('click', () => { if (wchResult) wchCopyText(wchResult.html); }); wchEl('wch-download-btn').addEventListener('click', wchDownload); if (window.ClipboardItem && navigator.clipboard && navigator.clipboard.write) { wchEl('wch-copy-rich-btn').style.display = ''; wchEl('wch-copy-rich-btn').addEventListener('click', wchCopyRich); } })();