kirari

Getting started

Introduction

Kirari turns source code into tokens, and tokens into escaped HTML. No DOM, no runtime dependencies, and every color comes from your CSS.

npm install @vortexlabs/kirari

The API has three core functions: tokenize splits source into typed tokens with exact offsets, render turns tokens into HTML, and highlight does both. Optional entry points add streaming, decorations, and language lookup, and separate packages integrate with Rehype and React.

Getting started

Installation

Install from npm and highlight your first snippet.

Terminal
npm install @vortexlabs/kirari

# or
pnpm add @vortexlabs/kirari
yarn add @vortexlabs/kirari
bun add @vortexlabs/kirari

Highlight your first snippet

JavaScript
import { highlight } from '@vortexlabs/kirari'
import '@vortexlabs/kirari/theme.css'

const html = highlight('const message = "Hello, world!"')
const block = `<pre class="kirari"><code>${html}</code></pre>`

highlight returns the inner HTML; wrap it in your own <pre><code>. JavaScript is the default. class="kirari" on the <pre> applies the theme’s background and text color.

Customize

Languages

Explore every opt-in language and format.

From components to configuration files. Import the definitions you need; the others stay out of your bundle.

Import from /core to ship only Python. The main entry always bundles JavaScript as its default.

JavaScript
import { tokenize, render } from '@vortexlabs/kirari/core'
import { python } from '@vortexlabs/kirari/languages/python'

const html = render(tokenize('print("Hello, world!")', python))
48 languages

JSX and TSX are separate definitions. Use hcl for Terraform and shell for common shell syntax. These are lexical grammars, not compiler parsers.

Customize

Themes

Pick from built-in themes like GitHub, Dracula, Nord, and Catppuccin.

Import one theme and add class="kirari" to your <pre>. Each file only sets fallback colors, so your own --kirari-* variables still win.

TypeScript
import '@vortexlabs/kirari/themes/github-dark.css'

@vortexlabs/kirari/theme.css is the same file as themes/default.css. Ported themes are adaptations of the originals’ published colors, credited in each file’s header. Try any of them on your own code in the playground.

Customize

Custom themes

Style token categories with CSS variables and classes.

Every category gets a class and a CSS variable. No inline colors, no re-highlighting when your theme changes.

CSS
.my-code {
  --kirari-background: #f8f7fc;
  --kirari-text: #35313f;
  --kirari-keyword: #7452bd;
  --kirari-string: #247060;
  --kirari-variable: #35435f;
  --kirari-comment: #74717c;
}

.my-code .kirari-keyword { font-weight: 600; }

Variables need a theme file imported (theme.css or any from Themes); it maps each variable to its class. Set them on any ancestor. No theme file? Style .kirari-keyword and the other classes directly.

A token for every detail

--kirari-text--kirari-keyword--kirari-variable--kirari-property--kirari-function--kirari-string--kirari-number--kirari-boolean--kirari-null--kirari-comment--kirari-operator--kirari-punctuation--kirari-type--kirari-regex--kirari-inserted--kirari-deleted--kirari-heading--kirari-tag--kirari-attribute--kirari-entity

Plain text renders without a span unless you assign it a custom class. --kirari-background controls the optional wrapper background.

Customize

Custom rendering

Use raw tokens and source offsets in your own UI.

Need a selection, a custom component, or a different rendering target? Keep the tokens and take it from there.

JavaScript
import { tokenize, render } from '@vortexlabs/kirari'

const tokens = tokenize('const count = 1')
const html = render(tokens, {
  classes: { comment: 'muted' },
  className: token =>
    token.type === 'variable' ? 'selected-name' : undefined,
})
variable{ type: 'variable', value: 'count', start: 6, end: 11 }

Offsets use UTF-16 code units, just like JavaScript’s slice. The start is inclusive, the end exclusive. Joining token values always reconstructs the original source.

For React or another UI framework, render each token’s value as text and assign classes yourself. For HTML with source-range annotations or line wrappers, use renderDecorated.

Customize

Decorations

Add source ranges and line wrappers to highlighted HTML.

Add classes to source ranges without touching syntax tokens. Optionally wrap each line for line numbers or annotations.

JavaScript
import { tokenize } from '@vortexlabs/kirari'
import { renderDecorated } from '@vortexlabs/kirari/decorations'

const source = 'const answer = 42\nconsole.log(answer)'
const html = renderDecorated(tokenize(source), {
  decorations: [
    { start: 6, end: 12, className: 'kirari-inserted' },
  ],
  lines: true,
})

Output: the first answer gets the inserted color; syntax colors stay.

Output
const answer = 42
console.log(answer)
  • Ranges: UTF-16 offsets, start inclusive, end exclusive. Overlaps combine classes.
  • lines: true wraps each line in .kirari-line with a 1-based data-line. Line endings stay outside.
  • lineClassName({ number, start, end }) adds a class per line.

Pass tokens that cover the whole source. Ranges must be nonempty, in bounds, and not split a surrogate pair. A range edge inside a CRLF needs lines: true. Output is escaped.

Advanced

Streaming

Apply token patches as source chunks arrive.

createTokenStream(language) takes chunks and returns patches. New input can change earlier tokens, so each patch replaces everything after an offset.

TypeScript
import { render } from '@vortexlabs/kirari/core'
import { createTokenStream } from '@vortexlabs/kirari/stream'
import { javascript } from '@vortexlabs/kirari/languages/javascript'
import type { Token } from '@vortexlabs/kirari/core'
import type { TokenPatch } from '@vortexlabs/kirari/stream'

const stream = createTokenStream(javascript)
const tokens: Token[] = []

const apply = (patch: TokenPatch) => {
  while (tokens.length && tokens[tokens.length - 1]!.start >= patch.from) {
    tokens.pop()
  }
  for (const token of patch.tokens) tokens.push(token)
}

apply(stream.write('const ans'))
apply(stream.write('wer = 42;\n'))
apply(stream.end())
const html = render(tokens)

stream.reset()
tokens.length = 0 // Clear consumer state before starting another source.
  • patch.from: source offset (UTF-16), not a token index. Drop tokens from there; append patch.tokens (already globally offset).
  • patch.length: total source length so far.
  • write(chunk) returns a patch. end(chunk?) closes the stream; writes then throw until reset().
  • reset() doesn’t clear your tokens or output. Do that yourself.

For live output, render only patch.tokens and replace that tail of your view. Re-rendering every token on each write repeats work.

Know the streaming limits

Only the unmodified built-in JavaScript, TypeScript, and JSON use 'checkpoint' mode (stream.mode), which rescans less. Everything else, including copies or extensions of those, uses 'full': every write retokenizes all source so far.

Even in checkpoint mode, a long line or unclosed multiline token can trigger repeated rescans. Both modes keep the full source in memory. Don’t mutate built-in definitions; copy them.

Advanced

Markdown fences

Resolve language aliases and highlight fenced code blocks.

Build a registry from the languages you import, then pass it to createMarkdown to highlight fence bodies. The registry resolves names, aliases, and filenames.

JavaScript
import { tokenize, render } from '@vortexlabs/kirari/core'
import { createRegistry } from '@vortexlabs/kirari/registry'
import { createMarkdown } from '@vortexlabs/kirari/markdown'
import { javascript } from '@vortexlabs/kirari/languages/javascript'
import { json } from '@vortexlabs/kirari/languages/json'

const registry = createRegistry([javascript, json])
registry.resolve(' JS ')       // javascript
registry.resolve('data.json')  // json
registry.resolve('app.tsx')    // undefined

const markdown = createMarkdown(registry)
const source = '# Example\n```js\nconst answer = 42\n```'
const html = render(tokenize(source, markdown))
Markdown output
# Example
```js
const answer = 42
```

```json
{"ready": true}
```

The first word after the fence picks the language. Backtick and tilde fences work, even unclosed. Offsets point into the full Markdown source. Unlabeled or unregistered fences stay plain. Fences nested inside a ```markdown fence aren’t highlighted.

The registry never loads grammars or guesses from content. JSX, TSX, JSONC, XML, Vue, and Svelte need their own definitions. Names beat aliases; duplicate names throw. The plain languages/markdown definition doesn’t highlight fences.

Advanced

Rehype & MDX

Highlight code blocks while building Markdown and MDX documents.

@vortexlabs/kirari-rehype highlights code blocks in Markdown and MDX at build time. The browser gets plain HTML and CSS, no highlighter.

Different from Markdown fences: that highlights Markdown source. This plugin highlights code blocks in the rendered document.

Terminal
npm install @vortexlabs/kirari @vortexlabs/kirari-rehype
JavaScript
import { unified } from 'unified'
import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import rehypeStringify from 'rehype-stringify'
import rehypeKirari from '@vortexlabs/kirari-rehype'
import { javascript } from '@vortexlabs/kirari/languages/javascript'

const processor = unified()
  .use(remarkParse)
  .use(remarkRehype)
  .use(rehypeKirari, { languages: [javascript] })
  .use(rehypeStringify)

const html = String(await processor.process(markdown))

Pass the languages you need; labels like js resolve via the registry. Nothing is auto-loaded or detected. Skipped: unknown or unlabeled blocks, inline code, and code that already has child elements.

Use it with MDX

JavaScript
import { compile } from '@mdx-js/mdx'
import rehypeKirari from '@vortexlabs/kirari-rehype'
import { javascript } from '@vortexlabs/kirari/languages/javascript'

const compiled = await compile(markdown, {
  rehypePlugins: [[rehypeKirari, { languages: [javascript] }]],
})

Import @vortexlabs/kirari/theme.css once. Blocks get pre.kirari and the usual .kirari-* classes and variables. Custom MDX code components must forward props and children.

Point readers to a line

A fence such as ```js {2,4-6} adds .kirari-line-highlighted to lines 2 and 4–6. Style it yourself:

CSS
.kirari-line-highlighted {
  background: var(--kirari-line-background, #7452bd18);
}

Other options: lines: true wraps lines without metadata; lineClassName({ number, start, end }) (1-based, UTF-16 offsets within the block); classes and className(token) as in core. Full type: Options.

Order: after remark-rehype, before stringify. With rehype-sanitize, sanitize first and keep language classes. Don’t run a second highlighter on the same blocks. Invalid line metadata, and braces inside quoted meta values, are ignored.

Advanced

React

Render highlighted code as React elements, on the server or while streaming.

<Code> renders tokens as React elements. No dangerouslySetInnerHTML, no hooks, so it works as a Server Component. Requires React 19.

Terminal
npm install @vortexlabs/kirari @vortexlabs/kirari-react
TSX
import { Code } from '@vortexlabs/kirari-react'
import { typescript } from '@vortexlabs/kirari/languages/typescript'
import '@vortexlabs/kirari/theme.css'

export const Example = () => (
  <Code language={typescript} highlightLines={[1, [3, 4]]}>
    {source}
  </Code>
)

Output: pre.kirari with the usual .kirari-* classes. Extra props go to the <pre>.

  • lines: wrap lines in .kirari-line with data-line.
  • highlightLines: lines or [first, last] ranges get .kirari-line-highlighted.
  • lineClassName, decorations, classes, tokenClassName: as in decorations.

Stream a response

TSX
'use client'
import { Code } from '@vortexlabs/kirari-react'
import { useTokenStream } from '@vortexlabs/kirari-react/stream'
import { javascript } from '@vortexlabs/kirari/languages/javascript'

export const StreamedCode = () => {
  const { tokens, write, end } = useTokenStream(javascript)
  // call write(chunk) as text arrives, then end()
  return <Code tokens={tokens} />
}

useTokenStream returns { tokens, write, end, reset } and rescans only the unfinished tail. It is client-only; <Code> is not.

Advanced

Custom grammars

Extend definitions with your own rules and categories.

Copy a definition and put your rules first. Each rule matches at the current source position; earlier rules win.

JavaScript
import { javascript } from '@vortexlabs/kirari/languages/javascript'
import type { Language } from '@vortexlabs/kirari'

const projectJavaScript: Language = {
  ...javascript,
  rules: [
    { type: 'constant', pattern: /\bAPI_KEY\b/ },
    ...javascript.rules,
  ],
}
CSS
.kirari-constant {
  color: var(--kirari-constant, #99631b);
}

Definitions can specify keywords, types, identifier patterns, and rules. An optional tokenize(source, language) callback can replace the scanner for structural syntax. Return complete tokens with contiguous source offsets.

Templates, regex literals, and structural scanners can handle syntax before ordinary rules. Rules do not override every construct in JSX, Vue, Svelte, HTML, CSS, or YAML. Definitions and callbacks are trusted application code.

Reference

API reference

Core functions, optional entry points, and their options.

A small API for the straightforward case, with optional entry points when you need more.

highlight(source, options?)

Returns an escaped HTML fragment. Options accept a language definition, a token-to-class classes map, and a className(token) callback. JavaScript is the default.

tokenize(source, language?)

Returns Token[] with type, value, start, and end. The main @vortexlabs/kirari entry defaults to JavaScript; @vortexlabs/kirari/core requires an explicit language.

render(tokens, options?)

Returns escaped HTML from readonly tokens. Custom classes append to Kirari’s standard classes. Rendering does not mutate tokens.

Package entry points
ImportWhat you get
@vortexlabs/kirariAll three core functions, with JavaScript defaults
@vortexlabs/kirari/coretokenize, render, token types; no default language
@vortexlabs/kirari/streamcreateTokenStream, TokenStream, TokenPatch
@vortexlabs/kirari/decorationsrenderDecorated, Decoration, DecoratedRenderOptions
@vortexlabs/kirari/registrycreateRegistry, LanguageRegistry
@vortexlabs/kirari/markdowncreateMarkdown(registry)
@vortexlabs/kirari/languages/*One named language definition
@vortexlabs/kirari/theme.cssOptional theme, driven by CSS variables
@vortexlabs/kirari-rehypeOptional Markdown and MDX plugin, Options

Core TypeScript types include Token, TokenType, Language, Rule, RenderOptions, and HighlightOptions. Import optional APIs and their types from their own entry points.

Reference

Scope & limits

Lexical coverage, trusted grammars, and runtime support.

Kirari highlights syntax. It doesn’t validate programs, resolve bindings, or infer types. Incomplete code is preserved, even when its classification is approximate.

Language coverage

Context-sensitive syntax uses heuristics. Most non-JavaScript interpolation remains inside string tokens. Markdown fences are opaque by default; createMarkdown enables registered fence languages. SQL dialect extensions and shell heredocs are not fully parsed. TSX type arguments on JSX tags, including nested arguments, preserve tag and attribute boundaries. Generic arrow functions, comparisons, and incomplete markup still use lexical heuristics.

Vue v-pre and expressions inside quoted Svelte attributes are not parsed. Unknown script/style preprocessors remain plain text. Advanced reader forms, custom string macros, and configuration dialects can receive approximate categories.

Runtime and security

Modern ESM runtimes with ES2022 features are required. The library has no DOM, Node, or Bun runtime imports. Source and class attributes are escaped. Custom regex rules and callbacks are trusted code; arbitrary user-supplied grammars are not sandboxed.

Bundle measurements

The JavaScript entry measures 2,879 bytes gzip; all 48 definitions together measure 18,650 bytes gzip. Measured with Bun 1.3.4 minification and gzip level 9, excluding CSS. These are library bundle sizes, not website sizes or throughput benchmarks.

To reproduce, run bun run size in the Kirari repository.

Search documentation

↑↓ to move↵ to open

kirari