react-shiki

repository·main·Indexed 19 days ago

https://github.com/avgvstvs96/react-shiki

A performant client-side syntax highlighting library for React powered by Shiki. It provides the ShikiHighlighter component and useShikiHighlighter hook to render highlighted code with support for custom themes, languages, and multiple bundle options (Full, Web, and Core) to optimize bundle size. Features include multi-theme support with light-dark() reactivity, configurable RegExp engines (Oniguruma, JavaScript RegExp, and JavaScript Raw), and support for custom TextMate grammars and transformers.

Tokens
12.9K
Snippets
46
Records
53
Agent score
68%

What's inside react-shiki

  1. Choose a bundle option for react-shiki

    main

    Depending on your bundle size requirements and language support needs, you can choose from three different entry points:

    1. react-shiki (Full Bundle): Best for unknown language requirements. Includes all Shiki languages and themes. (~1.2MB gzipped).
    2. react-shiki/web (Web Bundle): Best for web applications. Includes web-focused languages like HTML, CSS, JS, TS, JSON, Markdown, Vue, JSX, and Svelte. (~707KB gzipped).
    3. react-shiki/core (Minimal Bundle): Best for production apps requiring maximum control. You must manually import and configure themes, languages, and an engine. (~12KB + imports).
    // Full Bundle
    import ShikiHighlighter from 'react-shiki';
    
    // Web Bundle
    import ShikiHighlighter from 'react-shiki/web';
    
    // Core Bundle
    import ShikiHighlighter, {
      createHighlighterCore,
      createOnigurumaEngine,
      createJavaScriptRegexEngine,
    } from 'react-shiki/core';
  2. Integrate react-shiki with react-markdown

    main

    To use react-shiki for syntax highlighting within react-markdown, create a custom code component. This component should extract the language from the className (e.g., language-tsx) and use ShikiHighlighter for code blocks, while falling back to a standard <code> tag for inline code.

    Note: Since react-markdown v9.0.0, the inline prop was removed. You must use either the isInlineCode helper or the rehypeInlineCodeProperty plugin to distinguish between inline and block code.

    import ReactMarkdown from "react-markdown";
    import ShikiHighlighter, { isInlineCode } from "react-shiki";
    
    const CodeHighlight = ({ className, children, node, ...props }) => {
      const code = String(children).trim();
      const match = className?.match(/language-(\w+)/);
      const language = match ? match[1] : undefined;
      const isInline = node ? isInlineCode(node) : undefined;
    
      return !isInline ? (
        <ShikiHighlighter language={language} theme="github-dark" {...props}>
          {code}
        </ShikiHighlighter>
      ) : (
        <code className={className} {...props}>
          {code}
        </code>
      );
    };
    
    <ReactMarkdown
      components={{
        code: CodeHighlight,
      }}
    >
      {markdown}
    </ReactMarkdown>
  3. Make multi-themes reactive using light-dark()

    main

    The recommended way to make themes reactive to the user's system preference is to set defaultColor="light-dark()". This utilizes the CSS light-dark() function.

    Note: This requires the site to set the color-scheme CSS property.

    Implementation

    /* Required CSS */
    :root {
      color-scheme: light dark;
    }
    
    /* For class-based dark mode */
    :root.dark {
      color-scheme: dark;
    }
    // Component
    <ShikiHighlighter
      language="tsx"
      theme={{
        light: "github-light",
        dark: "github-dark",
      }}
      defaultColor="light-dark()"
    >
      {code.trim()}
    </ShikiHighlighter>
    
    // Hook
    const highlightedCode = useShikiHighlighter(code, "tsx", {
      light: "github-light",
      dark: "github-dark",
    }, {
      defaultColor: "light-dark()",
    });
    <ShikiHighlighter
      language="tsx"
      theme={{
        light: "github-light",
        dark: "github-dark",
      }}
      defaultColor="light-dark()"
    >
      {code.trim()}
    </ShikiHighlighter>
  4. Preload custom languages

    main

    If you are highlighting languages dynamically at runtime, you should preload your custom language grammars to ensure they are available.

    import mcfunction from "../langs/mcfunction.tmLanguage.json";
    import bosque from "../langs/bosque.tmLanguage.json";
    
    // Component
    <ShikiHighlighter
      language="typescript"
      theme="github-dark"
      preloadLanguages={[mcfunction, bosque]}
    >
      {code.trim()}
    </ShikiHighlighter>
    
    // Hook
    const highlightedCode = useShikiHighlighter(code, "typescript", "github-dark", {
      preloadLanguages: [mcfunction, bosque],
    });
    NOTE

    Bundled languages are loaded on demand and do not need to be preloaded.

  5. Configure line numbers and highlights

    main

    Line numbers are CSS-based and can be enabled via showLineNumbers. You can also specify which lines to highlight using highlightLineNumbers.

    Implementation

    // Component
    <ShikiHighlighter 
      language="javascript"
      theme="github-dark"
      showLineNumbers
      startingLineNumber={0} // default is 1
      highlightLineNumbers={[2, 4]}
    >
      {code}
    </ShikiHighlighter>
    
    // Hook
    const highlightedCode = useShikiHighlighter(code, "javascript", "github-dark", {
      showLineNumbers: true,
      startingLineNumber: 0,
      highlightLineNumbers: [2, 4],
    });
    NOTE

    No CSS import is needed. The hook injects line-number and highlight styles automatically when enabled. To customize them, override the following CSS variables or classes:

    • .rs-line-number (line span)
    • .rs-highlighted-line (line span)
    • .rs-has-line-numbers / .rs-has-highlighted-lines (container code element)

    highlightLineNumbers uses displayed line numbers, so it works in conjunction with startingLineNumber.

  6. Expand ESLint configuration for type-aware linting

    main

    For production applications, it is recommended to enable type-aware lint rules in your ESLint configuration. This requires configuring parserOptions to point to your tsconfig files and switching to type-checked plugin recommendations.

    1. Update parserOptions in your ESLint config file:
      • Set ecmaVersion to 'latest'.
      • Set sourceType to 'module'.
      • Provide an array of paths to your tsconfig files in project.
      • Set tsconfigRootDir to __dirname.
    2. Update your extends list:
      • Replace plugin:@typescript-eslint/recommended with plugin:@typescript-eslint/recommended-type-checked or plugin:@typescript-eslint/strict-type-checked.
      • Optionally add plugin:@typescript-eslint/stylistic-type-checked.
      • Install eslint-plugin-react and add plugin:react/recommended and plugin:react/jsx-runtime to the extends list.
    export default {
      // other rules...
      parserOptions: {
        ecmaVersion: 'latest',
        sourceType: 'module',
        project: ['./tsconfig.json', './tsconfig.node.json', './tsconfig.app.json'],
        tsconfigRootDir: __dirname,
      },
    }
  7. Configure the react-shiki/core minimal bundle

    main

    When using the react-shiki/core bundle, you must create a custom highlighter instance using createHighlighterCore. This allows you to dynamically import only the specific themes, languages, and engines you need, significantly reducing the client-side bundle size.

    import ShikiHighlighter, {
      createHighlighterCore,
      createOnigurumaEngine,
      createJavaScriptRegexEngine,
    } from 'react-shiki/core';
    
    // Create custom highlighter with dynamic imports
    const highlighter = await createHighlighterCore({
      themes: [import('@shikijs/themes/nord')],
      langs: [import('@shikijs/langs/typescript')],
      engine: createOnigurumaEngine(import('shiki/wasm')) 
        // or createJavaScriptRegexEngine()
    });
    
    // Pass the custom highlighter to the component
    <ShikiHighlighter highlighter={highlighter} language="typescript" theme="nord">
      {code}
    </ShikiHighlighter>
  8. Integrate with react-markdown

    main

    To use react-shiki within react-markdown, create a custom component that checks if the node is inline code or a code block. Use isInlineCode from react-shiki to distinguish between them.

    import ReactMarkdown from "react-markdown";
    import ShikiHighlighter, { isInlineCode } from "react-shiki";
    
    const CodeHighlight = ({ className, children, node, ...props }) => {
        const code = String(children).trim();
        const match = className?.match(/language-(\w+)/);
        const language = match ? match[1] : undefined;
        const isInline = node ? isInlineCode(node) : undefined;
    
        return !isInline ? (
            <ShikiHighlighter language={language} theme="catppuccin-mocha" {...props}>
                {code}
            </ShikiHighlighter>
        ) : (
            <code className={className} {...props}>
                {code}
            </code>
        );
    };
  9. Handle inline code in react-markdown v9+

    main

    Because react-markdown v9.0.0 removed the inline prop, react-shiki provides two methods to identify inline code:

    Method 1: Use the isInlineCode helper

    Pass the node object from react-markdown to the isInlineCode function. It identifies inline code by checking for the absence of newline characters.

    Method 2: Use the rehypeInlineCodeProperty plugin

    Add rehypeInlineCodeProperty to your rehypePlugins array in ReactMarkdown. This plugin adds an inline prop to your code components by checking if the <code> tag is nested within a <pre> tag.

    Example using the plugin:

    import ReactMarkdown from "react-markdown";
    import { rehypeInlineCodeProperty } from "react-shiki";
    
    // In your component:
    const CodeHighlight = ({ inline, className, children, ...props }) => {
      const code = String(children).trim();
      const match = className?.match(/language-(\w+)/);
      const language = match ? match[1] : undefined;
    
      return !inline ? (
        <ShikiHighlighter language={language} theme="github-dark" {...props}>
          {code}
        </ShikiHighlighter>
      ) : (
        <code className={className} {...props}>
          {code}
        </code>
      );
    };
    
    // In your render:
    <ReactMarkdown
      rehypePlugins={[rehypeInlineCodeProperty]}
      components={{
        code: CodeHighlight,
      }}
    >
      {markdown}
    </ReactMarkdown>
  10. Create a custom highlighter with react-shiki/core

    main

    To optimize client-side bundle size, use react-shiki/core to create a custom highlighter instance. This allows you to dynamically import only the specific themes, languages, and engines you need.

    import ShikiHighlighter, { 
        createHighlighterCore,        // re-exported from shiki/core
        createOnigurumaEngine,        // re-exported from shiki/engine/oniguruma
        createJavaScriptRegexEngine,  // re-exported from shiki/engine/javascript
    } from 'react-shiki/core';
    
    // Create custom highlighter with dynamic imports to optimize client-side bundle size
    const highlighter = await createHighlighterCore({
        themes: [import('@shikijs/themes/ayu-dark')],
        langs: [import('@shikijs/langs/typescript')],
        engine: createOnigurumaEngine(import('shiki/wasm')) 
            // or createJavaScriptRegexEngine()
    });
    
    <ShikiHighlighter highlighter={highlighter} language="typescript" theme="ayu-dark">
        {code.trim()}
    </ShikiHighlighter>