Syntax Highlight Code in Markdown
- Install Dependencies
- Configure the `ReactMarkdown` Component
- Choose the Right SyntaxHighlighter Import for Your Project
- Configure the `SyntaxHighlighter` component and add Line Highlighting Functionality
- Controlling Line Highlighting in Your Markdown
- Adding Custom CSS
- Fixing Mobile Issues
- Removing Line Numbers
Here is some fancy syntax highlighting using react-syntax-highlighter
and react-markdown
in Next.js!
1const Hello = () => { 2 3 return ( 4 <div> 5 Let's dive into syntax highlighting! 6 </div> 7 ) 8} 9 10export default Hello; 11
So how exactly is it done? Most info about this references a now deprecated way of doing this (renderer
is no longer a thing). With a bit of digging through the docs here's how you can get code syntax highlighting in Next.js (and Typescript).
Install Dependencies
npm i react-markdown react-syntax-highlighter
If you're using TypeScript, you will also want the Types. The following command will add the types package as a development dependency:
npm i @types/react-syntax-highlighter -D
Configure the ReactMarkdown
Component
I will assume you already have some page or component where you are processing Markdown. In the case of react-markdown
you may have a component called Markdown.tsx
that looks something like this:
1// Markdown.tsx 2 3import { FC } from 'react; 4import ReactMarkdown from 'react-markdown'; 5 6type MarkdownProps = { 7 markdown: string & { content?: string }; 8}; 9 10const Markdown: FC<MarkdownProps> = ({ markdown }) => { 11 12 return ( 13 <ReactMarkdown> 14 {markdown.content} 15 </ReactMarkdown> 16 ) 17} 18 19export default Markdown; 20
In this basic example, the ReactMarkdown
component is being passed raw markdown with the prop markdown.content
. You can change this to work for your project.
From here, let's simply add the components
prop to ReactMarkdown
. This gives us the ability to add SyntaxHighlighter
in the next steps.
1// Markdown.tsx 2 3import { FC } from 'react; 4import ReactMarkdown from 'react-markdown'; 5 6type MarkdownProps = { 7 markdown: string & { content?: string }; 8}; 9 10const Markdown: FC<MarkdownProps> = ({ markdown }) => { 11 12 const MarkdownComponents: object = { 13 // SyntaxHighlight code will go here 14 } 15 16 return ( 17 <ReactMarkdown 18 components={MarkdownComponents} 19 > 20 {markdown.content} 21 </ReactMarkdown> 22 ) 23} 24 25export default Markdown; 26
Choose the Right SyntaxHighlighter Import for Your Project
The official documentation covers this quite well, but here's the tl;dr:
- Unless you want a massive JS bundle on your markdown pages, you should only be using the Light Build. You will need to manually import the specific languages you are going to be syntax highlighting in your markdown.
- If you're using Next.js, change the theme import from
…/dist/esm/…
to…/dist/cjs/…
. Hopefully ESM is fully supported in Next.js soon. - If you want to syntax highlight TSX/JSX you must use Prism. Highlight.js does not support TSX or JSX.
For the rest of this guide, I will be using the Prism Light Build with cjs imports. I will add TSX, TypeScript, SCSS, Bash, Markdown, and JSON support. Simply add whichever languages you'd like to use. Check here for languages supported by Prism.
1// Markdown.tsx 2 3import { FC } from 'react; 4import ReactMarkdown from 'react-markdown'; 5import { PrismLight as SyntaxHighlighter } from 'react-syntax-highlighter'; 6import tsx from 'react-syntax-highlighter/dist/cjs/languages/prism/tsx'; 7import typescript from 'react-syntax-highlighter/dist/cjs/languages/prism/typescript'; 8import scss from 'react-syntax-highlighter/dist/cjs/languages/prism/scss'; 9import bash from 'react-syntax-highlighter/dist/cjs/languages/prism/bash'; 10import markdown from 'react-syntax-highlighter/dist/cjs/languages/prism/markdown'; 11import json from 'react-syntax-highlighter/dist/cjs/languages/prism/json'; 12 13SyntaxHighlighter.registerLanguage('tsx', tsx); 14SyntaxHighlighter.registerLanguage('typescript', typescript); 15SyntaxHighlighter.registerLanguage('scss', scss); 16SyntaxHighlighter.registerLanguage('bash', bash); 17SyntaxHighlighter.registerLanguage('markdown', markdown); 18SyntaxHighlighter.registerLanguage('json', json); 19 20const Markdown: FC<MarkdownProps> = ({ markdown }) => { 21 22 type MarkdownProps = { 23 markdown: string & { content?: string }; 24 }; 25 26 const MarkdownComponents: object = { 27 // SyntaxHighlight code will go here 28 } 29 30 return ( 31 <ReactMarkdown 32 components={MarkdownComponents} 33 > 34 {markdown.content} 35 </ReactMarkdown> 36 ) 37} 38 39export default Markdown; 40
Configure the SyntaxHighlighter
component and add Line Highlighting Functionality
Let's finally start adding code for our ReactMarkdown
custom component. We will import a theme and parse-numeric-range
for the line highlight logic.
1// Markdown.tsx 2 3... 4import rangeParser from 'parse-numeric-range'; 5import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'; 6 7type MarkdownProps = { 8 markdown: string & { content?: string }; 9}; 10 11const Markdown: FC<MarkdownProps> = ({ markdown }) => { 12 13 const syntaxTheme = oneDark; 14 15 const MarkdownComponents: object = { 16 code({ node, inline, className, ...props }) { 17 const hasLang = /language-(\w+)/.exec(className || ''); 18 const hasMeta = node?.data?.meta; 19 20 const applyHighlights: object = (applyHighlights: number) => { 21 if (hasMeta) { 22 const RE = /{([\d,-]+)}/; 23 const metadata = node.data.meta?.replace(/\s/g, ''); 24 const strlineNumbers = RE?.test(metadata) 25 ? RE?.exec(metadata)[1] 26 : '0'; 27 const highlightLines = rangeParser(strlineNumbers); 28 const highlight = highlightLines; 29 const data: string = highlight.includes(applyHighlights) 30 ? 'highlight' 31 : null; 32 return { data }; 33 } else { 34 return {}; 35 } 36 }; 37 38 return hasLang ? ( 39 <SyntaxHighlighter 40 style={syntaxTheme} 41 language={hasLang[1]} 42 PreTag="div" 43 className="codeStyle" 44 showLineNumbers={true} 45 wrapLines={hasMeta} 46 useInlineStyles={true} 47 lineProps={applyHighlights} 48 > 49 {props.children} 50 </SyntaxHighlighter> 51 ) : ( 52 <code className={className} {...props} /> 53 ) 54 }, 55 } 56 57 ... 58} 59 60export default Markdown; 61
- First we check for the existence of the metadata to prevent TypeScript errors if it's undefined.
- Inside of the
applyHighlights
function, we declareRE
as our Regex checker. - Then we declare a variable called
metadata
that removes any spaces from the object. strlineNumbers
checks the metadata against the Regex and ensures the object contains only numbers, hyphens, commas.rangeParser()
spits out an array of line numbers based upon the range we defined in markdown!- We then check which lines to highlight and apply a data attribute
data="highlight"
on the spans wrap the lines we defined.
We now have the ability to style data="highlight"
with some CSS. I will leave this up to you, as it will be highly dependent on the look and style you are going for.
Controlling Line Highlighting in Your Markdown
So now that you have your component code in place for React-Markdown
and SyntaxHighlighter
, how do you actually highlight lines of code within your markdown?
1```tsx {3-4, 8} 2
Simply add curly braces with comma separated line numbers and/or ranges of line numbers!
Adding Custom CSS
You can easily target your ReactMarkdown
and SyntaxHighlighter
components like so. You may have to use some !important
tags to target certain elements within the Syntax Highlighting, which is less than ideal, but it works.
1// Markdown.tsx 2... 3 4const Markdown: FC<MarkdownProps> = ({ markdown }) => { 5 6 ... 7 8 const MarkdownComponents: object = { 9 10 const styleMarkdown = css({ 11 '.codeStyle, pre, code, code span': { 12 // Your SyntaxHighlighter override styles here 13 }, 14 code: { 15 // Your general code styles here 16 }, 17 'pre code': { 18 // Your code-block styles here 19 }, 20 'h3 code': { 21 color: 'inherit' 22 }, 23 'span.linenumber': { 24 display: 'none !important' 25 }, 26 '[data="highlight"]': { 27 // Your custom line highlight styles here 28 }, 29 }) 30 31 code({ node, inline, className, ...props }) { 32 33 ... 34 35 return hasLang ? ( 36 <SyntaxHighlighter 37 className="codeStyle" 38 ... 39 > 40 {props.children} 41 </SyntaxHighlighter> 42 ) 43 } 44 } 45 46 return ( 47 <ReactMarkdown 48 ... 49 css={styleMarkdown} 50 > 51 {markdown.content} 52 </ReactMarkdown> 53 ) 54} 55 56... 57
Fixing Mobile Issues
There is a known issue with Prism where your CSS background color for the line highlight will not extend past the viewport. So when you scroll horizontally on a mobile screen, the highlighting abruptly ends.
There are a lot of over-engineered recommendations to fix it, including wrapping the pre tag in a container to apply more styles. The fix I came up with is hacky, yet elegant:
1code: { 2 transform: translateZ(0); 3 min-width: 100%; 4 float: left; 5 & > span { 6 display: block; 7 } 8} 9
I had almost forgotten about floats but hey... it works 🤷
Removing Line Numbers
The lineNumber prop must be set to true
or the line highlighting won't work at all. If you'd like to disable the line numbers, this should do the trick:
1span.linenumber { display: none; } 2
A better solution would be to write some additional functions to accept more meta strings for doing things like setting titles, disabling/enabling line numbers, etc. but I'll leave that for another day (and a future guide).
Whew, there you have it. Syntax highlighting your markdown in React using TypeScript with custom line highlighting. 🙌