Code
Code shows source code in a monospace block, or inline in a line of text. It highlights the code with Shiki, but raft-ui does not bundle Shiki: you create a highlighter and pass it in, so you choose the regex engine and only ship the languages you use. Without one, Code renders the text plain.
Setting up Shiki
Install shiki and create a highlighter from its fine-grained core build: a regex engine, the languages you need, and raft-ui’s codeTheme. Pass it to a ConfigProvider near the root of your app. Every Code below it uses it; the highlighter prop on one Code overrides it.
import { ConfigProvider, codeTheme } from "raft-ui";
import { createHighlighterCoreSync } from "shiki/core";
import { createJavaScriptRegexEngine } from "shiki/engine/javascript";
import javascript from "shiki/langs/javascript.mjs";
const highlighter = createHighlighterCoreSync({
engine: createJavaScriptRegexEngine(),
langs: [javascript],
themes: [codeTheme],
});
export function App() {
return <ConfigProvider highlighter={highlighter}>{/* … */}</ConfigProvider>;
}Code calls the highlighter’s codeToTokens while it renders and turns the tokens into <span>s, so a ready highlighter also highlights during server rendering.
Colors
codeTheme is Shiki’s CSS variables theme, the same object createCssVariablesTheme({ name: "raft-ui", variablePrefix: "--r-code-" }) from shiki/core returns. Its token colors are var(--r-code-token-keyword), var(--r-code-token-string) and so on, and every raft-ui theme defines those variables from its color roles. So the code follows the theme and the light or dark mode without being highlighted again, and you need no Shiki color theme or stylesheet. To recolor a role, override its variable, e.g. --r-code-token-keyword, with themeOverrides.
| Variable | Shiki tokens | Color role |
|---|---|---|
--r-code-foreground | Plain text | label-primary |
--r-code-token-keyword | Keywords, storage, operators | text-accent |
--r-code-token-string, --r-code-token-string-expression | Strings, templates, tags | text-success |
--r-code-token-constant | Numbers, constants, properties | text-warning |
--r-code-token-function | Functions, types, attributes | text-info |
--r-code-token-parameter | Parameters | text-primary |
--r-code-token-comment | Comments | label-tertiary (Brutal: label-secondary) |
--r-code-token-punctuation | Punctuation | label-secondary |
--r-code-token-inserted, -deleted, -changed | Diff lines | text-success, text-error, text-warning |
Brutal also sets keywords and functions bold and keeps comments upright; Elegant sets comments in italic.
Engines and languages
Code only highlights a language loaded into the highlighter, by its name or one of its aliases (js for javascript). Any other language renders plain, and so does a highlighter without codeTheme. Import each grammar from shiki/langs/<name>.mjs.
The JavaScript regex engine is synchronous, so createHighlighterCoreSync gives you a highlighter right away. The Oniguruma engine (shiki/engine/oniguruma with import("shiki/wasm")) matches VS Code exactly, but it loads WebAssembly, so it needs the async createHighlighterCore: Code renders plain until you pass the highlighter. Load the languages before passing a highlighter: Code highlights again when highlighter, code or language changes, not when a language is loaded into the same highlighter later.
Highlighting ahead of time
Static site generators (Astro, MDX with @shikijs/rehype) and servers can run Shiki while they build, so the browser loads no Shiki at all. Code shows their output in two ways, neither of which needs a highlighter:
tokens: the linescodeToTokens(code, { lang, theme: codeTheme.name }).tokensreturns. They are plain data, so a build step can write them to JSON and a server component can pass them to the client.Coderenders them exactly as it renders a highlighter’s.children: lines already rendered as elements, e.g. what MDX makes of the<pre><code>that@shikijs/rehypeoutputs.Coderenders them as they are, inside the same frame.
children win over tokens, and tokens over the highlighter. The copy button copies, and the gutter numbers, the text of the tokens or children: for children it is read from the strings in the React tree, so it works on the server too. Pass code as well when that text is not the source, e.g. when the children are components that render their own text: code is then what is copied. trim only applies to code.
Theme colors at build time
For build-time output to follow the raft-ui theme, highlight it with codeTheme too: every color is then a var(--r-code-*) reference that the current theme fills in. Also add codeTransformer, a Shiki transformer that gives each token <span> the classes Code gives its own tokens (r-code__token r-code__token--keyword), which carry each theme’s weights and styles (Brutal’s bold keywords, Elegant’s italic comments). Import both from raft-ui/shiki: it holds no React and no CSS, so a Node config can load it, while raft-ui imports its stylesheet.
import { codeTheme, codeTransformer } from "raft-ui/shiki";
import { createHighlighter } from "shiki";
const highlighter = await createHighlighter({ langs: ["tsx"], themes: [codeTheme] });
// For `tokens`:
const { tokens } = highlighter.codeToTokens(source, { lang: "tsx", theme: codeTheme.name });
// For `children`, via HAST or HTML:
const html = highlighter.codeToHtml(source, {
lang: "tsx",
theme: codeTheme.name,
transformers: [codeTransformer],
});With @shikijs/rehype, pass the same options: { theme: codeTheme, transformers: [codeTransformer] }.
MDX
MDX on Vite (@mdx-js/rollup) runs @shikijs/rehype while it compiles a page. Give it raft-ui’s theme and transformer:
// vite.config.ts
import mdx from "@mdx-js/rollup";
import rehypeShiki from "@shikijs/rehype";
import { codeTheme, codeTransformer } from "raft-ui/shiki";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
mdx({
providerImportSource: "@mdx-js/react",
rehypePlugins: [[rehypeShiki, { theme: codeTheme, transformers: [codeTransformer] }]],
}),
],
});Then map MDX’s pre to Code. Its child is the <code> element, whose children are the highlighted lines:
import { MDXProvider } from "@mdx-js/react";
import { isValidElement } from "react";
import type { ReactNode } from "react";
import { Code } from "raft-ui";
function Pre({ children }: { children?: ReactNode }) {
const lines = isValidElement<{ children?: ReactNode }>(children)
? children.props.children
: children;
return <Code>{lines}</Code>;
}
const components = { pre: Pre };
export function Page({ children }: { children: ReactNode }) {
return <MDXProvider components={components}>{children}</MDXProvider>;
}This site does the same with its own rehype pass. Astro (markdown.shikiConfig) and Next.js MDX (@shikijs/rehype in rehypePlugins) take the same theme and transformers.
Demos
Basic
A ConfigProvider passes the highlighter to every Code below it. Leading and trailing blank lines are trimmed; set trim={false} to keep them.
Inline
inline renders the code inside a line of text, sized relative to it.
Word wrap
Long lines scroll sideways in a ScrollArea. wordWrap wraps them instead.
Line numbers
showLineNumbers adds a gutter that stays in place while long lines scroll. Line numbers are not read out or copied with the code.
Copy
A block has a small CopyButton in its top-right corner. It shows while the block is hovered or focused, and always on touch screens. It copies the source text, after trim, and shows a check for two seconds. Set copyable={false} to leave it out; inline code never has one.
Loading Shiki asynchronously
A highlighter with the Oniguruma engine is created asynchronously, and here the Go grammar is loaded on demand too. Until it is ready, Code shows the plain text.
Tokens highlighted ahead of time
tokens takes the lines Shiki’s codeToTokens returned elsewhere, here written out as a build step would save them. No highlighter is involved, and line numbers and copying work as with code.
Children rendered ahead of time
children takes lines that are already elements, here a pre mapped to Code the way an MDX site maps the <pre><code> Shiki made at build time. The copy button copies their text.
Without a highlighter
With no highlighter, or a language it has not loaded, the code renders as plain monospace text.
Props
| Name | Type | Default | Description | Version |
|---|---|---|---|---|
children | ReactNode | undefined | Lines highlighted ahead of time, already rendered, e.g. from @shikijs/rehype. Rendered as given, over tokens and the highlighter. | Unreleased |
code | string | undefined | The source text. With children or tokens, only what is copied. | Unreleased |
copyable | boolean | true | Shows a button that copies the source text (after trim). Ignored when inline. | Unreleased |
highlighter | CodeHighlighter | undefined | Shiki highlighter for this Code, loaded with codeTheme. Defaults to the one from ConfigProvider. | Unreleased |
inline | boolean | false | Renders inside a line of text instead of as a block. | Unreleased |
language | string | undefined | A language loaded into highlighter, by name or alias. | Unreleased |
showLineNumbers | boolean | false | Shows a line number gutter. Ignored when inline or wordWrap. | Unreleased |
tokens | CodeToken[][] | undefined | Shiki tokens from codeToTokens with codeTheme, highlighted ahead of time. Rendered over the highlighter. | Unreleased |
trim | boolean | true | Trims leading and trailing whitespace off code. children and tokens render as given. | Unreleased |
wordWrap | boolean | false | Wraps long lines instead of scrolling them. | Unreleased |
CodeHighlighter is the part of Shiki’s HighlighterCore that Code calls (codeToTokens), so any highlighter from createHighlighterCore or createHighlighterCoreSync fits. raft-ui only exports types for it and never imports Shiki at runtime.
raft-ui/shiki exports codeTheme and codeTransformer (also exported from raft-ui) without React or CSS, for build steps.
Safety
The source text only ever becomes text in <span>s; it is never inserted as HTML, highlighted or not. Any code string or tokens is safe to show, including untrusted input. children are React elements and render as such: build them from trusted output, such as your own build step.