TechniqueMDXPrism

How to implement Syntax Highlighting in Code Blocks

2020-12-12Chris Tham

Why have boring code blocks in Markdown when you can have them styled in accordance with your website and with syntax highlighting based on the code language.

hero

Update (20 July 2022)

Please note that after migrating this site to MDX V2, I am no longer using this method for syntax highlighting, but using rehype-highlight instead. The rest of this article has been retained for historical purposes, as it illustrates an interesting technique that I am no longer using.

Original Article (for historical purposes)

You will notice that articles in this website look like this, with a wider than normal block, syntax highlighting, line numbers and a colour theme consistent with the rest of the site:

import React, { useState } from 'react'

function Example() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}

This is quite easily implemented using a custom Code Block component that is inserted in the MDX Provider embedded in pages/posts/[id.tsx]

const components = {
  a: A,
  code: CodeBlock
}

interface PostProps {
  id: string
  url: string
  source: Source
  frontMatter: FrontMatter
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const Post: React.FC<PostProps> = ({ id, url, source, frontMatter }) => {
  const content = hydrate(source, { components })
  return (
    <BlogLayout url={url} meta={frontMatter}>
      {content}
    </BlogLayout>
  )
}

So, in MDX posts, any a link is replaced by our customised A component and any code block uses our customised CodeBlock component which is defined like this:

const CodeBlock: React.FC<{
  children: string
  className: string
  live: string
  render: string
}> = ({ children, className, live, render }) => {
  const language = className ? className.replace(/language-/, '') : 'bash'

  if (live) {
    return (
      <div className="mt-10 bg-rosely5">
        <LiveProvider
          code={children.trim()}
          transformCode={(code) => '/** @jsx mdx */' + code}
          scope={{ mdx }}
        >
          <LivePreview />
          <LiveEditor />
          <LiveError />
        </LiveProvider>
      </div>
    )
  }

  if (render) {
    return (
      <div className="mt-10">
        <LiveProvider code={children}>
          <LivePreview />
        </LiveProvider>
      </div>
    )
  }

  return (
    <Highlight
      {...defaultProps}
      code={children.trim()}
      theme={theme}
      language={language as Language}
    >
      {({ className, style, tokens, getLineProps, getTokenProps }) => (
        <pre
          className={`${className} text-left mt-4 mr-0 p-2 overflow-scroll text-lg`}
          style={style}
        >
          {tokens.map((line, i) => (
            <div className="table-row" key={i} {...getLineProps({ line, key: i })}>
              <span className="table-cell text-right pr-4 select-none opacity-50 text-xs">
                {i + 1}
              </span>
              <span className="table-cell">
                {line.map((token, key) => (
                  <span key={key} {...getTokenProps({ token, key })} />
                ))}
              </span>
            </div>
          ))}
        </pre>
      )}
    </Highlight>
  )
}

Note that this relies on prism-react-renderer and a custom theme which is defined like this:

const theme: PrismTheme = {
  plain: {
    backgroundColor: rosely5,
    color: rosely1,
    fontSize: '0.9rem'
  },
  styles: [
    {
      types: ['comment', 'prolog', 'doctype', 'cdata', 'punctuation'],
      style: {
        color: rosely3
      }
    },
    {
      types: ['namespace'],
      style: {
        opacity: 0.7
      }
    },
    {
      types: ['tag', 'operator', 'number'],
      style: {
        color: rosely10
      }
    },
    {
      types: ['property', 'function'],
      style: {
        color: rosely2
      }
    },
    {
      types: ['tag-id', 'selector', 'atrule-id'],
      style: {
        color: rosely15
      }
    },
    {
      types: ['attr-name'],
      style: {
        color: rosely14
      }
    },
    {
      types: [
        'boolean',
        'string',
        'entity',
        'url',
        'attr-value',
        'keyword',
        'control',
        'directive',
        'unit',
        'statement',
        'regex',
        'at-rule'
      ],
      style: {
        color: rosely7
      }
    },
    {
      types: ['placeholder', 'variable'],
      style: {
        color: rosely9
      }
    },
    {
      types: ['deleted'],
      style: {
        textDecorationLine: 'line-through'
      }
    },
    {
      types: ['inserted'],
      style: {
        textDecorationLine: 'underline'
      }
    },
    {
      types: ['italic'],
      style: {
        fontStyle: 'italic'
      }
    },
    {
      types: ['important', 'bold'],
      style: {
        fontWeight: 'bold'
      }
    },
    {
      types: ['important'],
      style: {
        color: rosely11
      }
    }
  ]
}

The following languages are supported by default:

module.exports = {
  markup: true,
  bash: true,
  clike: true,
  c: true,
  cpp: true,
  css: true,
  "css-extras": true,
  javascript: true,
  jsx: true,
  "js-extras": true,
  coffeescript: true,
  diff: true,
  git: true,
  go: true,
  graphql: true,
  handlebars: true,
  json: true,
  less: true,
  makefile: true,
  markdown: true,
  objectivec: true,
  ocaml: true,
  python: true,
  reason: true,
  sass: true,
  scss: true,
  sql: true,
  stylus: true,
  tsx: true,
  typescript: true,
  wasm: true,
  yaml: true
};

How is the code block itself styled? Because I use Tailwind Typography, I ended up customising the tailwind.config.js file using a Technique described in Full Width Containers in Limited Width Parents:

pre: {
  color: '#27272a',
  backgroundColor: '#f4dede',
  overflowX: 'auto',
  left: '50%',
  marginLeft: '-40vw',
  marginRight: '-40vw',
  maxWidth: '80vw',
  position: 'relative',
  right: '50%',
  width: '80vw'
}

Subscribe to get updates to this site!

Like my articles? Enter your details and I will send you an email whenever the site has new content. I will not use your email for any other purpose.

Subscribe