Learn Shiki Rehype Pretty Code - Managing Custom Langs & Themes
Episode 12 of 23

Learn Shiki Rehype Pretty Code - Managing Custom Langs & Themes

You will learn to import local grammars and themes, register only the languages actually used, and keep the bundle size down with selective langs and themes selection and lazy loading.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Shiki bundles hundreds of languages and dozens of themes. Loading all of them burdens the bundle, even though most projects only use a handful of languages. In this episode you learn to manage langs and themes so the bundle stays light without losing capability.

Episode 12 covers how to import local grammars and themes, register languages selectively, use language aliases, and lazy loading strategies. With proper management, Shiki's build time stays fast and your static bundle doesn't bloat.

Why Limit the Language List

The Cost of Loading All Grammars

Every grammar is a TextMate file containing hundreds of regex rules. Loading all grammars at once slows startup and adds to the bundle size. Shiki is aware of this and provides several control points: from a full bundle, a web bundle, to per-language imports.

Since v4, languages and themes are split into the @shikijs/langs and @shikijs/themes packages. This lets bundlers split files per language so only what's needed is downloaded.

Choosing the Right Entry Point

  • shiki/bundle/full loads all languages and themes, suitable for generic tooling.
  • shiki/bundle/web loads popular web languages like HTML, CSS, JavaScript, TypeScript, and JSON.
  • shiki/core loads nothing; everything must be imported manually.

For blogs or documentation, shiki/bundle/web is usually enough. Choose shiki/core only if bundle size is extremely critical.

Importing Local Grammars and Themes

Custom Grammar from a File

Grammars don't only come from the bundle; you can load your own .tmLanguage.json. Read the file then put the grammar object into the langs array:

JSLoad a local grammar
import { createHighlighter } from "shiki";
import { readFileSync } from "node:fs";
 
const grammar = JSON.parse(
  readFileSync("./grammars/my-lang.tmLanguage.json", "utf8"),
);
 
const highlighter = await createHighlighter({
  langs: [grammar],
  themes: ["github-dark-default"],
});

A grammar object may carry name, scopeName, and embeddedLangs. Once registered, you can use the grammar name as a language in codeToHtml.

Custom Theme from an Object

A theme is a TextMate object with name, type, and settings properties. You can write it directly or read it from a JSON file:

JSCustom theme
const myTheme = {
  name: "my-theme",
  type: "dark",
  settings: [
    {
      scope: ["comment"],
      settings: { foreground: "#8b949e" },
    },
  ],
};
 
const highlighter = await createHighlighter({
  langs: ["typescript"],
  themes: [myTheme],
});

A custom theme is useful when your brand has its own palette. Its structure is standard TextMate, so tools like VSCode can produce these files.

Registering the Languages You Need

Selective Langs and Themes in rehype-pretty-code

In rehype-pretty-code, you register languages and themes via the langs and themes options:

JSSelective list
const options = {
  theme: "github-dark-default",
  langs: ["ts", "tsx", "js", "css", "json", "bash"],
  themes: ["github-dark-default", "github-light-default"],
};

The langs option accepts a list of language ids to be highlighted across all content. Languages outside this list will be rendered without colors, so make sure the list covers all the languages appearing in your articles.

Language Aliases with langAlias

Some authors use short aliases like js or py. Shiki already recognizes common aliases, but for custom names you can register langAlias:

JSRegister aliases
const highlighter = await createHighlighter({
  langs: ["javascript", "typescript"],
  langAlias: {
    mylang: "typescript",
  },
  themes: ["github-dark-default"],
});

With langAlias, you can write a fenced code block with the mylang language meta and Shiki maps it to TypeScript. This feature keeps authors comfortable with internal project terms.

Keeping the Bundle Size Down

Lazy Loading per Language

Use dynamic import functions so the bundler splits languages into separate chunks loaded only when needed:

JSDynamic import
import { createHighlighterCore } from "shiki/core";
import { createOnigurumaEngine } from "shiki/engine/oniguruma";
 
const highlighter = await createHighlighterCore({
  themes: [
    import("@shikijs/themes/github-dark-default"),
  ],
  langs: [
    import("@shikijs/langs/typescript"),
    () => import("@shikijs/langs/javascript"),
  ],
  engine: createOnigurumaEngine(import("shiki/wasm")),
});

Notice that shiki/core here loads no languages or themes at all. Each dynamic import becomes a separate chunk, so only the grammars actually used are downloaded.

Measuring the Optimization Result

Compare the bundle size before and after selection. The bun run build command will show a size report in the terminal. After being selective, the grammar chunk size usually drops drastically compared to a full bundle that can reach megabytes in uncompressed form.

Conclusion

Key takeaways:

  • Loading all grammars burdens the bundle and slows startup.
  • shiki/bundle/full and shiki/bundle/web provide ready-to-use presets.
  • Custom grammars and themes are imported as objects via the langs and themes options.
  • langAlias maps custom language names to already-registered grammars.
  • shiki/core plus dynamic imports makes each language a separate chunk.
  • Bundle size needs to be measured to validate the optimization results.

In episode 13 you'll learn performance and bundle optimization: the Oniguruma WASM loading mechanism, choosing an engine like @shikijs/engine-oniguruma, the difference between build-time and client-side highlighting, and grammar caching to avoid expensive re-renders.

Learn Shiki Rehype Pretty Code - Managing Custom Langs & Themes | Learn Shiki Rehype Pretty Code