The HTML code tag, the pre tag, and how to build a code block

<code> marks a piece of code inside a sentence. <pre> keeps spaces and line breaks exactly as typed. Put one inside the other and you have a code block.

Use <code> for a bit of code inside a sentence, such as a function name. Use <pre> when spaces and line breaks must stay exactly as typed.

For a block of code, put one inside the other: <pre><code>...</code></pre>. The <pre> keeps the layout, and the <code> says the content is code.

Try it first. Type some code with extra spaces and line breaks, and watch each version render it.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>code vs pre vs pre + code</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  label { font-size: 13px; font-weight: 600; }
  textarea {
    display: block; box-sizing: border-box; width: 100%; height: 86px; margin: 6px 0 8px;
    font: 13px/1.45 ui-monospace, Consolas, monospace; tab-size: 4;
    border: 1px solid #cfd4dc; border-radius: 8px; padding: 8px;
  }
  .opts { font-size: 13px; margin-bottom: 10px; }
  .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 10px; }
  .panel { background: #fff; border: 1px solid #e1e4ea; border-radius: 10px; padding: 10px; min-width: 0; }
  .panel h3 { margin: 0 0 6px; font: 700 13px ui-monospace, Consolas, monospace; color: #1d4ed8; }
  .panel p.out { margin: 0; font-size: 14px; }
  .panel pre { margin: 0; overflow-x: auto; font-size: 13px; line-height: 20px; }
  .count { margin-top: 8px; font-size: 12px; color: #5b6270; }
  code { background: #eef1f5; border-radius: 4px; padding: 1px 4px; }
  pre code { background: none; padding: 0; }  /* the pre box does the styling */
  pre.boxed { background: #1f2430; color: #e6e9ef; padding: 10px; border-radius: 8px; }
</style>
</head>
<body>
<label for="src">Type or paste some code (spaces, tabs and line breaks count):</label>
<textarea id="src" spellcheck="false"></textarea>
<div class="opts"><label><input type="checkbox" id="dots"> Show spaces as · and line ends as ↵</label></div>

<div class="grid">
  <div class="panel">
    <h3>&lt;code&gt;</h3>
    <p class="out">Run <code id="a"></code> now.</p>
    <div class="count" id="ca"></div>
  </div>
  <div class="panel">
    <h3>&lt;pre&gt;</h3>
    <pre id="b"></pre>
    <div class="count" id="cb"></div>
  </div>
  <div class="panel">
    <h3>&lt;pre&gt;&lt;code&gt;</h3>
    <pre class="boxed"><code id="c"></code></pre>
    <div class="count" id="cc"></div>
  </div>
</div>

<script>
  const src = document.getElementById('src');
  src.value = 'if (a < b && ok) {\n    total  =  a + b;\n}';

  function render() {
    const text = src.value;
    const dots = document.getElementById('dots').checked;
    // what an inline <code> keeps: each run of spaces, tabs and line breaks becomes one space
    const inline = text.replace(/\s+/g, ' ');
    const mark = (s) => s.replace(/ /g, '·').replace(/\t/g, '→').replace(/\n/g, '↵\n');

    // textContent, not innerHTML: < and & show up as characters, nothing is parsed
    document.getElementById('a').textContent = dots ? inline.replace(/ /g, '·') : text;
    document.getElementById('b').textContent = dots ? mark(text) : text;
    document.getElementById('c').textContent = dots ? mark(text) : text;

    // rendered lines = content height / line height (20px, set in the CSS)
    const count = (pre) => {
      const n = Math.round((pre.clientHeight - parseFloat(getComputedStyle(pre).paddingTop) * 2) / 20);
      return n + (n === 1 ? ' line' : ' lines');
    };
    const spaces = (s) => (s.match(/ /g) || []).length;
    document.getElementById('ca').textContent = 'Collapsed: ' + spaces(inline) + ' spaces kept of ' + spaces(text);
    document.getElementById('cb').textContent = count(document.getElementById('b')) + ', all ' + spaces(text) + ' spaces kept';
    document.getElementById('cc').textContent = count(document.querySelector('pre.boxed')) + ', same spacing as <pre>';
  }

  src.addEventListener('input', render);
  document.getElementById('dots').addEventListener('change', render);
  render();
</script>
</body>
</html>
Type in the box. The inline <code> collapses spaces and line breaks. Both <pre> versions keep them.

code, pre, and pre + code

The three versions differ in one thing: what happens to whitespace.

  • <code> is an inline element. It switches to a monospace font and changes nothing else. Runs of spaces, tabs and line breaks collapse into one space, as in any paragraph.
  • <pre> is a block element with white-space: pre by default. Every space and line break stays, and lines do not wrap.
  • <pre><code> looks the same as <pre> on its own, but it tells readers, search engines and tools that the block is code. Highlighting scripts and CSS rules usually target pre code.
<p>Call <code>render()</code> after the data loads.</p>

<pre><code>function render() {
  list.textContent = '';
}</code></pre>
Markup Whitespace Wraps long lines Use it for
<code> Collapsed Yes, like text A name or short snippet in a sentence
<pre> Kept No Any text where layout matters, such as ASCII art
<pre><code> Kept No A block of code

Escaping < and & inside code

<code> and <pre> do not switch off HTML parsing. A <div> written inside them is still read as a real tag, so it vanishes from the page instead of showing as text.

The parser turns <div> into an element. &lt;div&gt; shows the characters.
The parser turns <div> into an element. &lt;div&gt; shows the characters.

Two characters need escaping in code you paste into HTML:

  • < becomes &lt;
  • & becomes &amp;

> can stay as typed, because a tag only starts at <. The HTML entities guide lists the rest, and has an encoder you can paste code into.

If a script puts code on the page, set textContent instead of innerHTML. textContent never parses anything, so no escaping is needed. The first example above works that way.

The first line is blank: the newline after <pre>

The HTML parser drops a single line break that comes right after the <pre> start tag. That lets you start the code on the next line. The rule covers only <pre>, not the <code> inside it.

A line break after <code> stays, and shows as an empty first line.
A line break after <code> stays, and shows as an empty first line.

So write the first line of code directly after <code>, and close </code></pre> directly after the last line. Otherwise the block gets an empty line at the top, the bottom, or both.

kbd, samp and var

Three smaller tags sit next to <code>. Browsers show <kbd> and <samp> in a monospace font and <var> in italics.

Tag Meaning Example
<kbd> Something the user types or presses Press <kbd>Ctrl</kbd>+<kbd>C</kbd>
<samp> Output a program printed <samp>File not found</samp>
<var> A variable in maths or code The width is <var>w</var>

Long lines: scroll or wrap

A <pre> never wraps, so one long line makes the block wider than its box. On a phone, that can make the whole page scroll sideways. Switch between the options below and read the result under the code.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Long lines in a pre block</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  fieldset { border: 1px solid #d5d9e0; border-radius: 10px; padding: 8px 12px 10px; margin: 0 0 10px; background: #fff; }
  legend { font-size: 13px; font-weight: 700; padding: 0 4px; }
  fieldset label { display: inline-block; margin: 4px 14px 0 0; font-size: 13px; }
  .box { max-width: 520px; }

  pre {
    margin: 0; padding: 12px 14px; border-radius: 10px;
    background: #1f2430; color: #e6e9ef;
    font: 13px/1.5 ui-monospace, Consolas, monospace;
  }
  /* the three ways to handle a line that is wider than the box */
  pre.scroll { overflow-x: auto; }                               /* box scrolls, page does not */
  pre.wrap   { white-space: pre-wrap; overflow-wrap: anywhere; } /* long lines wrap */
  /* pre.none: nothing set, the line spills out of the box */

  #report { margin-top: 10px; font-size: 13px; padding: 8px 10px; border-radius: 8px; }
  .ok { background: #d6f2df; color: #0f5132; }
  .bad { background: #fde2da; color: #9a3412; }
</style>
</head>
<body>
<fieldset>
  <legend>Long lines</legend>
  <label><input type="radio" name="mode" value="scroll" checked> overflow-x: auto</label>
  <label><input type="radio" name="mode" value="wrap"> white-space: pre-wrap</label>
  <label><input type="radio" name="mode" value="none"> nothing</label>
</fieldset>
<fieldset>
  <legend>tab-size (the code is indented with tabs)</legend>
  <label><input type="radio" name="tab" value="8" checked> 8 (default)</label>
  <label><input type="radio" name="tab" value="4"> 4</label>
  <label><input type="radio" name="tab" value="2"> 2</label>
</fieldset>

<div class="box">
<pre id="code" class="scroll"><code>function load() {
	const url = "https://example.com/api/v2/reports/quarterly?region=emea&amp;format=json&amp;include=totals&amp;currency=eur&amp;page=1&amp;sort=desc";
	if (!url) {
		return null;
	}
	return url;
}</code></pre>
</div>
<div id="report"></div>

<script>
  const pre = document.getElementById('code');
  const report = document.getElementById('report');

  function update() {
    pre.className = document.querySelector('[name=mode]:checked').value;
    pre.style.tabSize = document.querySelector('[name=tab]:checked').value;

    // did the code push the page wider than the screen?
    const pageWider = document.documentElement.scrollWidth > document.documentElement.clientWidth;
    const boxScrolls = pre.scrollWidth > pre.clientWidth && getComputedStyle(pre).overflowX === 'auto';
    report.className = pageWider ? 'bad' : 'ok';
    report.textContent = pageWider
      ? 'The whole page now scrolls sideways. On a phone this drags the layout with it.'
      : boxScrolls
        ? 'Only the code box scrolls sideways. The page stays put.'
        : 'Every line fits. Long lines wrap inside the box.';
  }

  document.querySelectorAll('input').forEach((i) => i.addEventListener('change', update));
  update();
</script>
</body>
</html>
overflow-x: auto scrolls only the box. white-space: pre-wrap wraps the lines. tab-size sets how wide a tab is.
  • overflow-x: auto keeps each line intact and gives the box its own scrollbar. The page stays put. This suits code people will copy, because the line structure stays visible.
  • white-space: pre-wrap keeps spaces and line breaks but wraps long lines. Add overflow-wrap: anywhere as well, or an unbroken string such as a URL still sticks out.
pre { overflow-x: auto; }

/* or, to wrap instead */
pre { white-space: pre-wrap; overflow-wrap: anywhere; }

The same sideways-scroll problem shows up with wide tables. Making a table scroll uses the same wrapper idea.

tab-size

A tab character in <pre> is 8 spaces wide by default. Code indented with tabs then drifts far to the right. tab-size changes it:

pre { tab-size: 2; }

It changes only how wide a tab is drawn. The text still contains a tab, and a copy still pastes a tab.

A finished code block: file name, line numbers, copy button

This block adds four things to <pre><code>, with no external library.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Code block with line numbers and a copy button</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; }

  .code-block { margin: 0 0 12px; border-radius: 10px; overflow: hidden; background: #1f2430; }
  .code-block .bar {
    display: flex; align-items: center; justify-content: space-between;
    padding: 6px 8px 6px 14px; background: #2a3040; color: #aeb6c4;
    font: 12px ui-monospace, Consolas, monospace;
  }
  .code-block button {
    font: 600 12px system-ui, sans-serif; color: #e6e9ef; background: #3a4256;
    border: 0; border-radius: 6px; padding: 6px 10px; cursor: pointer;
  }
  .code-block button:hover { background: #4a5470; }
  .code-block pre {
    margin: 0; padding: 12px 0; overflow-x: auto;   /* long lines scroll inside the box */
    color: #e6e9ef; font: 13px/1.6 ui-monospace, Consolas, monospace; tab-size: 2;
    counter-reset: ln;                             /* line numbers start at 1 */
  }
  .code-block .line { display: block; padding-right: 14px; }
  .code-block .line::before {
    counter-increment: ln; content: counter(ln);   /* the number is CSS, not text */
    display: inline-block; width: 2.2em; margin-right: 14px; padding-right: 8px;
    text-align: right; color: #6b7386; border-right: 1px solid #3a4256;
    user-select: none;
  }
  .code-block .comment { color: #7fbf8e; font-style: italic; }
  .code-block .status { font: 12px system-ui, sans-serif; color: #f0c674; padding: 0 14px 10px; }
  .code-block .status:empty { display: none; }
</style>
</head>
<body>

<figure class="code-block">
  <div class="bar"><span>greet.js</span><button type="button">Copy</button></div>
<pre><code>// Say hello to everyone on the list
const names = ["Ada", "Grace", "Linus"];

for (const name of names) {
  if (name.length &lt; 5 &amp;&amp; name !== "") {
    console.log("Hi, " + name); // short names
  }
}</code></pre>
  <div class="status" role="status"></div>
</figure>

<figure class="code-block">
  <div class="bar"><span>page.html</span><button type="button">Copy</button></div>
<pre><code>&lt;!-- escape &lt; and &amp; inside code --&gt;
&lt;p&gt;Use &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; here.&lt;/p&gt;</code></pre>
  <div class="status" role="status"></div>
</figure>

<script>
  // matches // comments (after a space or at line start) and <!-- --> comments
  const COMMENT = /(^|\s)(\/\/.*|<!--.*?-->)/;

  document.querySelectorAll('.code-block').forEach((block) => {
    const code = block.querySelector('code');
    const source = code.textContent;          // keep the plain text for copying

    // rebuild the code as one span per line, using textContent only (no innerHTML)
    code.textContent = '';
    source.split('\n').forEach((text) => {
      const line = document.createElement('span');
      line.className = 'line';
      const m = text.match(COMMENT);
      if (m) {
        const at = m.index + m[1].length;
        line.append(text.slice(0, at));
        const c = document.createElement('span');
        c.className = 'comment';
        c.textContent = text.slice(at, at + m[2].length);
        line.append(c, text.slice(at + m[2].length));
      } else {
        line.textContent = text || ' ';     // keep empty lines one line tall
      }
      code.append(line);
    });

    const button = block.querySelector('button');
    const status = block.querySelector('.status');
    button.addEventListener('click', async () => {
      try {
        await navigator.clipboard.writeText(source);   // copies the code, never the numbers
        button.textContent = 'Copied';
        status.textContent = '';
      } catch {
        // clipboard blocked here: select the code so Ctrl+C / Cmd+C works
        getSelection().selectAllChildren(code);
        button.textContent = 'Copy';
        status.textContent = 'Copying is blocked on this page. The code is selected: press Ctrl+C (Cmd+C on a Mac).';
      }
      setTimeout(() => { button.textContent = 'Copy'; }, 1500);
    });
  });
</script>
</body>
</html>
Line numbers come from a CSS counter, so Copy never picks them up. Comments are coloured with one small regular expression.
  1. A header with the file name and a Copy button, in a <figure> around the <pre>.
  2. Line numbers: the script splits the code into one <span class="line"> per line. A CSS counter numbers them in ::before, so the numbers are not part of the text.
  3. Comment colour: a short regular expression finds // and <!-- --> comments on each line. The script builds the spans with textContent, never innerHTML, so nothing in the code can run as HTML.
  4. Copy: the script keeps the original text and passes it to navigator.clipboard.writeText(). If the page is not allowed to write to the clipboard, it selects the code and asks the reader to press Ctrl+C.
Typed numbers get copied with the code. Numbers drawn by a CSS counter do not.
Typed numbers get copied with the code. Numbers drawn by a CSS counter do not.
pre { counter-reset: ln; }
.line { display: block; }
.line::before {
  counter-increment: ln;
  content: counter(ln);
  user-select: none;
}

The HTML copy button guide covers the clipboard rules in more depth. Full syntax highlighting for many languages is a bigger job, and libraries exist for it. They work on the same <pre><code> markup, so you can add one later without changing the HTML.

For the font itself, coding fonts compares monospace options you can use in pre.

When it does not work

What you see Cause Fix
Part of the code disappears An unescaped < was read as a tag Write &lt; and &amp;, or set textContent from a script
Indentation and line breaks are gone The code is in <code> or <p>, not <pre> Wrap it in <pre><code>
The page scrolls sideways on a phone A long line in <pre> is wider than the screen overflow-x: auto or white-space: pre-wrap on the pre
An empty line at the top of the block A line break right after <code> Start the code on the same line as <pre><code>
Tab-indented code is far to the right Tabs are 8 spaces wide by default tab-size: 2 or 4
Copy also copies the line numbers The numbers are typed into the text Draw them with a CSS counter in ::before
Copy does nothing The page may not write to the clipboard Select the code and ask for Ctrl+C, as in the example

A code block is meant to be copied, and a screenshot of code cannot be. An .html file sent as an attachment may open as raw source on the other side.

If an AI chat gave you the page, ChatGPT HTML code preview explains why the chat's preview can differ from the real page.

To send the working version, paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its scripts run, so the people you send it to can scroll the code and press Copy themselves. If you change the code later, the same link shows the new version.

Questions people ask

What is the difference between the code tag and the pre tag?

<code> says "this text is code" and shows it in a monospace font, but it keeps the normal HTML whitespace rules, so extra spaces and line breaks collapse. <pre> keeps every space and line break as typed. For a multi-line code sample, use both: <pre><code>...</code></pre>.

Why does my HTML code disappear inside a code tag?

The browser reads a < inside <code> as the start of a real tag, the same as anywhere else. Write &lt; instead of < and &amp; instead of &, and the characters show as text.

Do I need to escape the > character?

Not in normal text. The parser only starts a tag at <, so > can stay as typed. Many people escape it as &gt; anyway for symmetry, which is also fine.

How do I stop a pre block from making the page scroll sideways on a phone?

Give the pre element overflow-x: auto so only the code box scrolls, or white-space: pre-wrap so long lines wrap. Add overflow-wrap: anywhere with pre-wrap if the code contains long strings with no spaces, such as URLs.

Do I need a library for syntax highlighting?

Not for a readable code block. Monospace text, a background, line numbers and a copy button go a long way. Highlighting libraries exist when you want full colouring for many languages, and they work on the same pre and code markup shown here.

Keep reading