Shiki Highlighter

The main highlighter utility to render syntax highlighted code.

ShikiMarkdownMDXClient

Shiki is a syntax highlighting library based on TextMate grammars. It transforms source code into accurate, theme-aware HTML, matching the highlighting used in VS Code.

Shiki Highlighter
export default function Page() {
  return <h1>Hello</h1>
}

Installation

shadcn/ui

shadcn/ui Command
pnpm dlx shadcn@latest add https://code-blocks.pheralb.dev/r/shiki-highlighter.json

Manual

  1. Install the following dependencies:
Shiki Dependencies
pnpm i shiki -D

As optional, you can also install separate packages for themes and languages:

Shiki Themes and Languages
pnpm i @shikijs/themes @shikijs/langs -D
  1. Create a your shiki utility file:
.ts
import { createJavaScriptRegexEngine } from "shiki/engine/javascript";
import {
  type HighlighterCore,
  type RegexEngine,
  createHighlighterCore,
} from "shiki/core";

// Themes:
import lightTheme from "@shikijs/themes/one-light";
import darkTheme from "@shikijs/themes/one-dark-pro";

// Languages:
import html from "@shikijs/langs/html";
import js from "@shikijs/langs/js";
import ts from "@shikijs/langs/ts";
import tsx from "@shikijs/langs/tsx";
import css from "@shikijs/langs/css";
import json from "@shikijs/langs/json";
import bash from "@shikijs/langs/bash";
import markdown from "@shikijs/langs/mdx";

let jsEngine: RegexEngine | null = null;
let highlighter: Promise<HighlighterCore> | null = null;

// Settings for UI components
const Themes = {
  light: "one-light",
  dark: "one-dark-pro",
};

type Languages = "html" | "js" | "ts" | "tsx" | "css" | "bash" | "json" | "mdx";

const getJsEngine = (): RegexEngine => {
  jsEngine ??= createJavaScriptRegexEngine();
  return jsEngine;
};

const highlight = async (): Promise<HighlighterCore> => {
  highlighter ??= createHighlighterCore({
    themes: [lightTheme, darkTheme],
    langs: [bash, js, ts, tsx, css, markdown, html, json],
    engine: getJsEngine(),
  });
  return highlighter;
};

export { highlight, Themes, type Languages };

For better performance, this utility only imports the languages and themes that you will use. If you don't prefer importing specific items, you can use shiki/bundle/web directly:

.ts
import {
  type Highlighter,
  type RegexEngine,
  createHighlighter,
} from "shiki/bundle/web";

let highlighter: Promise<Highlighter> | null = null;

// ...

const highlight = async (): Promise<Highlighter> => {
  highlighter ??= createHighlighter({
    langs: ["html", "js", "ts", "tsx", "css", "bash", "mdx"],
    themes: ["one-light", "one-dark-pro"],
    engine: getJsEngine(),
  });
  return highlighter;
};

This utility exports:

  • highlight(): A function that initializes and returns a Shiki highlighter instance with the specified theme and languages.
  • Themes: A type with the available themes. You can use it in your UI components to type the theme prop.
  • Languages: A type with the available languages. You can use it in your UI components to type the language prop.

Styles

This CSS file includes light/dark mode and all transformers styles:

.css
/* Tailwind CSS */
@import "./globals.css";

/* Shiki Light/Dark Mode */
html.light .shiki,
html.light .shiki span {
  font-family: var(--font-mono);
  background-color: transparent !important;
}

html.dark .shiki,
html.dark .shiki span {
  font-family: var(--font-mono);
  color: var(--shiki-dark) !important;
  background-color: transparent !important;
}

/* Base Shiki Pre & Span Styles */
pre.shiki {
  @apply py-3;
}

pre.shiki span.line {
  @apply px-4 py-0.5;
}

/* Shiki Word Wrap */
pre.shiki-word-wrap {
  white-space: pre-wrap;
  word-break: break-word;
}

pre.shiki-word-wrap span.line {
  display: inline-block;
  width: 100%;
  box-sizing: border-box;
  padding-top: 0.2px;
  padding-bottom: 0.2px;
}

/* Shiki Line Numbers */
pre.shiki-line-numbers code {
  counter-reset: step;
  counter-increment: step 0;
}

pre.shiki-line-numbers code:has(.line:nth-child(2)) .line::before {
  content: counter(step);
  counter-increment: step;
  width: 0.5rem;
  margin-right: 1.3rem;
  margin-left: 0.2rem;
  display: inline-block;
  @apply text-right font-mono text-xs text-neutral-500;
}

/* Shiki Highlight */
pre span.shiki-line-highlight {
  @apply relative z-0 inline-block w-full;
  &::after {
    content: "";
    @apply absolute top-0 left-0 -z-10 h-full w-full border-l-2 border-neutral-400 bg-neutral-500/20! opacity-40;
  }
}

/* Shiki Notation Diff */
pre.has-diff span.line.diff {
  @apply relative inline-block w-full;
}

pre.has-diff span.line.diff.add {
  @apply bg-green-300/20 dark:bg-green-700/20;
  &::before {
    content: "+";
    @apply absolute left-2 text-green-600 dark:text-green-400;
  }
}

pre.has-diff span.line.diff.remove {
  @apply bg-red-300/20 opacity-70 dark:bg-red-600/20;
  &::before {
    content: "-";
    @apply absolute left-2 text-red-600 dark:text-red-400;
  }
}

/* Shiki Notation Focus */
pre.shiki-has-focused .line:not(.focused) {
  @apply opacity-50 blur-[0.8px] transition-opacity duration-200 ease-in-out;
}

pre.shiki-has-focused:hover .line:not(.focused) {
  @apply opacity-100 blur-none;
}

/* Shiki Line Anchors */
pre.shiki-line-anchors {
  scroll-margin-top: 2rem;
}

pre.shiki-line-anchors .line:target {
  @apply relative inline-block w-full bg-blue-300/15 dark:bg-blue-500/15;
  &::before {
    content: "";
    @apply absolute left-0 top-0 h-full w-1 bg-blue-500 dark:bg-blue-400;
  }
}

pre.shiki-line-numbers.shiki-line-anchors code .line::before {
  @apply cursor-pointer select-none transition-colors;
}

pre.shiki-line-numbers.shiki-line-anchors code .line::before:hover {
  @apply text-blue-500 underline dark:text-blue-400;
}

Usage

You can use the highlight function to get syntax highlighted code. For example:

Shiki Highlighter Usage
import { highlight } from "@/utils/shiki";

const code = `console.log('hello')`;
const highlighter = await highlight();
const html = highlighter.codeToHtml(code, {
  lang: "javascript",
  theme: "one-light",
});

Themes

There are multiple themes available in the @shikijs/themes package. You can find the full list of themes in the Shiki Themes documentation.

If you need to use a different theme:

  1. Update the import statements in the shiki-highlighter.ts file:
.ts
// Themes:
import lightTheme from "@shikijs/themes/one-light"; // Light Theme
import darkTheme from "@shikijs/themes/one-dark-pro"; // Dark Theme
  1. Update the Themes type:
.ts
type Themes = "one-light" | "one-dark-pro";

Languages

There are multiple languages available in the @shikijs/langs package. You can find the full list of languages in the Shiki Languages documentation.

If you need to use different languages:

  1. Update the import statements in the shiki-highlighter.ts file:
.ts
// Languages:
import js from "@shikijs/langs/js";
import ts from "@shikijs/langs/ts";
import tsx from "@shikijs/langs/tsx";
//...
  1. Update the Languages type:
.ts
type Languages = "js" | "ts" | "tsx" | ... ;

Markdown/MDX Integration

  1. Install the @shikijs/rehype package:
Shiki Rehype Dependency
pnpm i @shikijs/rehype -D
  1. Create a rehypeShikiOptions utility to configure the MDX plugin:
.tsx
import type { RehypeShikiCoreOptions } from "@shikijs/rehype/core";

const rehypeShikiOptions: RehypeShikiCoreOptions = {
  themes: {
    light: "one-light",
    dark: "one-dark-pro",
  },
};

export { rehypeShikiOptions };
  1. Use the highlight and rehypeShikiOptions in your MDX setup as Rehype plugin:
.ts
import rehypeShiki from "@shikijs/rehype/core";
import { highlight } from "@/utils/shiki-highlighter";
import { rehypeShikiOptions } from "@/utils/rehype-shiki-options";

const highlighter = await highlight();

const mdx = await compileMDX(context, document, {
  rehypePlugins: [[rehypeShiki, highlighter, rehypeShikiOptions]],
});