How to Convert HTML Into Clean Plain Text Safely
Converting HTML to plain text is four jobs, not one: remove the tags, throw away the contents of <script> and <style> elements, put line breaks where <br> and block elements used to be, and decode character references such as & into the characters they stand for. Deleting everything between angle brackets does only the first job, and does it unreliably.
The quick route is to paste the markup into the Text Cleaner, choose the Plain Text preset and press Clean Text. The rest of this guide covers what that conversion has to get right, why a one-line regex does not, and what “safely” means when the HTML came from somewhere you do not control.
Why deleting tags with a regex fails
The pattern most people try first is <[^>]*>: a <, any run of characters that are not >, then a >. Replace every match with nothing and the tags appear to be gone. It holds up on tidy snippets and breaks on ordinary markup.
| Input | What is left | What went wrong |
|---|---|---|
<a title="size>10" href="/x">big</a> |
10" href="/x">big |
A > is legal inside a quoted attribute value, and the pattern stops at the first one it meets. |
<!-- hide if a>b -->Done |
b -->Done |
A comment ends at -->, not at the first >. |
<style>p{color:red}</style>Hi |
p{color:red}Hi |
The tags go, but the CSS between them stays behind as if it were text. Script code does the same. |
Scores <50 or >90 need review |
Scores 90 need review |
Two comparison signs in prose look like one long tag, and half the sentence disappears. |
Fish & chips |
Fish & chips |
Character references are not tags, so nothing decodes them. |
<p>One</p><p>Two</p> |
OneTwo |
The tags were the only thing separating the paragraphs. |
A longer pattern can patch some of these cases, but HTML has more: unquoted attribute values, attributes with no value, a tag cut off at the end of a fragment. The HTML Standard defines tag recognition as a state machine, because whether a > ends a tag depends on what came before it. A reliable converter tracks the same states.
What a correct conversion does
- Recognises markup by context. A
<begins markup only when a letter,/,!or?comes straight after it. Followed by a space or a digit, it is a less-than sign and stays in the text. Inside a tag, a quoted attribute value runs to its closing quote, whatever it contains. - Drops script and style content, and comments. That is code and notes for the browser, never visible on the page.
- Turns structure into line breaks. A
<br>is a line break, and paragraphs, headings, list items,<div>blocks and table rows each start on a new line when rendered. Inline tags such as<strong>,<em>and<a>are removed with nothing in their place, becauseun<b>believ</b>ableis still one word. - Decodes character references once, after the tags are gone. Named references (
&,<, ,—) and numeric ones in decimal (’) or hexadecimal (’) become the characters they name. The order matters:<br>in the source is text that a browser displays as<br>, and decoding it before stripping tags would delete it. Decoding twice is also wrong, since&lt;should come out as<. - Tidies the whitespace that is left. A browser normally collapses runs of spaces, tabs and line breaks in the source into one space, so a converter has to collapse indentation too. And
decodes to a no-break space (U+00A0), which looks like a space but does not match one in a search; the guide to the non-breaking space has the details.
Safety: extracting text is not sanitising
Two tasks get mixed up here. Converting to text produces a string with no markup in it; shown as text, it cannot run anything. Sanitising keeps some HTML (bold, links, lists) for display and removes the dangerous parts. That is a harder problem with dedicated libraries of its own, and a tag stripper is not a substitute for one. This guide and the tool deal only with the first task, and two rules still apply.
Do not use a live page to read untrusted HTML. A tempting shortcut in browser code is to create an element, assign the HTML to its innerHTML and read back textContent. That assignment hands the markup to the HTML parser of the page you are on. A <script> inserted this way does not run, but other markup can still execute code. MDN’s own example is an <img> with an invalid src and an onerror attribute: the image fails to load and the handler runs.
Keep the result as text. Correct decoding turns <b> into the characters <b>, so the output can contain angle brackets. If a program later puts the result into a web page, it has to insert it as text, for example through textContent, and never as HTML.
The safe ways to convert are a text-only scanner, which reads the markup as characters and never builds elements from it, or an inert parser. In browser code DOMParser is the inert option: parseFromString() parses the markup into a separate in-memory document in which scripts are marked non-executable and event handlers are not called. Read the text out of that document and never move its nodes into the visible page, where they could run. textContent includes the contents of <script> and <style> and puts no line breaks between blocks, so those two steps still need handling.
Converting with the Text Cleaner
The two options that do the work are in the Markup card under the Input Text box.
- Strip HTML tags uses a text-only scanner. It never builds or runs the pasted HTML. It removes tags and comments, drops the contents of
<script>and<style>, and turns structure into line breaks: headings, paragraphs, lists and tables are set apart by one blank line, while<br>, list items,<div>blocks and table rows start a new line. Table cells are separated by a tab. A<that does not begin a tag, as ina < bor<50, is left alone. - Decode HTML entities runs once, after the tags are gone. It handles named entities such as
&, ,—and’, and numeric ones such as’and’. It requires the closing semicolon, soAT&Tis not touched, and a name it does not recognise is left as it is.
The Plain Text preset turns both on, together with the whitespace cleanup of General Clean (non-breaking spaces to ordinary spaces, trailing spaces trimmed, repeated spaces collapsed, runs of blank lines reduced to one) and the removal of zero-width spaces and byte order marks. Everything runs in your browser, and the markup is not uploaded.
Put through that preset one at a time, the six inputs in the table above come out as big, Done, Hi, the complete Scores <50 or >90 need review, Fish & chips, and One and Two as separate paragraphs.
Three behaviours are worth knowing:
- Source layout. When the markup contains structural tags, whitespace is treated as a browser treats it: line breaks and indentation in the source collapse to single spaces, and the tags decide where lines break. With inline tags only, such as
<b>or<span>, your original line breaks and spacing are kept; tick Trim each line if indentation remains. - Anything shaped like a tag is removed. A placeholder such as
<your-name>and the type inList<String>both disappear. Look for these before you convert. An address in angle brackets, such as<https://example.com>or<name@example.com>, is not shaped like a tag and is kept. - Markdown markers go too. The preset also removes Markdown heading, bold/italic and code markers. To keep asterisks, underscores or backticks, choose General Clean instead and tick only Strip HTML tags and Decode HTML entities. Removing Markdown formatting describes what those options touch.
A worked example
Before
<h2>Shipping & returns</h2>
<p>Orders over $50 ship free.<br>Allow five working days — longer in December.</p>
<ul>
<li>Returns: 30 days</li>
<li>Refunds aren’t automatic. See the <a href="https://example.com/refunds">refund page</a>.</li>
</ul>
<script>track("shipping-note");</script>
After
Shipping & returns
Orders over $50 ship free.
Allow five working days — longer in December.
Returns: 30 days
Refunds aren’t automatic. See the refund page.
That is the Plain Text preset with nothing else ticked. The heading, the paragraph and the list are separated by one blank line each, and the <br> and the list items became single line breaks. All four entities were decoded, and the non-breaking space before the price was then converted to an ordinary space. The script line has gone. The em dash and the curly apostrophe remain, because the Typography options stay off unless you tick them. The change summary reads “15 HTML tags stripped”, “4 HTML entities decoded” and “1 non-breaking space converted”.
What plain text cannot keep
- Tables. Each row becomes a line with a tab between cells. Tab-separated lines usually paste into a spreadsheet as columns, but in a plain text field the columns no longer line up.
- Link addresses and image descriptions. The link text stays, as “refund page” does above, but the URL lived in the
hrefattribute and disappears with the tag. Images vanish along with theiralttext. Copy the addresses you need before converting. - List numbering. The numbers of an ordered list are generated by the browser and are not in the source, so the items come out as plain lines.
- Layout and emphasis. Columns, alignment, colours, heading levels, bold and italics have no plain-text equivalent. Lines inside
<pre>are kept, but the whitespace cleanup still collapses repeated spaces within a line, so check code samples and aligned text. - Visibility. Text hidden by a style sheet is still in the source, and a scanner that does not apply CSS includes it.
None of this is needed when the page is simply open in a browser: select the visible text, copy it and paste it as plain text, which works for the reason given in plain text versus rich text. And if what you are fighting is styling that arrived through the clipboard rather than literal tags in the text, see how to remove formatting from text.
Sources
- HTML Standard: The HTML syntax — WHATWG
- Element: innerHTML property — MDN Web Docs
- DOMParser: parseFromString() method — MDN Web Docs
- Node: textContent property — MDN Web Docs
- white-space CSS property — MDN Web Docs
This guide was drafted with AI assistance and reviewed by the publisher. See the editorial policy for how guides are written and corrected.