Metadata
-
Date
-
Last update
-
Tagged
-
Older post
-
Newer post
Syntax highlighting codeblocks with theme-ui

A recent blog post by Prince about how to use syntax highlighting in a site that uses mdx made me want to add syntax highlighting for codeblocks using the same method.
I was using @theme-ui/prism
, which did the same thing, making me postpone implementing it for myself.
His followup post about highlighting lines convinced me. I was going to follow that guide, and combine it with the goodness of @theme-ui/prism
.
I wanted to keep using the awesome presets from @theme-ui/prism
to use colors from popular syntax highlighting themes such as night owl, dracula, night owl light, and many more.
Using the @theme-ui/prism
component
Even though I’m going to remove the @theme-ui/prism
component immediately after this, the official docs for @theme-ui/prism
have an excellent explanation for how to replace the codeblocks (triple backticks) in markdown/mdx with a custom component.
Replacing the component
The Prism
component that those docs told us to use? I replaced it with my own.
I created a component called CodeBlock.js
, and used that instead.
It uses the same internals as @theme-ui/prism
. Only this time, I can tweak them and add cool stuff like line highlighting.
Those internals are powered by a package called prism-react-renderer
.
To start, this file will look almost identical to the Prism
component from @theme-ui/prism
.
/** @jsx jsx */import React from "react";import Highlight, { defaultProps } from "prism-react-renderer";import { jsx, Themed } from "theme-ui";
const CodeBlock = ({ children, className: outerClassName, ...props }) => { // MDX will pass the language as className // className also includes className(s) theme-ui injected const [language] = outerClassName.replace(/language-/, ``).split(` `); if (typeof children !== `string`) { // MDX will pass in the code string as children return null; } return ( <Highlight {...defaultProps} {...props} code={children.trim()} language={language} theme={undefined} > {({ className, style, tokens, getLineProps, getTokenProps }) => ( <Themed.pre className={`${outerClassName} ${className}`} style={style}> {tokens.map((line, index) => ( <div key={index} {...getLineProps({ line, key: index })}> {line.map((token, key) => ( <span key={key} {...getTokenProps({ token, key })} // https://github.com/system-ui/theme-ui/pull/721 sx={token.empty ? { display: `inline-block` } : undefined} /> ))} </div> ))} </Themed.pre> )} </Highlight> );};
export default CodeBlock;
Adding line highlighting
The tutorial I mentioned earlier applies a CSS-class to the lines that should be highlighted.
In theme-ui, a variant can be used to style those lines.
Getting relevant data into the component
After the three backtick followed by the language, optional key/value pairs may be passed.
Those keys will be passed into the CodeBlock
component as props.
eg. for ```javascript puppies=cute
The CodeBlock
component will have a prop called puppies
with a value of "cute"
.
I used this to pass a range of line numbers into the component under the hl
prop (short for highlight-lines).
From that point, I followed Prince’s guide again. Inside the component, I created a function that returns a boolean signalling if a line should be highlighted.
const CodeBlock = ({ children, className: outerClassName, hl, ...props }) => { const shouldHighlightLine = getShouldHighlightLine(hl); return ( <Highlight {...defaultProps} {...props} code={children.trim()} language={language} theme={undefined} > // ... </Highlight> );};
The logic to create that function lives outside of the component.
import rangeParser from "parse-numeric-range";
const getShouldHighlightLine = (hl) => { if (hl) { const lineNumbers = rangeParser(hl); return (index) => lineNumbers.includes(index + 1); } return () => false;};
Theme-ui variant
I added some CSS to the theme-ui file to the styles.CodeBlock.highlightLine
variant and applied it to the lines that should be highlighted.
// ...tokens.map((line, index) => ( <div key={index} {...getLineProps({ line, key: index })} sx={ shouldHighlightLine(index) ? { variant: `styles.CodeBlock.highlightLine` } : undefined } > {line.map((token, key) => ( // ... ))} </div>));// ...
A wrapping element
A small caveat is that the background for highlighted lines doesn’t extend all the way if the user has to (horizontally) scroll the codeblock.
This can be solved by wrapping the entire component in another element, and applying some CSS.
// ...<div sx={{ variant: `styles.CodeBlock` }}> <Highlight {...defaultProps} {...props} code={children.trim()} language={language} theme={undefined} > // ... </Highlight></div>
At this point I also moved the nightOwlLight
preset to the styles.CodeBlock
variant inside the theme-ui file.
The corresponding variant in the theme-ui file:
import nightOwlLight from "@theme-ui/prism/presets/night-owl-light.json";// ...CodeBlock: { ...nightOwlLight, overflow: `auto`, pre: { backgroundColor: `transparent`, float: `left`, minWidth: `100%`, margin: 0, },},// ...
Adding title support
The steps to add a section above the codeblock that can hold a title are very similar to the line highlighting ones.
Getting relevant data into the component
Pass a prop called title
into the CodeBlock
component.
eg. ```javascript title=CodeBlock.js
will make the title
prop have a value of "Codeblock.js"
Own element
Above the existing outer <div>
, create another one and conditionally render the title
.
Theme-ui variant
A theme-ui variant, styles.CodeBlock.title
makes sure the element can be styled via the theme-ui file.
const CodeBlock = ({ children, className: outerClassName, title, hl, title, ...props}) => { return ( <React.Fragment> {title && <div sx={{ variant: `styles.CodeBlock.title` }}>{title}</div>} <div sx={{ variant: `styles.CodeBlock` }}>{/* ... */}</div> </React.Fragment> );};
Final code
/** @jsx jsx */import React from "react";import Highlight, { defaultProps } from "prism-react-renderer";import rangeParser from "parse-numeric-range";import { jsx, Themed } from "theme-ui";
const getShouldHighlightLine = (hl) => { if (hl) { const lineNumbers = rangeParser(hl); return (index) => lineNumbers.includes(index + 1); } return () => false;};
const CodeBlock = ({ children, className: outerClassName, title, hl, ...props}) => { // MDX will pass the language as className // className also includes className(s) theme-ui injected const [language] = outerClassName.replace(/language-/, ``).split(` `); if (typeof children !== `string`) { // MDX will pass in the code string as children return null; } const shouldHighlightLine = getShouldHighlightLine(hl); return ( <React.Fragment> {title && <div sx={{ variant: `styles.CodeBlock.title` }}>{title}</div>} <div sx={{ variant: `styles.CodeBlock` }}> <Highlight {...defaultProps} {...props} code={children.trim()} language={language} theme={undefined} > {({ className, style, tokens, getLineProps, getTokenProps }) => ( <Themed.pre className={`${outerClassName} ${className}`} style={style} > {tokens.map((line, index) => ( <div key={index} {...getLineProps({ line, key: index })} sx={ shouldHighlightLine(index) ? { variant: `styles.CodeBlock.highlightLine` } : undefined } > {line.map((token, key) => ( <span key={key} {...getTokenProps({ token, key })} // https://github.com/system-ui/theme-ui/pull/721 sx={token.empty ? { display: `inline-block` } : undefined} /> ))} </div> ))} </Themed.pre> )} </Highlight> </div> </React.Fragment> );};
export default CodeBlock;
import nightOwlLight from "@theme-ui/prism/presets/night-owl-light.json";
const theme = { styles: { CodeBlock: { ...nightOwlLight, overflow: `auto`, pre: { backgroundColor: `transparent`, float: `left`, minWidth: `100%`, margin: 0, }, highlightLine: { backgroundColor: `#f0f0f0`, borderLeftColor: `#49d0c5`, borderLeftStyle: `solid`, borderLeftWidth: `0.25em`, display: `block`, marginRight: `-1em`, marginLeft: `-1em`, paddingRight: `1em`, paddingLeft: `0.75em`, }, title: { fontFamily: `mono`, backgroundColor: nightOwlLight.backgroundColor, borderBottomWidth: `2px`, borderBottomStyle: `solid`, borderBottomColor: `#f0f0f0`, color: nightOwlLight.color, }, }, },};
export default theme;