Highlight.js in a single HTML file

highlight.js is a JavaScript library that colours the code inside pre and code tags. Link a theme, load the script and call hljs.highlightAll(). Nothing to install.

highlight.js is an open-source syntax highlighter written in JavaScript. On a web page it finds the code inside <pre><code> tags and colours keywords, strings and comments.

In a single HTML file you need three things: a theme stylesheet, one script tag and one call to hljs.highlightAll().

There is nothing to install and no build step. Here is the whole thing. Edit the code inside a block and the example reruns.

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>highlight.js in one HTML file</title>
<!-- 1. a theme: without it the code gets classes but no colours -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.12.0/styles/github.min.css">
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f6f7f9; color: #1d2330; }
  h2 { font-size: 14px; margin: 0 0 6px; color: #4b5563; }
  pre { margin: 0 0 16px; border: 1px solid #d9dde3; border-radius: 8px; overflow: auto; }
  pre code.hljs { font-size: 14px; line-height: 1.5; }
</style>
</head>
<body>

<h2>JavaScript</h2>
<pre><code class="language-javascript">// say hello after one second
function greet(name) {
  const msg = `Hello, ${name}!`;
  setTimeout(() => console.log(msg), 1000);
  return msg.length;
}</code></pre>

<h2>HTML (written with &amp;lt; and &amp;gt;)</h2>
<pre><code class="language-html">&lt;button class="buy" onclick="pay()"&gt;
  Buy now
&lt;/button&gt;
&lt;!-- one comment --&gt;</code></pre>

<!-- 2. the library (pinned version), 3. one call that colours every pre code -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.12.0/highlight.min.js"></script>
<script>
  hljs.highlightAll();
</script>
</body>
</html>
A theme, the library from cdnjs, and one call. Both blocks are coloured.

The README says highlight.js works in the browser and on the server, does not depend on other frameworks, and can detect the language by itself. It is released under the BSD License.

The smallest working file

Copy this into an empty .html file and open it in a browser:

<!doctype html>
<meta charset="utf-8">
<link rel="stylesheet"
  href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.12.0/styles/github.min.css">

<pre><code class="language-javascript">const total = price * 2;
console.log(total);</code></pre>

<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.12.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
A theme, the library and one call turn plain code into coloured code.
A theme, the library and one call turn plain code into coloured code.
  1. The stylesheet is a theme with the colours. Without it, highlight.js still adds classes, but nothing changes colour.
  2. The script tag loads highlight.min.js, which puts one global, hljs, on the page.
  3. hljs.highlightAll() finds every block that matches pre code and colours it.

The class="language-javascript" on the <code> tag names the language. Leave it out and highlight.js guesses. To keep a block uncoloured, give it the class nohighlight.

Escape the tags in HTML examples

This mistake comes up when the code you show is itself HTML. The browser reads the page before highlight.js runs. A raw <h1> inside <code> becomes a real heading, and the tags never reach the highlighter.

Raw tags turn into page elements. Escaped tags stay as text and get coloured.
Raw tags turn into page elements. Escaped tags stay as text and get coloured.

In a test with version 11.12.0, a block holding a raw <h1>Hi</h1> showed only "Hi". The console logged "One of your code blocks includes unescaped HTML. This is a potentially serious security risk."

Inside the block, write the characters as entities:

<pre><code class="language-html">&lt;button class="buy"&gt;Buy now&lt;/button&gt;</code></pre>

Write & as &amp; too, so an entity inside your example stays as typed. HTML entities lists the common ones, and the code and pre tags explains the markup itself.

Which CDN address to use

The highlight.js README lists three CDNs: cdnjs, jsDelivr and unpkg. Each has a pinned address for the library and the themes:

<!-- cdnjs -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.12.0/highlight.min.js"></script>
<!-- jsDelivr -->
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.12.0/build/highlight.min.js"></script>
<!-- unpkg -->
<script src="https://unpkg.com/@highlightjs/cdn-assets@11.12.0/highlight.min.js"></script>

On 1 October 2026 the latest version on npm is 11.12.0, and all three addresses above load it. The README also says not to link to the GitHub source files directly, because the library has to be built first.

Themes live in the styles/ folder next to the script. github.min.css is light and github-dark.min.css is dark.

Languages that are not in the default build

The README says the core library supports over 180 languages, but the CDN highlight.min.js does not bundle every one. In version 11.12.0, hljs.listLanguages() returned 36 names, including JavaScript, Python, CSS, SQL, Go and Rust.

A language outside the build needs its own file, loaded after the library.
A language outside the build needs its own file, loaded after the library.

For anything else, load its file from the languages/ folder, after the library and before highlightAll():

<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.12.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.12.0/languages/elixir.min.js"></script>
<script>hljs.highlightAll();</script>

Without that file, a language-elixir block stays plain. The console says "Could not find the language 'elixir'" and "Falling back to no-highlight mode".

Highlighting text you get at runtime

highlightAll() colours the blocks it finds when it runs. Code you add to the page later stays plain. For that, the API offers three other calls:

Call Takes Gives back
hljs.highlightAll() Nothing Colours every pre code on the page
hljs.highlightElement(el) One element Colours that element in place
hljs.highlight(text, options) A string and a language An object; .value is coloured HTML
hljs.highlightAuto(text) A string The same, plus the detected .language and .relevance

This example calls highlight() or highlightAuto() on every keystroke and puts .value into a <code> element:

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>Highlight text as you type</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.12.0/styles/github.min.css">
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f6f7f9; color: #1d2330; }
  .row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 8px; font-size: 14px; }
  select { padding: 6px 8px; font: inherit; border: 1px solid #c9ced6; border-radius: 6px; background: #fff; }
  textarea { box-sizing: border-box; width: 100%; height: 130px; padding: 8px; font: 14px/1.5 ui-monospace, monospace; border: 1px solid #c9ced6; border-radius: 8px; resize: vertical; }
  pre { margin: 10px 0 0; border: 1px solid #d9dde3; border-radius: 8px; overflow: auto; }
  pre code.hljs { font-size: 14px; line-height: 1.5; min-height: 120px; }
  #info { margin: 8px 0 0; font-size: 14px; color: #4b5563; }
</style>
</head>
<body>
<div class="row">
  <label for="lang">Language</label>
  <select id="lang">
    <option value="auto">Detect automatically</option>
    <option value="javascript">JavaScript</option>
    <option value="python">Python</option>
    <option value="xml">HTML / XML</option>
    <option value="css">CSS</option>
    <option value="sql">SQL</option>
  </select>
</div>
<textarea id="src" spellcheck="false">SELECT name, total
FROM orders
WHERE total > 100
ORDER BY total DESC;</textarea>
<pre><code id="out" class="hljs"></code></pre>
<p id="info"></p>

<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.12.0/highlight.min.js"></script>
<script>
  const src = document.getElementById('src');
  const lang = document.getElementById('lang');
  const out = document.getElementById('out');
  const info = document.getElementById('info');

  function render() {
    // highlight() and highlightAuto() take a string and return escaped HTML in .value
    const result = lang.value === 'auto'
      ? hljs.highlightAuto(src.value)
      : hljs.highlight(src.value, { language: lang.value });
    out.innerHTML = result.value;
    info.textContent = 'Language: ' + result.language + ', relevance: ' + result.relevance;
  }

  src.addEventListener('input', render);
  lang.addEventListener('change', render);
  render();
</script>
</body>
</html>
Type in the box. With Detect automatically, the line under the code shows which language highlight.js guessed.

Automatic detection is a guess. In our test, a four-line Python function was detected as CSS until the code grew longer. When you know the language, pass it, or put the language- class on the block.

Typing <img> into the box shows text, not an image. In our test, the .value string had the tag escaped, so it was safe to put into innerHTML.

A finished example: a code snippet card

Tabs for four languages, a light and dark theme switch, and a footer with the language and line count. Elixir is loaded as an extra language file.

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 snippet card</title>
<!-- two themes; the dark one starts disabled -->
<link id="light" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.12.0/styles/github.min.css">
<link id="dark" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.12.0/styles/github-dark.min.css" disabled>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #eef0f3; color: #1d2330; }
  body.is-dark { background: #161b22; color: #e6edf3; }
  .card { max-width: 680px; margin: 0 auto; border-radius: 12px; overflow: hidden; border: 1px solid #c9ced6; }
  .is-dark .card { border-color: #30363d; }
  .bar { display: flex; flex-wrap: wrap; gap: 4px; padding: 8px; background: #fff; }
  .is-dark .bar { background: #0d1117; }
  .bar button { padding: 6px 11px; font: inherit; font-size: 14px; border: 0; border-radius: 6px; background: transparent; color: inherit; cursor: pointer; }
  .bar button.on { background: #2563eb; color: #fff; }
  .bar .theme { margin-left: auto; border: 1px solid #c9ced6; }
  pre { margin: 0; }
  pre[hidden] { display: none; }
  pre code.hljs { font-size: 14px; line-height: 1.55; padding: 14px; }
  .foot { display: flex; justify-content: space-between; padding: 6px 12px; font-size: 13px; background: #fff; color: #4b5563; }
  .is-dark .foot { background: #0d1117; color: #8b949e; }
</style>
</head>
<body>
<div class="card">
  <div class="bar" id="tabs">
    <button class="on" data-tab="0">HTML</button>
    <button data-tab="1">CSS</button>
    <button data-tab="2">JS</button>
    <button data-tab="3">Elixir</button>
    <button class="theme" id="theme">Dark theme</button>
  </div>
<pre><code class="language-html">&lt;form id="signup"&gt;
  &lt;label&gt;Email &lt;input type="email" required&gt;&lt;/label&gt;
  &lt;button&gt;Join&lt;/button&gt;
&lt;/form&gt;</code></pre>
<pre hidden><code class="language-css">#signup {
  display: flex;
  gap: 8px;
}
#signup button { background: #2563eb; color: white; }</code></pre>
<pre hidden><code class="language-javascript">document.getElementById('signup')
  .addEventListener('submit', (e) =&gt; {
    e.preventDefault();
    console.log('Thanks!');
  });</code></pre>
<pre hidden><code class="language-elixir">defmodule Signup do
  def join(email) when is_binary(email) do
    {:ok, String.downcase(email)}
  end
end</code></pre>
  <div class="foot"><span id="lang"></span><span id="lines"></span></div>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.12.0/highlight.min.js"></script>
<!-- Elixir is not in the default build, so load its language file after the library -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.12.0/languages/elixir.min.js"></script>
<script>
  const blocks = document.querySelectorAll('.card pre');
  const buttons = document.querySelectorAll('#tabs [data-tab]');

  // highlightElement() runs right away (highlightAll() may wait for the page to finish loading)
  blocks.forEach(pre => hljs.highlightElement(pre.querySelector('code')));

  function show(i) {
    blocks.forEach((pre, n) => pre.hidden = n !== i);
    buttons.forEach((b, n) => b.classList.toggle('on', n === i));
    const code = blocks[i].querySelector('code');
    // the last language-* class is the one highlight.js used (html becomes xml)
    const used = [...code.classList].filter(c => c.startsWith('language-')).pop();
    document.getElementById('lang').textContent = used.slice(9);
    document.getElementById('lines').textContent = code.textContent.split('\n').length + ' lines';
  }
  buttons.forEach((b, i) => b.addEventListener('click', () => show(i)));
  show(0);

  // swap themes by turning one stylesheet off and the other on
  document.getElementById('theme').addEventListener('click', (e) => {
    const dark = document.getElementById('dark');
    dark.disabled = !dark.disabled;
    document.getElementById('light').disabled = !dark.disabled;
    document.body.classList.toggle('is-dark', !dark.disabled);
    e.target.textContent = dark.disabled ? 'Dark theme' : 'Light theme';
  });
</script>
</body>
</html>
Tabs switch the visible block. The theme button turns one stylesheet off and the other on.
  • Theme switch: both stylesheets are linked, the dark one with the disabled attribute. The button flips disabled on each. Dark mode with CSS covers following the system setting instead.
  • Language label: highlight.js adds the language it used as a class. A block marked language-html also gets language-xml, because HTML is highlighted as XML.
  • Highlighting right away: the card calls highlightElement() on each block. While the page is loading, highlightAll() waits for it to finish, so code right after it still sees plain blocks.

A copy button is a common next step. HTML copy button shows one in a few lines.

highlight.js with npm and a bundler

In a project with a bundler or Node.js, you install the package and import it. The README gives this for loading only the languages you need:

// npm install highlight.js
import hljs from 'highlight.js/lib/core';
import javascript from 'highlight.js/lib/languages/javascript';
hljs.registerLanguage('javascript', javascript);

Those bare imports need a bundler. For a single HTML file, use the script tags above, or the ES module files the README lists under each CDN.

When it does not work

What you see Cause Fix
hljs is not defined Your call runs before the library tag Put the highlight.js script tag first
Code is plain, no colours No theme stylesheet Add a styles/*.min.css link
HTML example shows only text Raw < in the code block Write &lt; and &gt;
"Could not find the language" The language is not in the default build Load its languages/*.min.js file
Wrong language colours Auto detection guessed Add a language- class
Code added later stays plain highlightAll() already ran Call hljs.highlightElement(el)
"Element previously highlighted" The same element was highlighted twice Highlight it once, or clear dataset.highlighted first
An inline <code> is not coloured The default selector is pre code Wrap it in <pre> or call highlightElement

Each row was reproduced in Chromium with version 11.12.0. HTML JavaScript not working covers reading the console.

Coloured code is easier to send as a page than as a file. An .html attachment may open as plain text on a phone, and a screenshot cannot be copied from or switched to the dark theme.

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, its scripts run and highlight.js loads from cdnjs, so the people you send it to see the coloured code and can use the tabs themselves.

Anyone with the link can open it without an account. If you change the code later, the same link shows the new version. Share HTML code as a link covers more ways to send code.

Questions people ask

Do I need npm or a build step to use highlight.js?

No. The highlight.js README gives a stylesheet link, a script tag and one call to hljs.highlightAll() as the bare minimum for a web page. npm and imports are for projects that use a bundler or Node.js.

Which highlight.js version should I use?

On 1 October 2026 the latest version on npm is 11.12.0, and the README uses 11.12.0 in its CDN examples. Keep the version in the address so the page does not change when a new release comes out.

Is highlight.js free?

Yes. The README says it is released under the BSD License, and the LICENSE file in the repository is the BSD 3-Clause License.

Why does my HTML example show only the text and not the tags?

The browser read your tags as real HTML before highlight.js saw them. Write < as &lt; and > as &gt; inside the code block. highlight.js also logs a warning about unescaped HTML in the console.

How do I get a dark theme?

Link a dark stylesheet, such as github-dark.min.css, instead of a light one. To switch at runtime, include both link tags and set the disabled property on the one you do not want, as the snippet card example does.

Keep reading