# Next.js# Shiki# Syntax highlighting# Tailwind CSS

Modern Syntax Highlighting with Shiki in Next.js 14

Learn how to implement Shiki for modern and performant syntax highlighting in your React and Next.js projects with this comprehensive step-by-step guide.

, 6m read

Post image for modern syntax highlighting with Shiki in Next.js
Last updated on .

For integrating code blocks on this website I was looking for a modern approach to syntax highlighting and came across Shiki, a pretty new syntax highlighter created by Pine Wu and maintained and rewritten by Anthony Fu. It is used by big companies like Vercel for their Next.js docs or Astro for their syntax highlighting. As I couldn't find a guide myself for the app router of Next.js 13 and above, I thought I'll write one by myself.

What we build

Let's have a look at what we are going to build today. A code block with syntax highlighting that enables us to also highlight certain lines and even show code diffs. The following code block is an example.

AvatarImage.tsx
"use client";

import { useRouter } from "next/navigation";
import ArrowCircleLeft from "@/assets/icons/unicons/arrow-circle-left.svg";
import { cn } from "@/lib/utils";

export default function GoBackButton({
  className,
  text = "Go back",
}: {
  className?: string;
  text?: string;
}) {
  const router = useRouter();
  return (
    <button
      onClick={() => router.back()}
      className={cn(
        "group/back inline-flex justify-start gap-2 justify-self-start",
        className,
      )}
    >
      <ArrowCircleRight className="size-6" />
      <ArrowCircleLeft className="size-6" />
      <span className="group-hover/back:underline group-hover/back:underline-offset-4">
        {text}
      </span>
    </button>
  );
}

The final code

If you are just interested in the final outcome, you can find the code in my repository nikolailehbrink/shiki-next.

Info

There I also added the „Copy to Clipboard“ functionality, which I am not going to talk about in this blog post because I felt it was out of scope.

Why Shiki?

Choosing Shiki for syntax highlighting in React and especially Next.js apps can be a smart move for many reasons. First, Shiki is based on TextMate grammars, the same system used by Visual Studio Code, ensuring accurate and visually appealing code highlighting. More importantly for the performance of our website, Shiki renders highlighted code ahead of time. This means no extra JavaScript is shipped to the client for syntax highlighting, aligning with Next.js's push for server components and minimal client-side JavaScript.

The results

Faster load times, improved SEO, and a seamless developer experience without sacrificing the quality of our code's presentation.

Project setup

The easiest way to get started with Next.js is by using create-next-app. This CLI tool enables you to quickly start building a new Next.js application, with everything set up for you.

Terminal
npx create-next-app@latest

Install Shiki

In your projects folder, open the terminal and add shiki as a dev dependency with the following command:

Terminal
npm install -D shiki

Create the code component

Now that we have installed everything we can integrate shiki. So let's create a new folder called components in which we create a new file called Code.tsx which will be our code server component. Let's add a basic structure for this functional component.

components/Code.tsx
export default function Code() {
  return <div>Code</div>;
}

Integrate shiki

In order to make the syntax highlighting work, we have to use the codeToHtml function that shiki provides. Because of the asynchronous nature of this function we have to refactor the code to make the component an async function. Then call the codeToHtml function with the code that should be formatted, the language and the theme as an argument. Lastly we set the inner HTML of the <div> to the rendered HTML from shiki with dangerouslySetInnerHTML:

components/Code.tsx
import { codeToHtml } from "shiki";

export default async function Code() {
  const html = await codeToHtml("const a = 1 + 3", {
    lang: "javascript",
    theme: "nord",
  });

  return <div dangerouslySetInnerHTML={{ __html: html }}></div>;
}

Display component

Now open your root page app/page.tsx, delete all of it's initial content and add the code component to the page.

app/page.tsx
import Code from "@/components/Code";

export default function Home() {
  return (
    <>
      <Code />
    </>
  );
}

When running the development server (typically on localhost:3000),we see a code block with syntax highlighting applied, which is rendered on the server.

Pretty cool and easy right?

Make component reusable

But we don't stop there, because right now everything is hard-coded and that's not the purpose of this tutorial. So let's extend our component to take the code, the language (lang) and the theme as props.

components/Code.tsx
import { codeToHtml } from "shiki";
import type { BundledLanguage, BundledTheme } from "shiki"; // Import the types from shiki

type Props = {
  code: string;
  lang?: BundledLanguage;
  theme?: BundledTheme;
};

export default async function Code({
  code,
  lang = "javascript",
  theme = "nord",
}: Props) {
  
  const html = await codeToHtml(code, {
    lang,
    theme,
  });

  return (
    <div
      dangerouslySetInnerHTML={{ __html: html }}
    ></div>
  );
}

Then inside our page.tsx we can call the Code component and pass down the props. Also note that thanks to TypeScript we now have autosuggestions by the editor for the lang and theme props.

app/page.tsx
import Code from "@/components/Code";

export default function Home() {
  return (
    <>
      <Code code="let a = 1 + 4" />
      <Code code="console.log('Hello, world!')" lang="typescript" />
      <Code
        code={`fn main() { println!(\"Hello, world!\"); }`}
        lang="rust"
        theme="github-dark"
      />
    </>
  );
}

When opening the page you should see three blocks with different syntax highlighting applied (I adjusted my font size heavily for the following images):

Syntax highlighted code

Enhancing the component

The basic functionality is done, now let's add some enhancements to our code block.

Highlighting Specific Lines

One thing that I definitely didn't want to miss out on, was an implementation of line highlighting. Shiki has a few transformers that let us easily set this up.

First, install the common transformers package for Shiki:

Terminal
npm i -D @shikijs/transformers

Then import the transformerNotationHighlight function:

components/Code.tsx
import { codeToHtml } from "shiki";
import { 
  transformerNotationHighlight,
} from "@shikijs/transformers";
import type { BundledLanguage, BundledTheme } from "shiki";

type Props = {
  code: string;
  lang?: BundledLanguage;
  theme?: BundledTheme;
};
export default async function Code({
  code,
  lang = "javascript",
  theme = "nord",
}: Props) {

  const html = await codeToHtml(code, {
    lang,
    theme,
    transformers: [transformerNotationHighlight()],
  });

  return <div dangerouslySetInnerHTML={{ __html: html }}></div>;
}

Add a comment like // [!code highlight] in the code prop to add a highlighted class to the rendered HTML.

app/page.tsx
import Code from "@/components/Code";

export default function Home() {
  return (
    <>
      <Code code="let a = 1 + 4" />
      <Code code="console.log('Hello, world!')" lang="typescript" />
      <Code
        code={`fn main() { println!(\"Hello, world!\"); } \\ [!code highlight] -> instead of \\ do //`}
        lang="rust"
        theme="github-dark"
      />
    </>
  );
}

If you inspect your code now in the Developer Tools you can see that the highlighted class was added to that specific <span>:

DOM
<pre
  class="shiki github-dark has-highlighted"
  style="background-color: #24292e; color: #e1e4e8"
  tabindex="0"
>
  <code>
    <span class="line highlighted">...</span>
  </code>
</pre>

Now we can target the class with CSS and style it to our liking. I will demonstrate it in a simple way with TailwindCSS and it's arbitrary values.

components/Code.tsx
...
return (
  <div
    className="[&_.highlighted]:bg-blue-700"
    dangerouslySetInnerHTML={{ __html: html }}
  ></div>
);
...

Now your code block should indicate the highlighted line with a blue background.

Code component with highlighted line

This isn't the prettiest code block yet, but we will focus on styling in the end. Let's look at a different transformer, with which we can show code changes with, which you might have seen on sites like GitHub.

Showing Code Changes

To add classes for added and removed lines, we follow the same steps as above, but with a different transformer called transformerNotationDiff. First import the transformer.

components/Code.tsx
import { codeToHtml } from "shiki";
import {
  transformerNotationHighlight,
  transformerNotationDiff,
} from "@shikijs/transformers";
...
export default async function Code({
  code,
  lang = "javascript",
  theme = "nord",
}: Props) {
    ...
    transformers: [transformerNotationHighlight()],
    transformers: [transformerNotationHighlight(), transformerNotationDiff()],
  ...
}

Then add the // [!code ++] comment for added lines and a respective // [!code --] comment for removed lines to your code prop.

app/page.tsx
import Code from "@/components/Code";

export default function Home() {
  return (
    <>
      <Code code="let a = 1 + 4 \\ [!code --] -> use // instead of \\" />
      <Code
        code="console.log('Hello, world!') \\ [!code ++] -> use // instead of \\"
        lang="typescript"
      />
      ...
    </>
  );
}

This again adds the diff remove and diff add classes on the rendered HTML that we can use for styling the component later.

DOM
<pre class="shiki nord has-diff" style="background-color:#2e3440ff;color:#d8dee9ff" tabindex="0">
    <code>
        <span class="line diff remove">...</span>
        <span class="line diff add">...</span>
    </code>
</pre>

Including Filenames

Besides the highlighting I wanted to add filenames to my code components. That way the reader always knows to which file the given code belongs to.

So let's add a filename prop to our component and some initial styling to see the effect.

components/Code.tsx
...

type Props = {
  ...
  filename?: string;
};

export default async function Code({
  ...
  filename,
}: Props) {
  ...

  return (
    <div>
      <div className="bg-neutral-800">
        {filename && ( 
          <div className="bg-neutral-900 text-sm inline-flex py-2 px-4">
            {filename}
          </div>
        )}
      </div>
      <div
        className="[&_.highlighted]:bg-blue-700"
        dangerouslySetInnerHTML={{ __html: html }}
      ></div>
    </div>
  );
}

In our page.tsx let's add filenames by passing a name to the filename prop.

app/page.tsx
import Code from "@/components/Code";

export default function Home() {
  return (
    <>
      <Code code="let a = 1 + 4" />
      <Code code="let a = 1 + 4" filename="index.js" />
       <Code
        ...
        filename="index.ts"
      />
      <Code
        ...
        filename="main.rs"
      />
    </>
  );
}

Now we should see the filenames on top of the code block.

Code component with filenames

Customizing appearance

We have all the functionality implemented, so we can move over to the fun part and style our code block.

Integrate Line Numbers

One common thing to integrate in a code block are line numbers, making the component visually more appealing while also giving the reader a reference for specific lines in the code.

Info

In Issue #3 from Shiki this was discussed and a user called Alex Peattie presented a pretty awesome CSS solution to line numbers in shiki.

I refactored the code from the solution to TailwindCSS utility classes in app/globals.css.

app/globals.css
.shiki { // class assigned by shiki
  counter-reset: step;
  counter-increment: step 0;
  .line {
    &::before {
      counter-increment: step;
      @apply mr-6 inline-block w-4 border-transparent text-right text-neutral-600 content-[counter(step)];
    }
  }
}

To make the nested css work, we have to add the tailwindcss/nesting in our postcss.config.js.

Tip

You don't have to install the plugin as it already comes packed with TailwindCSS which was preinstalled via create-next-app.

postcss.config.js
module.exports = {
  plugins: {
    "tailwindcss/nesting": {},
    tailwindcss: {},
    autoprefixer: {},
  },
};

Now the code block should have line numbers:

Code Block with line numbers

Styling Highlights and Diffs

In order to enhance the design of the individual highlighted lines, I added the following styles:

app/globals.css
.shiki {
  counter-reset: step;
  counter-increment: step 0;
  .line {
    @apply border-l-4 border-transparent; 
    &::before {
      counter-increment: step;
      @apply mr-6 inline-block w-4 border-transparent text-right text-neutral-600 content-[counter(step)];
    }
    &.highlighted, 
    &.diff { 
      @apply -ml-4 -mr-5 inline-block w-[calc(100%+(theme(spacing.5)+theme(spacing.4)))] pl-4 pr-5; 
    } 
    &.highlighted { 
      @apply border-neutral-500 bg-neutral-800; 
    } 
    &.diff { 
      &.add, 
      &.remove { 
        span:first-child::before { 
          @apply -ml-4 inline-flex w-4; 
        } 
      } 
      &.add { 
        @apply border-blue-500 bg-blue-500/25 before:text-blue-500; 
        span:first-child::before { 
          @apply text-blue-500 content-["+"]; 
        } 
      } 
      &.remove { 
        @apply border-orange-500 bg-orange-500/30 opacity-70 *:!text-neutral-400 before:text-orange-500; 
        span:first-child::before { 
          @apply text-orange-500 content-["-"]; 
        } 
      } 
    } 
  }
}

Final Touch

Lastly, I added some styles to make the whole component more visually appealing and added a background gradient.

components/Code.tsx
...

export default async function Code({
  ...
}: Props) {
  ...
  
  return (
    <div className="rounded-lg bg-gradient-to-r from-sky-200 to-sky-400 p-4 !pr-0 md:p-8 lg:p-12 [&>pre]:rounded-none max-w-xl">
      <div className="overflow-hidden rounded-s-lg">
        <div className="flex items-center justify-between bg-gradient-to-r from-neutral-900 to-neutral-800 py-2 pl-2 pr-4 text-sm">
          <span className="-mb-[calc(0.5rem+2px)] rounded-t-lg border-2 border-white/5 border-b-neutral-700 bg-neutral-800 px-4 py-2 ">
            {filename}
          </span>
        </div>
        <div
          className="border-t-2 border-neutral-700 text-sm [&>pre]:overflow-x-auto [&>pre]:!bg-neutral-900 [&>pre]:py-3 [&>pre]:pl-4 [&>pre]:pr-5 [&>pre]:leading-snug [&_code]:block [&_code]:w-fit [&_code]:min-w-full"
          dangerouslySetInnerHTML={{ __html: html }}
        ></div>
      </div>
    </div>
  );
}

Let's switch once again to our page.tsx, remove all three code blocks and add a new one with more than one line of code. Add some comments like // [!code highlight], // [!code ++] or // [!code --] on different lines to see the styles applied.

app/page.tsx
import Code from "@/components/Code";

export default function Home() {
  return (
    <>
      <Code code="let a = 1 + 4" filename="index.js" />
      <Code
        code="console.log('Hello, world!')"
        lang="typescript"
        filename="index.ts"
      />
      <Code
        code={`fn main() { println!(\\"Hello, world!\\"); }`}
        lang="rust"
        theme="github-dark"
        filename="main.rs"
      />
      <Code
        code={`return (\\ [!code ++] use // instead of \\
  <div className="rounded-lg bg-gradient-to-r from-sky-300 to-sky-500 p-4 !pr-0 md:p-8 lg:p-12 [&>pre]:rounded-none max-w-xl">\\ [!code --] use // instead of \\
    <div className="overflow-hidden rounded-s-lg">\\ [!code highlight] use // instead of \\
      <div className="flex items-center justify-between bg-gradient-to-r from-neutral-900 to-neutral-800 py-2 pl-2 pr-4 text-sm">
        <span className="-mb-[calc(0.5rem+2px)] rounded-t-lg border-2 border-white/5 border-b-neutral-700 bg-neutral-800 px-4 py-2 ">
          {filename}
        </span>
      </div>
      <div
        className="border-t-2 border-neutral-700 text-sm [&>pre]:overflow-x-auto [&>pre]:!bg-neutral-900 [&>pre]:py-3 [&>pre]:pl-4 [&>pre]:pr-5 [&>pre]:leading-snug [&_code]:block [&_code]:w-fit [&_code]:min-w-full"
        dangerouslySetInnerHTML={{ __html: html }}
      ></div>
    </div>
  </div>
);
`}
        lang="tsx"
        theme="ayu-dark"
        filename="app/page.tsx"
      />
    </>
  );
}

You should now see the final code block.

Final code block with applied styles

Bonus: Copy to clipboard functionality

I also incorporated a „Copy to Clipboard“ button into the code components on this site. If you're interested in how to implement this feature, you can check out the complete code on Github.

Tip

The commits in the repository chronologically represent the steps outlined in this tutorial.