Skip to navigation
2-3 minutes read
By Titus Wormer

Syntax highlighting

This guide explores how to apply syntax highlighting to code blocks. MDX supports standard markdown syntax (CommonMark). It does not apply syntax highlighting to code blocks by default.

There are two ways to accomplish syntax highlighting: at compile time or at runtime. Doing it at compile time means the effort is spent upfront so that readers will have a fast experience as no extra code is sent to them (syntax highlighting needs a lot of code to work). Doing it at runtime gives more flexibility by moving the work to the client. This can result in a slow experience for readers though. It also depends on what framework you use (as in it’s specific to React, Preact, Vue, etc.)

Syntax highlighting at compile time

Use for example rehype-starry-night (starry-night), rehype-highlight (lowlight, highlight.js), or @mapbox/rehype-prism (refractor, prism) by doing something like this:

example.js
import {compile} from '@mdx-js/mdx'
import rehypeStarryNight from 'rehype-starry-night'

const code = `~~~js
console.log(1)
~~~`

console.log(
  String(await compile(code, {rehypePlugins: [rehypeStarryNight]}))
)
(alias) function compile(vfileCompatible: Readonly<Compatible>, compileOptions?: Readonly<CompileOptions> | null | undefined): Promise<VFile>
import compile

Compile MDX to JS.

  • @param vfileCompatible MDX document to parse.
  • @param compileOptions Compile configuration (optional).
  • @return Promise to compiled file.
(alias) function rehypeStarryNight(options?: Readonly<Options> | null | undefined): (tree: Root, file: VFile) => Promise<Root>
import rehypeStarryNight

Plugin to highlight code with starry-night.

  • @param options Configuration (optional).
  • @returns Transform.
const code: "~~~js\nconsole.log(1)\n~~~"
namespace console
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
  • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without calling require('console').

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:

console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
//   Error: Whoops, something bad happened
//     at [eval]:5:15
//     at Script.runInThisContext (node:vm:132:18)
//     at Object.runInThisContext (node:vm:309:38)
//     at node:internal/process/execution:77:19
//     at [eval]-wrapper:6:22
//     at evalScript (node:internal/process/execution:76:60)
//     at node:internal/main/eval_string:23:3

const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr

Example using the Console class:

const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);

myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err

const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
(method) Console.log(message?: any, ...optionalParams: any[]): void

Prints to stdout with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout

See util.format() for more information.

  • @since v0.1.100
var String: StringConstructor
(value?: any) => string

Allows manipulation and formatting of text strings and determination and location of substrings within strings.

(alias) compile(vfileCompatible: Readonly<Compatible>, compileOptions?: Readonly<CompileOptions> | null | undefined): Promise<VFile>
import compile

Compile MDX to JS.

  • @param vfileCompatible MDX document to parse.
  • @param compileOptions Compile configuration (optional).
  • @return Promise to compiled file.
const code: "~~~js\nconsole.log(1)\n~~~"
(property) rehypePlugins?: PluggableList | null | undefined

List of rehype plugins (optional).

(alias) function rehypeStarryNight(options?: Readonly<Options> | null | undefined): (tree: Root, file: VFile) => Promise<Root>
import rehypeStarryNight

Plugin to highlight code with starry-night.

  • @param options Configuration (optional).
  • @returns Transform.
Expand equivalent JSX
output.jsx
<>
  <pre>
    <code className="language-js">
      <span className="pl-en">console</span>.<span className="pl-c1">log</span>(<span className="pl-c1">1</span>)
    </code>
  </pre>
</>

Important: you must likely also include CSS somewhere on the page. See the documentation of the plugin you’re using for more information.

Syntax highlighting at run time

Use for example react-syntax-highlighter, by doing something like this:

example.jsx
import SyntaxHighlighter from 'react-syntax-highlighter'
import Post from './example.mdx' // Assumes an integration is used to compile MDX -> JS.

console.log(<Post components={{code}} />)

function code({className, ...properties}) {
  const match = /language-(\w+)/.exec(className || '')
  return match
    ? <SyntaxHighlighter language={match[1]} PreTag="div" {...properties} />
    : <code className={className} {...properties} />
}
Expand equivalent JSX
output.jsx
<>
  <pre>
    <div
      className="language-js"
      style={{
        background: '#F0F0F0',
        color: '#444',
        display: 'block',
        overflowX: 'auto',
        padding: '0.5em'
      }}
    >
      <code style={{whiteSpace: 'pre'}}>
        <span>console.</span>
        <span style={{color: '#397300'}}>log</span>
        <span>(</span>
        <span style={{color: '#880000'}}>1</span>
        <span>)</span>
      </code>
    </div>
  </pre>
</>

Syntax highlighting with the meta field

Markdown supports a meta string for code:

example.mdx
```js filename="index.js"
console.log(1)
```

The meta part is everything after the language (in this case, js). This is a hidden part of markdown: it’s normally ignored. But as the above example shows, it’s a useful place to put some extra fields.

@mdx-js/mdx doesn’t know whether you’re handling code as a component or what the format of that meta string is, so it defaults to how markdown handles it: meta is ignored.

But what if you want to access meta at runtime? That’s exactly what the rehype plugin rehype-mdx-code-props does. It lets you type JSX attributes in the meta part which you can access by with a component for pre.

That plugin, like all rehype plugins, can be passed as rehypePlugins in ProcessorOptions. More info on plugins is available in § Extending MDX