Syntax Highlight Code in Markdown
Here is an example of syntax highlighting with react-syntax-highlighter and react-markdown in Next.js:
const Hello = () => { return ( <div> Let's dive into syntax highlighting! </div> )}export default HellotsxSo, how is it done?
Much of the information available online refers to a now-deprecated approach. The renderer API is no longer available.
After some digging through the documentation, here is how you can add code syntax highlighting to a Next.js project using TypeScript.
Install the Dependencies
npm install react-markdown react-syntax-highlighter
bashIf you are using TypeScript, you will also need the corresponding type definitions:
npm install @types/react-syntax-highlighter --save-dev
bashConfigure the ReactMarkdown Component
I will assume that you already have a page or component that processes Markdown.
When using react-markdown, you may have a component named Markdown.tsx that looks something like this:
// Markdown.tsx
import { FC } from 'react'
import ReactMarkdown from 'react-markdown'
type MarkdownProps = {
markdown: string & { content?: string }
}
const Markdown: FC<MarkdownProps> = ({ markdown }) => {
return (
<ReactMarkdown>
{markdown.content}
</ReactMarkdown>
)
}
export default Markdown
tsxIn this basic example, the ReactMarkdown component receives raw Markdown from markdown.content. You can adapt this value to match the structure of your own project.
Next, add the components prop to ReactMarkdown. This will allow us to provide a custom renderer for code elements in the following steps.
// Markdown.tsximport { FC } from 'react'import ReactMarkdown from 'react-markdown'type MarkdownProps = { markdown: string & { content?: string }}const Markdown: FC<MarkdownProps> = ({ markdown }) => { const MarkdownComponents: object = { // Syntax highlighting code will go here } return ( <ReactMarkdown components={MarkdownComponents}> {markdown.content} </ReactMarkdown> )}export default MarkdowntsxChoose the Correct SyntaxHighlighter Import
The official documentation covers the available builds in detail, but here are the main considerations:
- Unless you want to add a large amount of JavaScript to your Markdown pages, use the Light Build. This requires you to import and register each language you intend to highlight.
- When this post was written, Next.js projects required changing theme imports from
dist/esmtodist/cjs. Full ESM support was still being tracked in this Next.js issue. - To highlight TSX or JSX, use Prism. Highlight.js does not support those languages.
For the rest of this guide, I will use the Prism Light Build with CommonJS imports. I will add support for TSX, TypeScript, SCSS, Bash, Markdown, and JSON.
Import only the languages you need. You can find the full list of languages supported by Prism here.
// Markdown.tsximport { FC } from 'react'import ReactMarkdown from 'react-markdown'import { PrismLight as SyntaxHighlighter } from 'react-syntax-highlighter'import tsx from 'react-syntax-highlighter/dist/cjs/languages/prism/tsx'import typescript from 'react-syntax-highlighter/dist/cjs/languages/prism/typescript'import scss from 'react-syntax-highlighter/dist/cjs/languages/prism/scss'import bash from 'react-syntax-highlighter/dist/cjs/languages/prism/bash'import markdown from 'react-syntax-highlighter/dist/cjs/languages/prism/markdown'import json from 'react-syntax-highlighter/dist/cjs/languages/prism/json'SyntaxHighlighter.registerLanguage('tsx', tsx)SyntaxHighlighter.registerLanguage('typescript', typescript)SyntaxHighlighter.registerLanguage('scss', scss)SyntaxHighlighter.registerLanguage('bash', bash)SyntaxHighlighter.registerLanguage('markdown', markdown)SyntaxHighlighter.registerLanguage('json', json)type MarkdownProps = { markdown: string & { content?: string }}const Markdown: FC<MarkdownProps> = ({ markdown }) => { const MarkdownComponents: object = { // Syntax highlighting code will go here } return ( <ReactMarkdown components={MarkdownComponents}> {markdown.content} </ReactMarkdown> )}export default MarkdowntsxConfigure SyntaxHighlighter and Add Line Highlighting
Now we can begin adding the custom code renderer.
We will import a Prism theme and parse-numeric-range, which will handle the line-number ranges used by our highlighting logic.
// Markdown.tsx...import rangeParser from 'parse-numeric-range'import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'type MarkdownProps = { markdown: string & { content?: string }}const Markdown: FC<MarkdownProps> = ({ markdown }) => { const syntaxTheme = oneDark const MarkdownComponents: object = { code({ node, inline, className, ...props }) { const hasLang = /language-(\w+)/.exec(className || '') const hasMeta = node?.data?.meta const applyHighlights: object = (lineNumber: number) => { if (hasMeta) { const RE = /{([\d,-]+)}/ const metadata = node.data.meta?.replace(/\s/g, '') const stringLineNumbers = RE.test(metadata) ? RE.exec(metadata)?.[1] || '0' : '0' const highlightLines = rangeParser(stringLineNumbers) const data: string | null = highlightLines.includes(lineNumber) ? 'highlight' : null return { data } } return {} } return hasLang ? ( <SyntaxHighlighter style={syntaxTheme} language={hasLang[1]} PreTag="div" className="codeStyle" showLineNumbers={true} wrapLines={hasMeta} useInlineStyles={true} lineProps={applyHighlights} > {props.children} </SyntaxHighlighter> ) : ( <code className={className} {...props} /> ) }, } ...}export default MarkdowntsxHere is what the line-highlighting logic is doing:
- First, we check whether metadata exists to avoid errors when it is undefined.
- Inside
applyHighlights, we define a regular expression that looks for line numbers enclosed in curly braces. - We then remove spaces from the metadata string.
stringLineNumberschecks the metadata against the regular expression and extracts numbers, commas, and ranges.rangeParser()converts the range into an array of individual line numbers.- Finally, we check whether the current line should be highlighted and apply a
data="highlight"attribute when it matches.
We can now target data="highlight" with CSS. The exact styling will depend on the visual design of your project.
Controlling Line Highlighting in Markdown
Once the ReactMarkdown and SyntaxHighlighter components are configured, you can specify highlighted lines directly in the Markdown code fence:
```tsx {3-4, 8}
```
markdownAdd comma-separated line numbers or ranges inside curly braces. In this example, lines 3, 4, and 8 will be highlighted.
Adding Custom CSS
You can target the rendered ReactMarkdown and SyntaxHighlighter elements with custom CSS.
You may need to use !important for some styles generated by the syntax highlighter. This is not ideal, but it can be necessary when overriding inline or library-provided styles.
// Markdown.tsx...const Markdown: FC<MarkdownProps> = ({ markdown }) => { ... const styleMarkdown = css({ '.codeStyle, pre, code, code span': { // SyntaxHighlighter override styles }, code: { // General code styles }, 'pre code': { // Code block styles }, 'h3 code': { color: 'inherit' }, 'span.linenumber': { display: 'none !important' }, '[data="highlight"]': { // Custom line highlight styles }, }) const MarkdownComponents: object = { code({ node, inline, className, ...props }) { ... return hasLang ? ( <SyntaxHighlighter className="codeStyle" ... > {props.children} </SyntaxHighlighter> ) : ( <code className={className} {...props} /> ) } } return ( <ReactMarkdown components={MarkdownComponents} css={styleMarkdown} > {markdown.content} </ReactMarkdown> )}...tsxFixing Mobile Highlighting Issues
Prism has a known issue where a highlighted line's background color may not extend beyond the viewport.
When a code block is wider than a mobile screen and the user scrolls horizontally, the highlighted background can end abruptly at the original viewport width.
Some suggested solutions involve wrapping the pre element in another container and applying additional styles. The following workaround is simpler:
code: {
transform: translateZ(0);
min-width: 100%;
float: left;
& > span {
display: block;
}
}
scssIt relies on a float, but it resolves the issue.
Removing Line Numbers
The showLineNumbers prop must be set to true for this line-highlighting implementation to work.
To hide the visible line numbers while preserving the highlighting behavior, use the following CSS:
span.linenumber {
display: none;
}
scssA more flexible implementation could accept additional metadata for options such as titles or enabling and disabling line numbers, but that is outside the scope of this guide.
You should now have syntax highlighting for Markdown code blocks in React, including support for highlighting selected lines.
Thanks to Prince for the helpful write-up and the function that inspired this implementation. His guide is particularly useful for projects using Gatsby or prism-react-renderer.