erwannrousseau.dev

GitHubLinkedIn
post image How to use Shiki 式 Syntax highlighter with NextJS and Tailwind

How to use Shiki 式 Syntax highlighter with NextJS and Tailwind

Set Up Your Next.js Project

First things first—if you don’t have a Next.js project yet, start with a bit of magic:

pnpm dlx create-next-app@latest code-block --ts --tailwind --app --use-pnpm
Answer a few bootstrap questions.

And voilà! You've got a fresh new project, ready to be enhanced with a awesome CodeBlock component.

Download Your Favorite VS Code Theme Files

Go into your VS Code editor and find your favorite theme. A big shout-out to the creator of the Pace Theme for that awesome style!

Next, copy your theme files in json format into your Next.js project, maybe in lib/themes.

For a clean structure, place them in a dedicated folder. Here’s an example for a dual theme setup:

./lib/themes/dark.json and ./lib/themes/light.json

Install Shiki

pnpm add shiki

Create the highlightCode Function

Now for the real deal: let’s write a highlightCode function that will turn your code into a work of art. Add this gem:

Typescript
lib/highlight-code.ts
import { promises as fs } from "node:fs";
import path from "node:path";
import { createHighlighter } from "shiki";

export async function highlightCode(code: string, language: string) {
  const darkTheme = await fs.readFile(
    path.join(process.cwd(), "lib/themes/dark.json"),
    "utf-8",
  );

  const lightTheme = await fs.readFile(
    path.join(process.cwd(), "lib/themes/light.json"),
    "utf-8",
  );

  const highlighter = await createHighlighter({
    themes: [JSON.parse(darkTheme), JSON.parse(lightTheme)],
    langs: [language],
  });

  const html = highlighter.codeToHtml(code, {
    lang: language,
    themes: {
      light: "Pace Light",
      dark: "Pace Dark",
    },
  });

  highlighter.dispose();

  return html;
}

Create a Server Component to Inject Styled Code

Now, let's create a component that will allow us to inject all this stylish code right into our page! Create code-block.tsx in your components folder:

TSX
code-block.tsx
import type React from "react";
import { highlightCode } from "@/lib/highlight-code";

interface CodeBlockWrapperProps {
  code: string;
  language: string;
}

export default async function CodeBlock({
  code,
  language,
}: CodeBlockWrapperProps) {
  const highlightedCode = await highlightCode(code, language);

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

Create a Client-Side Wrapper for Top-Notch Style

Let’s add a client wrapper to bring your code block to life with a copy option, scrolling, and a few other neat extras. Here’s the CodeBlockWrapper component:

TSX
code-block-wrapper.tsx
"use client";

import { CheckIcon, Copy } from "lucide-react";
import React from "react";

interface CodeBlockWrapperProps extends React.HTMLAttributes<HTMLElement> {
  code: string;
  filename?: string;
}

export function CodeBlockWrapper({
  children,
  code,
  filename,
  ...props
}: CodeBlockWrapperProps) {
  const [isOpened, setIsOpened] = React.useState(false);
  const [hasCopied, setHasCopied] = React.useState(false);

  React.useEffect(() => {
    const timeoutId = setTimeout(() => {
      setHasCopied(false);
    }, 2000);
    return () => clearTimeout(timeoutId);
  }, [hasCopied]);

  const copyToClipboard = () => {
    setHasCopied(true);
    navigator.clipboard.writeText(code);
  };

  return (
    <figure
      className="group relative my-4 overflow-y-hidden rounded-md border focus-visible:outline-none"
      {...props}
    >
      {filename && (
        <div className="flex flex-row items-center justify-between gap-2 border-b px-4 py-1.5">
          <figcaption>{filename}</figcaption>
          <button
            type="button"
            onClick={copyToClipboard}
            className="size-6 [&_svg]:size-4"
          >
            <span className="sr-only">Copy</span>
            {hasCopied ? <CheckIcon /> : <Copy />}
          </button>
        </div>
      )}
      <div className="overflow-auto">
        <div className="table min-w-full">
          {!filename && (
            <button
              type="button"
              onClick={copyToClipboard}
              className="absolute top-2 right-4 size-6 [&_svg]:size-4"
            >
              <span className="sr-only">Copy</span>
              {hasCopied ? <CheckIcon /> : <Copy />}
            </button>
          )}
          <div
            className={`w-full rounded-md font-mono text-sm leading-relaxed [&_pre]:p-4 ${
              !isOpened ? "max-h-[350px]" : ""
            }`}
          >
            {children}
          </div>

          {code.split("\n").length > 10 && (
            <div className="absolute inset-x-0 bottom-0 flex items-center justify-center bg-gradient-to-b from-transparent to-gray-100 p-2 dark:to-gray-700">
              <button
                type="button"
                onClick={() => setIsOpened(!isOpened)}
                className="text-pink-400 text-xs"
              >
                {isOpened ? "Collapse" : "Expand"}
              </button>
            </div>
          )}
        </div>
      </div>
    </figure>
  );
}

Configure Dark and Light Themes for Shiki

Before using our CodeBlockWrapper component, we need to add a little CSS so that the dark and light themes automatically adjust.

Add this code to your global CSS file:

/* SHIKI */
html.dark .shiki,
html.dark .shiki span {
  color: var(--shiki-dark) !important;
  background-color: var(--shiki-dark-bg) !important;
  font-style: var(--shiki-dark-font-style) !important;
  font-weight: var(--shiki-dark-font-weight) !important;
  text-decoration: var(--shiki-dark-text-decoration) !important;
}

Use the Component and Admire the Results

Finally, it’s showtime! Call CodeBlockWrapper in your component, pass in your code, and enjoy the view:

<CodeBlockWrapper
  code={code}
  filename={filename}
>
  <CodeBlock code={code} language={language} />
</CodeBlockWrapper>
P.S.: All the code blocks on my portfolio are generated like this! Not too shabby, right?

And that’s it! Your code is now ready to shine with Shiki, and in the theme of your choice. 🎉

If you want to dive deeper into the code, check out the repo from my portfolio, where I walk through exactly how it’s done: components/utils/code-block-wrapper.tsx