Svelte Highlight
Svelte component library for highlighting code using highlight.js.
Installation
npm i svelte-highlightpnpm i svelte-highlightbun add svelte-highlightyarn add svelte-highlightUsage
The default Highlight component requires two props:
code: text to highlightlanguage: language grammar used to highlight the text
Import languages from svelte-highlight/languages.
See the Languages page for a list of supported languages.
<script>
import Highlight from "svelte-highlight";
import typescript from "svelte-highlight/languages/typescript";
import horizonDark from "svelte-highlight/styles/horizon-dark";
const code = "const add = (a: number, b: number) => a + b;";
</script>
<svelte:head>
{@html horizonDark}
</svelte:head>
<Highlight language={typescript} {code} />Import styles from svelte-highlight/styles.
There are two ways to add styles:
Injected styles: JavaScript styles injected using the svelte:head APICSS StyleSheet: CSS file that may require an appropriate file loader
Refer to the Styles page for a list of supported styles.
CSS StyleSheets can also be externally linked from a Content Delivery Network (CDN) like unpkg.com .
<link
rel="stylesheet"
href="https://unpkg.com/svelte-highlight/styles/github.css"
/>
Scoping styles
Themes target global .hljs selectors, so the
last one injected wins. That is fine when every block shares one theme.
Use HighlightStyle when blocks on the same page
need different themes, like a style gallery or a light snippet next to a
dark one.
<script>
import { Highlight, HighlightStyle } from "svelte-highlight";
import typescript from "svelte-highlight/languages/typescript";
import a11yDark from "svelte-highlight/styles/a11y-dark";
import github from "svelte-highlight/styles/github";
const code = "const add = (a: number, b: number) => a + b;";
</script>
<HighlightStyle theme={a11yDark}>
<Highlight language={typescript} {code} />
</HighlightStyle>
<HighlightStyle theme={github}>
<Highlight language={typescript} {code} />
</HighlightStyle>Both themes render on the same page, scoped to each block:
const add = (a: number, b: number) => a + b;const add = (a: number, b: number) => a + b;Dark mode
HighlightStyle can emit a light and a dark theme together and switch between them. Pass light and dark instead of theme.
The mode prop controls how the two themes are
switched (default "auto"):
"auto": wrap each theme in a@media (prefers-color-scheme)query so the OS or browser preference decides."light"/"dark": emit only that single theme.- any other string: treated as a CSS selector that gates the dark block
while light stays the default—e.g.
[data-theme="dark"]to drive theming from a manual toggle.
Both themes are scoped to the wrapper, so different blocks can use
different theme pairs on the same page. When light and dark are both set they take precedence over theme; passing only theme keeps the existing single-theme behavior unchanged.
<script>
import { Highlight, HighlightStyle } from "svelte-highlight";
import typescript from "svelte-highlight/languages/typescript";
import github from "svelte-highlight/styles/github";
import githubDark from "svelte-highlight/styles/github-dark";
const code = "const add = (a: number, b: number) => a + b;";
</script>
<HighlightStyle light={github} dark={githubDark}>
<Highlight language={typescript} {code} />
</HighlightStyle>Renders following your system prefers-color-scheme:
const add = (a: number, b: number) => a + b;Or gate the dark block behind a CSS selector for a manual toggle:
<HighlightStyle light={github} dark={githubDark} mode={'[data-theme="dark"]'}>
<Highlight language={typescript} {code} />
</HighlightStyle>Svelte Syntax Highlighting
Use the HighlightSvelte component for Svelte
syntax highlighting.
<script>
import { HighlightSvelte } from "svelte-highlight";
import horizonDark from "svelte-highlight/styles/horizon-dark";
const code = `<button on:click={() => { console.log(0); }}>Click me</button>`;
</script>
<svelte:head>
{@html horizonDark}
</svelte:head>
<HighlightSvelte {code} />Auto-highlighting
The HighlightAuto component invokes the highlightAuto API from highlight.js.
<script>
import { HighlightAuto } from "svelte-highlight";
import horizonDark from "svelte-highlight/styles/horizon-dark";
const code = ".body { padding: 0; margin: 0; }";
</script>
<svelte:head>
{@html horizonDark}
</svelte:head>
<HighlightAuto {code} /> Optionally, you can restrict language detection to a specific subset using
the languageNames prop. This can improve
performance and accuracy.
<script>
import { HighlightAuto } from "svelte-highlight";
import atomOneDark from "svelte-highlight/styles/atom-one-dark";
const code = "const x = 42;";
</script>
<svelte:head>
{@html atomOneDark}
</svelte:head>
<HighlightAuto {code} languageNames={["javascript", "typescript"]} />Action
Use the highlight action to highlight existing <pre><code> markup in place. This is useful for progressively enhancing
server-rendered content like Markdown without swapping in a component.
The action accepts the same language prop as the
components. When code is omitted, the element's
existing textContent is highlighted. Updating code re-highlights the element.
<script>
import { highlight } from "svelte-highlight";
import typescript from "svelte-highlight/languages/typescript";
import github from "svelte-highlight/styles/github";
const code = "const add = (a, b) => a + b;";
</script>
<svelte:head>
{@html github}
</svelte:head>
<pre><code use:highlight={{ language: typescript, code }}></code></pre>Line Numbers
Use the LineNumbers component to render the
highlighted code with line numbers.
1 | |
2 | |
3 | |
4 | |
5 | |
6 | |
7 | |
8 | |
9 | |
10 | |
11 | |
12 | |
13 | |
14 | |
15 | |
Set hideBorder to true to hide the border of the line numbers column.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
By default, overflowing horizontal content is contained by a scrollbar.
Set wrapLines to true to apply a white-space: pre-wrap rule to the pre element.
1 | |
2 | |
3 | |
4 | |
5 | |
6 | |
7 | |
8 | |
9 | |
10 | |
11 | |
12 | |
13 | |
14 | |
15 | |
Use --style-props to customize visual
properties.
1 | |
2 | |
3 | |
4 | |
5 | |
6 | |
7 | |
8 | |
9 | |
10 | |
11 | |
12 | |
13 | |
14 | |
15 | |
16 | |
17 | |
18 | |
19 | |
20 | |
21 | |
Use container-level variables like --border-radius, --width, and --max-width to style the outer container without :global overrides.
1 | |
2 | |
3 | |
4 | |
5 | |
6 | |
7 | |
8 | |
9 | |
10 | |
11 | |
12 | |
13 | |
14 | |
15 | |
16 | |
17 | |
18 | |
19 | |
Use startingLineNumber to customize the starting
line number. By default, line numbers start at 1.
42 | |
43 | |
44 | |
45 | |
46 | |
47 | |
48 | |
49 | |
50 | |
51 | |
52 | |
53 | |
54 | |
55 | |
56 | |
Use highlightedLines to highlight specific
lines. Indices start at zero.
1 | |
2 | |
3 | |
4 | |
5 | |
6 | |
7 | |
8 | |
9 | |
10 | |
11 | |
12 | |
13 | |
14 | |
15 | |
Use --unhighlighted-opacity or --unhighlighted-filter to de-emphasize the remaining lines and focus attention on the highlighted
ones.
1 | |
2 | |
3 | |
4 | |
5 | |
6 | |
7 | |
8 | |
9 | |
10 | |
11 | |
12 | |
13 | |
14 | |
15 | |
16 | |
17 | |
18 | |
19 | |
Use --highlighted-background to customize the
background color of highlighted lines.
1 | |
2 | |
3 | |
4 | |
5 | |
6 | |
7 | |
8 | |
9 | |
10 | |
11 | |
12 | |
13 | |
14 | |
15 | |
16 | |
17 | |
18 | |
19 | |
When using a custom slot, forward langtag and languageName from Highlight to LineNumbers.
1 | |
2 | |
3 | |
4 | |
5 | |
6 | |
7 | |
8 | |
9 | |
10 | |
11 | |
12 | |
13 | |
14 | |
Copy Button
Compose the CopyButton component alongside Highlight to add a copy-to-clipboard button. Position it by wrapping both in a
relatively-positioned container.
By default, it copies the code using the native
Clipboard API and shows a transient "copied" state.
<script>
import Highlight, { CopyButton } from "svelte-highlight";
import typescript from "svelte-highlight/languages/typescript";
import horizonDark from "svelte-highlight/styles/horizon-dark";
const code = "const add = (a: number, b: number) => a + b";
</script>
<svelte:head>
{@html horizonDark}
</svelte:head>
<div style="position: relative">
<Highlight language={typescript} {code} />
<CopyButton {code} />
</div> Pass a copy function to override the default
copy behavior—for example, to add logging or a custom toast.
<script>
import Highlight, { CopyButton } from "svelte-highlight";
import typescript from "svelte-highlight/languages/typescript";
import horizonDark from "svelte-highlight/styles/horizon-dark";
const code = "const add = (a: number, b: number) => a + b";
// Override the default Clipboard API behavior.
function copy(code) {
navigator.clipboard.writeText(code);
console.log("Copied:", code);
}
</script>
<svelte:head>
{@html horizonDark}
</svelte:head>
<div style="position: relative">
<Highlight language={typescript} {code} />
<CopyButton {code} {copy} />
</div> Provide custom button content using the default slot. The slot exposes a copied boolean.
<script>
import Highlight, { CopyButton } from "svelte-highlight";
import typescript from "svelte-highlight/languages/typescript";
import horizonDark from "svelte-highlight/styles/horizon-dark";
const code = "const add = (a: number, b: number) => a + b";
</script>
<svelte:head>
{@html horizonDark}
</svelte:head>
<div style="position: relative">
<Highlight language={typescript} {code} />
<CopyButton {code} let:copied --copy-right="2em">
{#if copied}
Copied!
{:else}
Copy
{/if}
</CopyButton>
</div> Use --copy-* style props to customize the
offset, size, and colors of the button.
<script>
import Highlight, { CopyButton } from "svelte-highlight";
import typescript from "svelte-highlight/languages/typescript";
import horizonDark from "svelte-highlight/styles/horizon-dark";
const code = "const add = (a: number, b: number) => a + b";
</script>
<svelte:head>
{@html horizonDark}
</svelte:head>
<div style="position: relative">
<Highlight language={typescript} {code} />
<CopyButton
{code}
--copy-background="rgba(255, 255, 255, 0.1)"
--copy-color="#fff"
--copy-border-radius="8px"
--copy-size="2.5em"
/>
</div> Compose CopyButton with LineNumbers by wrapping both in a relatively-positioned container.
1 | |
2 | |
3 | |
4 | |
5 | |
6 | |
7 | |
8 | |
9 | |
10 | |
11 | |
12 | |
13 | |
14 | |
15 | |
16 | |
17 | |
18 | |
When using a language tag alongside CopyButton, offset --langtag-top and --langtag-right so the tag sits to the left of the button.
<script>
import Highlight, { CopyButton } from "svelte-highlight";
import typescript from "svelte-highlight/languages/typescript";
import horizonDark from "svelte-highlight/styles/horizon-dark";
const code = "const add = (a: number, b: number) => a + b";
</script>
<svelte:head>
{@html horizonDark}
</svelte:head>
<div style="position: relative">
<Highlight
language={typescript}
{code}
langtag
--langtag-top="0"
--langtag-right="3em"
--langtag-padding="0.25em 0.5em"
--langtag-font-size="0.75em"
/>
<CopyButton {code} />
</div>Code Window
Wrap a code block in the CodeWindow component to
frame it with window chrome. It is purely cosmetic—the default slot
renders your content unchanged.
Use the variant prop to choose the chrome style: "macos" (default) renders traffic-light dots, "terminal" renders a prompt, and "plain" renders just the
title bar. The optional title is shown in the
title bar.
The chrome is themable with --window-*, --titlebar-*, and --dot-* style props.
<script>
import Highlight, { CodeWindow } from "svelte-highlight";
import json from "svelte-highlight/languages/json";
import atomOneDark from "svelte-highlight/styles/atom-one-dark";
const code = '{ "name": "svelte-highlight" }';
</script>
<svelte:head>
{@html atomOneDark}
</svelte:head>
<CodeWindow variant="macos" title="package.json">
<Highlight language={json} {code} />
</CodeWindow>Switch the variant prop between "macos", "terminal", and "plain". Each block below also pairs it with a
different language and theme:
{
"name": "svelte-highlight",
"version": "7.12.0",
"type": "module"
}$ npm install svelte-highlight
added 1 package in 1.2s
$ npm run buildconst add = (a: number, b: number) => a + b;Every part of the chrome is themable with style props. The window is square by
default; give it rounded corners with --window-radius and recolor the body and title bar:
<CodeWindow
variant="macos"
title="example.ts"
--window-radius="12px"
--window-background="#0d1117"
--titlebar-background="#161b22"
--titlebar-color="#8b949e"
>
<Highlight language={typescript} {code} />
</CodeWindow>const add = (a: number, b: number) => a + b;Animation
Use Typewriter inside Highlight's default slot with the highlighted prop. It prints the code one character at a time, syntax highlighting
included. A blinking caret marks the end of the typed text and hides when
typing stops.
<script>
import Highlight, { Typewriter } from "svelte-highlight";
import typescript from "svelte-highlight/languages/typescript";
import github from "svelte-highlight/styles/github";
const code = "const add = (a, b) => a + b;";
</script>
<svelte:head>
{@html github}
</svelte:head>
<Highlight language={typescript} {code} let:highlighted>
<Typewriter {highlighted} />
</Highlight>const greet = (name) => {
return `Hello, ${name}!`;
}; Set speed (milliseconds per character) and pause
with play. Turn play back on to pick up where you left off. A new highlighted value starts over from the first character. Fire on:done when the last character is visible.
Customize the caret with --caret-width, --caret-height, --caret-gap, --caret-color, and --caret-blink. The demo below puts Typewriter inside a CodeWindow.
async function load(url) {
const res = await fetch(url);
return res.json();
}Status: revealing…
Terminal Output
AnsiOutput renders terminal output with ANSI SGR escape codes as styled HTML. The
parser is separate from highlight.js. Build logs, CLI output, and test
runners are the usual cases.
Standard, bright, 256-color, and 24-bit truecolor codes work, along with bold, dim, italic, and underline. Bad sequences are dropped.
Theme the 16 base colors and bold/dim styling with --ansi-* props. autoContrast (default on) flips text to
black or white when it wouldn't read on its background.
<script>
import { AnsiOutput } from "svelte-highlight";
// Raw program output, escape codes included.
const text = "\x1b[32m✓\x1b[0m build succeeded";
</script>
<AnsiOutput {text} />Test runner, framed with a terminal window
<script>
import { AnsiOutput, CodeWindow } from "svelte-highlight";
const text = `\x1b[1mRUN\x1b[0m \x1b[2mv2.1.0 /svelte-highlight\x1b[0m
\x1b[42;30m PASS \x1b[0m tests/ansi.test.ts \x1b[2m(17 tests)\x1b[0m
\x1b[41;37m FAIL \x1b[0m tests/parser.test.ts \x1b[2m(1 failed)\x1b[0m
\x1b[31m×\x1b[0m malformed input is dropped, not thrown
\x1b[1mTest Files\x1b[0m \x1b[32m1 passed\x1b[0m \x1b[31m1 failed\x1b[0m \x1b[2m(2)\x1b[0m
\x1b[1m Tests\x1b[0m \x1b[32m16 passed\x1b[0m \x1b[31m1 failed\x1b[0m \x1b[2m(17)\x1b[0m
\x1b[1m Duration\x1b[0m \x1b[2m312ms\x1b[0m`;
</script>
<CodeWindow variant="terminal" title="vitest">
<AnsiOutput {text} />
</CodeWindow>RUN v2.1.0 /svelte-highlight
PASS tests/ansi.test.ts (17 tests)
FAIL tests/parser.test.ts (1 failed)
× malformed input is dropped, not thrown
Test Files 1 passed 1 failed (2)
Tests 16 passed 1 failed (17)
Duration 312msThemed palette (Solarized-ish). Colors are --ansi-* CSS variables.
<script>
import { AnsiOutput } from "svelte-highlight";
const text = `\x1b[1;32m$\x1b[0m npm run build
\x1b[36mvite v7.3.3\x1b[0m \x1b[2mbuilding for production...\x1b[0m
\x1b[32m✓\x1b[0m 42 modules transformed.
dist/index.html \x1b[2m0.46 kB\x1b[0m \x1b[33m│ gzip: 0.30 kB\x1b[0m
dist/assets/index-a1b2c3.js \x1b[2m143.21 kB\x1b[0m \x1b[33m│ gzip: 46.12 kB\x1b[0m
\x1b[1;32m✓ built in 2.58s\x1b[0m`;
</script>
<AnsiOutput
{text}
--ansi-background="#002b36"
--ansi-foreground="#93a1a1"
--ansi-green="#859900"
--ansi-yellow="#b58900"
--ansi-cyan="#2aa198"
--ansi-red="#dc322f"
--ansi-dim-opacity="0.65"
/>$ npm run build
vite v7.3.3 building for production...
✓ 42 modules transformed.
dist/index.html 0.46 kB │ gzip: 0.30 kB
dist/assets/index-a1b2c3.js 143.21 kB │ gzip: 46.12 kB
✓ built in 2.58sGit diff
<script>
import { AnsiOutput } from "svelte-highlight";
const text = `\x1b[1mdiff --git a/src/ansi.js b/src/ansi.js\x1b[0m
\x1b[36m@@ -1,4 +1,9 @@\x1b[0m
export function parseAnsi(text) {
\x1b[32m+ if (!text) return [];\x1b[0m
\x1b[32m+ const segments = [];\x1b[0m
\x1b[31m- // TODO: implement\x1b[0m
return segments;
}`;
</script>
<AnsiOutput {text} />diff --git a/src/ansi.js b/src/ansi.js
@@ -1,4 +1,9 @@
export function parseAnsi(text) {
+ if (!text) return [];
+ const segments = [];
- // TODO: implement
return segments;
}Light theme. Custom background, palette, and type via --ansi-* props.
<script>
import { AnsiOutput } from "svelte-highlight";
const text = `\x1b[1mRUN\x1b[0m \x1b[2mv2.1.0 /svelte-highlight\x1b[0m
\x1b[42;30m PASS \x1b[0m tests/ansi.test.ts \x1b[2m(17 tests)\x1b[0m
\x1b[41;37m FAIL \x1b[0m tests/parser.test.ts \x1b[2m(1 failed)\x1b[0m
\x1b[31m×\x1b[0m malformed input is dropped, not thrown
\x1b[1mTest Files\x1b[0m \x1b[32m1 passed\x1b[0m \x1b[31m1 failed\x1b[0m \x1b[2m(2)\x1b[0m
\x1b[1m Tests\x1b[0m \x1b[32m16 passed\x1b[0m \x1b[31m1 failed\x1b[0m \x1b[2m(17)\x1b[0m
\x1b[1m Duration\x1b[0m \x1b[2m312ms\x1b[0m`;
</script>
<AnsiOutput
{text}
--ansi-background="#fdf6e3"
--ansi-foreground="#586e75"
--ansi-green="#859900"
--ansi-red="#dc322f"
--ansi-yellow="#b58900"
--ansi-blue="#268bd2"
--ansi-cyan="#2aa198"
--ansi-padding="1.25em"
--ansi-font-size="0.8125em"
--ansi-line-height="1.7"
--ansi-bold-weight="600"
--ansi-dim-opacity="0.7"
/>RUN v2.1.0 /svelte-highlight
PASS tests/ansi.test.ts (17 tests)
FAIL tests/parser.test.ts (1 failed)
× malformed input is dropped, not thrown
Test Files 1 passed 1 failed (2)
Tests 16 passed 1 failed (17)
Duration 312msEditable
HighlightEditable is a contenteditable code block. It re-highlights on every edit and keeps the caret where you
left it.
bind:code keeps your state in sync. Try typing in the block below.
bind:code: const add = (a: number, b: number) => a + b;
Enter inserts a newline, Tab/Shift+Tab indent the selected lines, and Cmd/Ctrl+Z / Shift+Z undo and redo. Set tabSize and historyLimit to customize.
bind:this exposes undo(), redo(), indent(), insert(), setCode(), and clear(). on:history reports canUndo and canRedo.
Customize the focus outline with --outline-color, --outline-width, and --outline-offset.
File Tabs
FileTabs groups snippets behind a tab strip. Pass file names as files and use let:active in the default slot to render the matching Highlight block.
Arrow keys move between tabs; Home and End jump to the ends. bind:active sets the open tab from your code. on:change fires when the user picks a different one.
<script>
import Highlight, { FileTabs } from "svelte-highlight";
import javascript from "svelte-highlight/languages/javascript";
import typescript from "svelte-highlight/languages/typescript";
import github from "svelte-highlight/styles/github";
const sources = {
"App.svelte": { language: typescript, code: "const answer = 42;" },
"index.js": { language: javascript, code: "export default answer;" },
};
const files = Object.keys(sources);
</script>
<svelte:head>
{@html github}
</svelte:head>
<FileTabs {files} let:active>
<Highlight language={sources[active].language} code={sources[active].code} />
</FileTabs>Pick a tab, or use the arrow keys:
const answer = 42;Language Targeting
All Highlight components apply a data-language attribute on the codeblock containing the language name.
This is also compatible with custom languages.
See the Languages page for a list of supported languages.
[data-language="css"] {
/* custom style rules */
}Language Tags
All Highlight components allow for a tag to be
added at the top-right of the codeblock displaying the language name.
Customize the language tag using style props. With LineNumbers, forward langtag and languageName from the parent slot (see Line Numbers above).
Defaults:
--langtag-top: 0--langtag-right: 0--langtag-background: inherit--langtag-color: inherit--langtag-border-radius: 0--langtag-padding: 1em
See the Languages page for a list of supported languages.
<script>
import { HighlightAuto } from "svelte-highlight";
$: code = `body {
padding: 0;
color: red;
}`;
</script>
<HighlightAuto {code} langtag /> <HighlightAuto
{code}
langtag
--langtag-top="0.5rem"
--langtag-right="0.5rem"
--langtag-background="linear-gradient(135deg, #2996cf, 80%, white)"
--langtag-color="#fff"
--langtag-border-radius="6px"
--langtag-padding="0.5rem"
/>Loading a Language by Name
Import a language as a static string when you know it ahead of time—the bundler can then split out only the grammars you reference.
When the language is known only at runtime—a Markdown
fence, an API field, or a user-selected value—use the loadLanguage helper to import a grammar by name. It resolves with the language object
and rejects with an Unknown language error for
an unrecognized name.
<script>
import { Highlight, loadLanguage } from "svelte-highlight";
import github from "svelte-highlight/styles/github";
// The language name is not known until runtime.
export let language = "typescript";
export let code = "const add = (a, b) => a + b;";
</script>
<svelte:head>
{@html github}
</svelte:head>
{#await loadLanguage(language) then grammar}
<Highlight {code} language={grammar} />
{:catch}
<pre>{code}</pre>
{/await}Examples
Get started with example set-ups , including SvelteKit, Vite, Rollup, Routify, and Webpack.