How to use Shiki 式 Syntax highlighter with Next.js and Tailwind CSS 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:
Copy to clipboard pnpm create next-app 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 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 Copy to clipboard 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 ;
}
Expand
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:
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 } }
/>
) ;
}
Expand
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 Copy to clipboard "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 >
) ;
}
Expand
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:
Copy to clipboard /* 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:
Copy to clipboard < 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