To make single source newlines read as spaces while blank lines start paragraphs, create paragraph structure before applying normal text wrapping. On a Mac, you can test this in a local browser page; CSS alone does not make that distinction.

This guide is for someone preparing English prose for a simple web page or Markdown preview. The task is to preserve readable source lines while producing flowing paragraphs that rewrap when the browser window changes width.

Why a CSS whitespace setting does not solve both requirements

Consider this synthetic source text. Each paragraph occupies two source lines, and an empty line separates the paragraphs:

Panel A introduces the layout
and describes the first section.

Panel B introduces another section
and describes the remaining space.

The intended result is two paragraphs. Within each paragraph, the source newline becomes a space. The browser then chooses visible line breaks according to the available width.

With white-space: normal, source newlines collapse along with other collapsible whitespace. That includes consecutive newlines: an empty source line does not create a paragraph. With white-space: pre-line, newline characters remain line breaks, including the single newlines you wanted to join. MDN documents these behaviors in its white-space property reference.

Setting applied to one text block Single source newline Empty source line Suitable for this task?
white-space: normal Collapses with whitespace Collapses too Only after paragraphs are created
white-space: pre-line Preserves a line break Preserves breaks No
white-space: pre-wrap Preserves a line break Preserves breaks No
Separate paragraph elements with white-space: normal Flows within each paragraph Represented by a paragraph boundary Yes

W3C describes whitespace processing as a rendering operation that leaves the underlying document data unchanged. Its segment-break rules also depend on surrounding text; replacing a newline with a space is appropriate for the English prose used here, rather than a universal transformation for all text. See CSS Text Module Level 3.

The practical solution therefore has two stages: recognize paragraphs in the source, then let the browser lay out their text. The WHATWG HTML Standard defines the p element as representing a paragraph. Its visible separation can be controlled independently through styling. See the p element.

Choose the path that matches your source

Use the following decision aid before changing anything. It separates source interpretation from appearance, which helps avoid fixing the wrong layer.

What you have What to do
Plain text containing only prose Split on empty lines, join single newlines inside each paragraph, and create paragraph elements
Markdown with headings, lists, or code blocks Use a Markdown parser and check its soft-break behavior
Existing HTML with separate paragraph elements Check the paragraphs’ whitespace and spacing styles
A page containing explicit line-break elements Correct the source or rendering configuration if those breaks are unintended
A page you can only view Use an available source or export workflow; the examples here require control of the document

Do not replace every newline with a space before identifying paragraphs: that removes the separators needed to distinguish them. Likewise, do not substitute repeated br elements for paragraph structure. WHATWG defines br as a line break and says it must not be used to separate thematic groups within a paragraph. See the br element.

Create a working local example with TextEdit

The following complete page requires TextEdit and a browser with JavaScript enabled. It contains an editable source field and a separate preview. It uses no external libraries.

  1. Open TextEdit and choose File > New, then Format > Make Plain Text.
  2. Paste the complete example below.
  3. Choose File > Save and name the file paragraph-demo.html.
  4. If prompted about the extension, choose Use .html.
  5. Open the saved file in Safari or another desktop browser.

Apple documents the plain-text creation and extension steps in Work with HTML documents in TextEdit on Mac. When reopening the file for editing, use TextEdit’s Display HTML files as HTML code instead of formatted text setting under TextEdit > Settings > Open and Save.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Paragraph flow demo</title>
  <style>
    body {
      max-width: 46rem;
      margin: 2rem auto;
      padding: 0 1rem;
      font: 1rem/1.6 system-ui, sans-serif;
    }
    label { display: block; }
    textarea {
      box-sizing: border-box;
      width: 100%;
      min-height: 12rem;
      font: 1rem/1.5 monospace;
    }
    #preview p {
      white-space: normal;
      margin: 0;
    }
    #preview p + p { margin-top: 1em; }
  </style>
</head>
<body>
  <h1>Paragraph flow demo</h1>
  <label for="source">Source text</label>
  <textarea id="source">Panel A introduces the layout
and describes the first section.

Panel B introduces another section
and describes the remaining space.</textarea>
  <h2>Preview</h2>
  <div id="preview"></div>
  <script>
    function toParagraphs(source) {
      return source.replace(/\r\n?/g, '\n')
        .split(/\n[ \t]*\n(?:[ \t]*\n)*/)
        .map(block => block
          .replace(/^[ \t\n]+|[ \t\n]+$/g, '')
          .replace(/[ \t]*\n[ \t]*/g, ' '))
        .filter(text => text.length > 0);
    }

    const source = document.getElementById('source');
    const preview = document.getElementById('preview');

    function render() {
      const paragraphs = toParagraphs(source.value).map(text => {
        const paragraph = document.createElement('p');
        paragraph.textContent = text;
        return paragraph;
      });
      preview.replaceChildren(...paragraphs);
    }

    source.addEventListener('input', render);
    render();
  </script>
</body>
</html>

The converter first normalizes carriage-return and CRLF line endings to LF. It then splits the text wherever a line is empty or contains only ordinary spaces and tabs. Within each resulting block, it replaces a remaining newline, plus adjacent spaces or tabs, with one ordinary space.

These are explicit rules for this example, not a general document parser. Several empty lines produce one paragraph boundary. Leading and trailing ordinary whitespace is removed. A blank input produces no paragraphs. The original source remains visible in the input field, so the transformation can be compared with it.

Each output paragraph receives its text through textContent. MDN documents that assigning this property replaces a node’s children with a text node. Here it is assigned only to newly created, empty paragraphs. See Node: textContent property.

The conversion function passed six deterministic checks in Node.js: ordinary paragraph input, CRLF input, carriage-return input, blank-only input, repeated empty lines, and preservation of a nonbreaking space. Browser rendering was not executed during preparation; use the visual checks below to verify it on your Mac.

Check paragraph boundaries and wrapping separately

Start with the supplied sample. The preview should contain Panel A introduces the layout and describes the first section. as one paragraph, followed by the corresponding Panel B paragraph. At a narrow window width, either paragraph may occupy several visible lines.

Now make these controlled edits:

  1. Replace the single newline after layout with a space. The preview’s paragraph structure should stay the same.
  2. Restore that newline and remove the empty line between the two blocks. The preview should become one paragraph.
  3. Add an empty line containing only spaces or tabs between the blocks. The preview should return to two paragraphs.
  4. Add several empty lines there. The gap should remain one paragraph gap.
  5. Resize the browser window. The paragraph boundary should remain fixed while the lines inside each paragraph rewrap.

This sequence distinguishes source boundaries from browser wrapping. In the example, paragraph spacing comes from the adjacent-paragraph margin rule. Changing that margin changes the visible gap without changing how the source is parsed.

If you use white-space: normal without this converter, a visual join does not mean the source newline was deleted. MDN explains that ordinary text-content whitespace remains in the DOM even when CSS collapses it during rendering. See Handling whitespace.

Use Markdown parsing for Markdown documents

Markdown already supplies the paragraph convention needed here. CommonMark defines a paragraph as consecutive nonblank lines that are not another block type; empty lines separate paragraphs. A normal newline within a paragraph is a soft break, which a renderer may output as a newline or a space. See CommonMark paragraphs and soft line breaks.

Paste the synthetic sample into your Markdown editor’s source view and inspect its preview. If every single newline produces a visible break, check the editor’s documented rendering options. Do not assume every Markdown application exposes the same setting.

For a project using the named library markdown-it, its documented breaks option defaults to false. Setting it to true converts paragraph newlines into br elements. Keeping it false supports the flowing-paragraph outcome, provided the output CSS also collapses whitespace. See MarkdownItOptions.

Explicit Markdown hard breaks still require attention. Under CommonMark, two or more trailing spaces or a trailing backslash can create a hard line break. Remove those markers only where they are unintended. The rules are documented under hard line breaks.

Do not run the plain-text converter over an entire Markdown document. It does not recognize headings, lists, fenced code, or indentation rules. Let a Markdown parser preserve those structures; apply whitespace styling to the resulting prose paragraphs.

Preserve browser and text-shaping boundaries

Keep each prose paragraph as continuous text wherever practical. The example creates one text node per paragraph and leaves line wrapping and glyph layout to the browser. It does not measure words or insert a separate element around every character.

That boundary matters for typography. W3C specifies circumstances in which shaping must continue across inline-element boundaries and circumstances in which it must break, including certain spacing and alignment changes. Avoid adding unnecessary inline boxes merely to control line positions. See Shaping Across Element Boundaries.

Use the converter only when every isolated source newline is an editorial wrap. If a newline intentionally divides labels, or falls inside a word, joining it with a space changes the content. The example also treats a line containing a nonbreaking space as content rather than an empty separator; MDN distinguishes nonbreaking spaces from ordinary collapsible document whitespace in Handling whitespace.

For English prose, establish paragraph boundaries first and allow normal browser wrapping inside them. The local example makes that rule inspectable: the source retains its editable lines, the preview contains real paragraphs, and resizing changes only the visible line wrapping.

Sources